Merge pull request #3190 from Infisical/revert-3189-revert-3128-daniel/view-secret-value-permission

feat(api/secrets): view secret value permission 2
This commit is contained in:
Maidul Islam
2025-03-10 23:19:39 -04:00
committed by GitHub
77 changed files with 2776 additions and 1216 deletions
@@ -120,4 +120,3 @@ export default {
}; };
} }
}; };
+1
View File
@@ -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()
+2 -2
View File
@@ -1,6 +1,6 @@
import z from "zod"; import z from "zod";
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { RAW_SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
@@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
const AccessListEntrySchema = z const AccessListEntrySchema = z
.object({ .object({
allowedActions: z.nativeEnum(ProjectPermissionActions).array(), allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(),
id: z.string(), id: z.string(),
membershipId: z.string(), membershipId: z.string(),
name: z.string() name: z.string()
@@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 200: z.object({
secretVersions: secretRawSchema.array() secretVersions: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
}) })
} }
}, },
@@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
offset: req.query.offset, offset: req.query.offset,
secretId: req.params.secretId secretId: req.params.secretId
}); });
return { secretVersions }; return { secretVersions };
} }
}); });
+5 -7
View File
@@ -1,10 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; import { SecretSnapshotsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => { export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
secretVersions: secretRawSchema secretVersions: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true }) .omit({ _id: true, environment: true, workspace: true, type: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretId: z.string(), secretId: z.string(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
}).array()
}) })
.array(), .array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
@@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId id: req.params.secretSnapshotId
}); });
return { secretSnapshot }; return { secretSnapshot };
} }
}); });
@@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
@@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
body: z.object({ body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
isTemporary: z.literal(false) isTemporary: z.literal(false)
@@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }), z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
z.object({ z.object({
@@ -3,6 +3,7 @@ import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), permissions: ProjectPermissionV2Schema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
isTemporary: z.literal(false) isTemporary: z.literal(false)
@@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }), z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
z.object({ z.object({
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs"; import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -37,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.CREATE.slug), .describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions) permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -92,7 +95,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.UPDATE.slug), .describe(PROJECT_ROLE.UPDATE.slug),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.UPDATE.permissions)
.optional()
.superRefine(checkForInvalidPermissionCombination)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1,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
); );
@@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
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 { 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";
@@ -77,6 +77,7 @@ import {
TSecretApprovalDetailsDTO, TSecretApprovalDetailsDTO,
TStatusChangeDTO TStatusChangeDTO
} from "./secret-approval-request-types"; } from "./secret-approval-request-types";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
type TSecretApprovalRequestServiceFactoryDep = { type TSecretApprovalRequestServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -88,7 +89,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 +112,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 +918,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 +1007,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,
@@ -1363,9 +1371,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) action = ProjectPermissionSecretActions.Delete;
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;
}; };
+3
View File
@@ -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
View File
@@ -1,4 +1,5 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
export class DatabaseError extends Error { export class DatabaseError extends Error {
name: string; name: string;
@@ -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,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,
+58 -99
View File
@@ -1,13 +1,7 @@
import picomatch from "picomatch"; import picomatch from "picomatch";
import { z } from "zod"; import { z } from "zod";
import { import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
SecretApprovalRequestsSchema,
SecretsSchema,
SecretTagsSchema,
SecretType,
ServiceTokenScopes
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -23,7 +17,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas";
const SecretReferenceNode = z.object({ const SecretReferenceNode = z.object({
key: z.string(), key: z.string(),
@@ -31,6 +25,14 @@ const SecretReferenceNode = z.object({
environment: z.string(), environment: z.string(),
secretPath: z.string() secretPath: z.string()
}); });
const convertStringBoolean = (defaultValue: boolean = false) => {
return z
.enum(["true", "false"])
.default(defaultValue ? "true" : "false")
.transform((value) => value === "true");
};
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] }; type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
@@ -75,17 +77,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ tags: SanitizedTagSchema.array()
tags: SecretTagsSchema.pick({ })
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
})
)
}) })
} }
}, },
@@ -139,13 +133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
}) })
}) })
} }
@@ -247,21 +235,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
expandSecretReferences: z viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue),
.enum(["true", "false"]) expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand),
.default("false") recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive),
.transform((value) => value === "true") include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports),
.describe(RAW_SECRETS.LIST.expand),
recursive: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.recursive),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.includeImports),
tagSlugs: z tagSlugs: z
.string() .string()
.describe(RAW_SECRETS.LIST.tagSlugs) .describe(RAW_SECRETS.LIST.tagSlugs)
@@ -274,15 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array(), .array(),
imports: z imports: z
@@ -293,6 +264,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.omit({ createdAt: true, updatedAt: true }) .omit({ createdAt: true, updatedAt: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional() secretMetadata: ResourceMetadataSchema.optional()
}) })
.array() .array()
@@ -342,6 +314,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences, expandSecretReferences: req.query.expandSecretReferences,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: workspaceId, projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
path: secretPath, path: secretPath,
metadataFilter: req.query.metadataFilter, metadataFilter: req.query.metadataFilter,
includeImports: req.query.include_imports, includeImports: req.query.include_imports,
@@ -376,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")
]) ])
@@ -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">;
@@ -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);
@@ -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
}; };
}; };
+44 -1
View File
@@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => {
} }
}; };
const findManySecretsWithTags = async (
filter: {
secretIds: string[];
type: SecretType;
},
tx?: Knex
) => {
try {
const secrets = await (tx || db.replicaNode())(TableName.Secret)
.whereIn(`${TableName.Secret}.id` as "id", filter.secretIds)
.where("type", filter.type)
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
const data = sqlNestRelationships({
data: secrets,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get many secrets with tags" });
}
};
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
try { try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
@@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => {
upsertSecretReferences, upsertSecretReferences,
findReferencedSecretReferences, findReferencedSecretReferences,
findAllProjectSecretValues, findAllProjectSecretValues,
pruneSecretReminders pruneSecretReminders,
findManySecretsWithTags
}; };
}; };
+38 -15
View File
@@ -1,5 +1,4 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { subject } from "@casl/ability";
import path from "path"; import path from "path";
import { import {
@@ -12,8 +11,9 @@ import {
TSecretFolders, TSecretFolders,
TSecrets TSecrets
} from "@app/db/schemas"; } from "@app/db/schemas";
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { import {
buildSecretBlindIndexFromName, buildSecretBlindIndexFromName,
@@ -51,6 +51,8 @@ import {
TUpdateManySecretsRawFnFactory TUpdateManySecretsRawFnFactory
} from "./secret-types"; } from "./secret-types";
export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = "<hidden-by-infisical>";
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
const appCfg = getConfig(); const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({ const secretBlindIndex = await buildSecretBlindIndexFromName({
@@ -189,13 +191,10 @@ export const recursivelyGetSecretPaths = ({
// Filter out paths that the user does not have permission to access, and paths that are not in the current path // Filter out paths that the user does not have permission to access, and paths that are not in the current path
const allowedPaths = paths.filter( const allowedPaths = paths.filter(
(folder) => (folder) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { secretPath: folder.path
environment, }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
secretPath: folder.path
})
) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
); );
return allowedPaths; return allowedPaths;
@@ -344,6 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
export const decryptSecretRaw = ( export const decryptSecretRaw = (
secret: TSecrets & { secret: TSecrets & {
secretValueHidden: boolean;
workspace: string; workspace: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -362,12 +362,14 @@ export const decryptSecretRaw = (
key key
}); });
const secretValue = decryptSymmetric128BitHexKeyUTF8({ const secretValue = !secret.secretValueHidden
ciphertext: secret.secretValueCiphertext, ? decryptSymmetric128BitHexKeyUTF8({
iv: secret.secretValueIV, ciphertext: secret.secretValueCiphertext,
tag: secret.secretValueTag, iv: secret.secretValueIV,
key tag: secret.secretValueTag,
}); key
})
: INFISICAL_SECRET_VALUE_HIDDEN_MASK;
let secretComment = ""; let secretComment = "";
@@ -385,6 +387,7 @@ export const decryptSecretRaw = (
secretPath: secret.secretPath, secretPath: secret.secretPath,
workspace: secret.workspace, workspace: secret.workspace,
environment: secret.environment, environment: secret.environment,
secretValueHidden: secret.secretValueHidden,
secretValue, secretValue,
secretComment, secretComment,
version: secret.version, version: secret.version,
@@ -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
};
};
+2 -1
View File
@@ -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) {
+293 -99
View File
@@ -6,14 +6,23 @@ import {
ActionProjectType, ActionProjectType,
ProjectMembershipRole, ProjectMembershipRole,
ProjectUpgradeStatus, ProjectUpgradeStatus,
ProjectVersion,
SecretEncryptionAlgo, SecretEncryptionAlgo,
SecretKeyEncoding, SecretKeyEncoding,
SecretsSchema, SecretsSchema,
SecretType SecretType
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
@@ -48,6 +57,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri
import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { import {
conditionallyHideSecretValue,
decryptSecretRaw, decryptSecretRaw,
fnSecretBlindIndexCheck, fnSecretBlindIndexCheck,
fnSecretBulkDelete, fnSecretBulkDelete,
@@ -71,6 +81,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
}; };
}; };
+12 -2
View File
@@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">; type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
@@ -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 })
); );
}); });
@@ -1,13 +1,21 @@
import { useState } from "react"; /* eslint-disable no-nested-ternary */
import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; import { useEffect, useState } from "react";
import {
faChevronRight,
faExclamationTriangle,
faEye,
faEyeSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { AxiosError } from "axios";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useGetSecretReferenceTree } from "@app/hooks/api"; import { useGetSecretReferenceTree } from "@app/hooks/api";
import { TSecretReferenceTraceNode } from "@app/hooks/api/types"; import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types";
import style from "./SecretReferenceDetails.module.css"; import style from "./SecretReferenceDetails.module.css";
@@ -84,7 +92,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
const { data, isPending } = useGetSecretReferenceTree({ const { data, isPending, isError, error } = useGetSecretReferenceTree({
secretPath, secretPath,
environmentSlug: environment, environmentSlug: environment,
projectId, projectId,
@@ -94,6 +102,26 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const tree = data?.tree; const tree = data?.tree;
const secretValue = data?.value; const secretValue = data?.value;
useEffect(() => {
if (error instanceof AxiosError) {
const err = error?.response?.data as TApiErrors;
if (err?.error === ApiErrorTypes.CustomForbiddenError) {
createNotification({
title: "You don't have permission to view reference tree",
text: "You don't have permission to view one or more of the referenced secrets.",
type: "error"
});
return;
}
createNotification({
title: "Error fetching secret reference tree",
text: "Please try again later.",
type: "error"
});
}
}, [error]);
if (isPending) { if (isPending) {
return ( return (
<div className="flex items-center justify-center py-4"> <div className="flex items-center justify-center py-4">
@@ -114,11 +142,16 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
</FormControl> </FormControl>
<FormLabel className="mb-2" label="Reference Tree" /> <FormLabel className="mb-2" label="Reference Tree" />
<div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200"> <div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200">
{tree && ( {isError ? (
<div className="flex items-center justify-center py-4">
<FontAwesomeIcon icon={faExclamationTriangle} className="mr-2 text-red-500" />
<p className="text-red-500">Error fetching secret reference tree</p>
</div>
) : tree ? (
<ul className={style.tree}> <ul className={style.tree}>
<SecretReferenceNode node={tree} isRoot secretKey={secretKey} /> <SecretReferenceNode node={tree} isRoot secretKey={secretKey} />
</ul> </ul>
)} ) : null}
</div> </div>
<div className="mt-2 text-sm text-mineshaft-400"> <div className="mt-2 text-sm text-mineshaft-400">
Click a secret key to view its sub-references. Click a secret key to view its sub-references.
+22
View File
@@ -0,0 +1,22 @@
import { twMerge } from "tailwind-merge";
import { Tooltip } from "../Tooltip/Tooltip";
interface IProps {
className?: string;
tooltipText?: string;
}
export const Blur = ({ className, tooltipText }: IProps) => {
return (
<Tooltip content={tooltipText} isDisabled={!tooltipText}>
<div
className={twMerge("flex w-80 flex-grow items-center py-1 pl-4 pr-2", className)}
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
</Tooltip>
);
};
@@ -0,0 +1 @@
export { Blur } from "./Blur";
@@ -120,6 +120,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused; const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
const { data: secrets } = useGetProjectSecrets({ const { data: secrets } = useGetProjectSecrets({
viewSecretValue: false,
environment: suggestionSource.environment || "", environment: suggestionSource.environment || "",
secretPath: suggestionSource.secretPath || "", secretPath: suggestionSource.secretPath || "",
workspaceId, workspaceId,
@@ -7,6 +7,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 ProjectPermissionDynamicSecretActions { export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential", ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential", CreateRootCredential = "create-root-credential",
@@ -138,7 +147,7 @@ export type SecretImportSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionSecretActions,
( (
| ProjectPermissionSub.Secrets | ProjectPermissionSub.Secrets
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
@@ -1,4 +1,5 @@
export { export {
useGetAccessibleSecrets,
useGetProjectSecretsDetails, useGetProjectSecretsDetails,
useGetProjectSecretsOverview, useGetProjectSecretsOverview,
useGetProjectSecretsQuickSearch useGetProjectSecretsQuickSearch
@@ -11,6 +11,7 @@ import {
DashboardSecretsOrderBy, DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch, TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse, TDashboardProjectSecretsQuickSearchResponse,
TGetAccessibleSecretsDTO,
TGetDashboardProjectSecretsByKeys, TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO, TGetDashboardProjectSecretsOverviewDTO,
@@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { groupBy, unique } from "@app/lib/fn/array"; import { groupBy, unique } from "@app/lib/fn/array";
import { SecretV3Raw } from "../types";
export const dashboardKeys = { export const dashboardKeys = {
all: () => ["dashboard"] as const, all: () => ["dashboard"] as const,
getDashboardSecrets: ({ getDashboardSecrets: ({
@@ -58,6 +61,17 @@ export const dashboardKeys = {
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }), ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
"quick-search", "quick-search",
params params
] as const,
getAccessibleSecrets: ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) =>
[
...dashboardKeys.all(),
"accessible-secrets",
{ projectId, secretPath, environment, filterByAction }
] as const ] as const
}; };
@@ -207,6 +221,7 @@ export const useGetProjectSecretsDetails = (
search = "", search = "",
includeSecrets, includeSecrets,
includeFolders, includeFolders,
viewSecretValue,
includeImports, includeImports,
includeDynamicSecrets, includeDynamicSecrets,
tags tags
@@ -231,6 +246,7 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -247,6 +263,7 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -292,6 +309,22 @@ export const fetchProjectSecretsQuickSearch = async ({
return data; return data;
}; };
const fetchAccessibleSecrets = async ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>(
"/api/v1/dashboard/accessible-secrets",
{
params: { projectId, secretPath, environment, filterByAction }
}
);
return data.secrets;
};
export const useGetProjectSecretsQuickSearch = ( export const useGetProjectSecretsQuickSearch = (
{ {
projectId, projectId,
@@ -354,3 +387,32 @@ export const useGetProjectSecretsQuickSearch = (
placeholderData: (previousData) => previousData placeholderData: (previousData) => previousData
}); });
}; };
export const useGetAccessibleSecrets = ({
projectId,
secretPath,
environment,
filterByAction,
options
}: TGetAccessibleSecretsDTO & {
options?: Omit<
UseQueryOptions<
SecretV3Raw[],
unknown,
SecretV3Raw[],
ReturnType<typeof dashboardKeys.getAccessibleSecrets>
>,
"queryKey" | "queryFn"
>;
}) => {
return useQuery({
...options,
queryKey: dashboardKeys.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction
}),
queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction })
});
};
+11
View File
@@ -1,3 +1,4 @@
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -69,6 +70,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
TGetDashboardProjectSecretsOverviewDTO, TGetDashboardProjectSecretsOverviewDTO,
"environments" "environments"
> & { > & {
viewSecretValue: boolean;
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
tags: Record<string, boolean>; tags: Record<string, boolean>;
@@ -100,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = {
environment: string; environment: string;
keys: string[]; keys: string[];
}; };
export type TGetAccessibleSecretsDTO = {
projectId: string;
secretPath: string;
environment: string;
filterByAction:
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;
};
+1 -5
View File
@@ -10,18 +10,14 @@ import {
formatedConditionsOperatorNames, formatedConditionsOperatorNames,
PermissionConditionOperators PermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types"; } from "@app/context/ProjectPermissionContext/types";
import { camelCaseToSpaces } from "@app/lib/fn/string";
import { ApiErrorTypes, TApiErrors } from "./types"; import { ApiErrorTypes, TApiErrors } from "./types";
// this is saved in react-query cache // this is saved in react-query cache
export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"]; export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"]; export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
const camelCaseToSpaces = (input: string) => {
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
mutationCache: new MutationCache({ mutationCache: new MutationCache({
onError: (error) => { onError: (error) => {
@@ -79,6 +79,7 @@ export const decryptSecrets = (
id: encSecret.id, id: encSecret.id,
env: encSecret.environment, env: encSecret.environment,
key: secretKey, key: secretKey,
secretValueHidden: encSecret.secretValueHidden,
value: secretValue, value: secretValue,
tags: encSecret.tags, tags: encSecret.tags,
comment: secretComment, comment: secretComment,
@@ -137,6 +137,7 @@ export const useGetImportedSecretsSingleEnv = ({
env: encSecret.environment, env: encSecret.environment,
key: encSecret.secretKey, key: encSecret.secretKey,
value: encSecret.secretValue, value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags, tags: encSecret.tags,
comment: encSecret.secretComment, comment: encSecret.secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
@@ -176,6 +177,7 @@ export const useGetImportedSecretsAllEnvs = ({
env: encSecret.environment, env: encSecret.environment,
key: encSecret.secretKey, key: encSecret.secretKey,
value: encSecret.secretValue, value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags, tags: encSecret.tags,
comment: encSecret.secretComment, comment: encSecret.secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
@@ -75,6 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
id: secretVersion.secretId, id: secretVersion.secretId,
env: data.environment.slug, env: data.environment.slug,
key: secretVersion.secretKey, key: secretVersion.secretKey,
secretValueHidden: secretVersion.secretValueHidden,
value: secretVersion.secretValue || "", value: secretVersion.secretValue || "",
tags: secretVersion.tags, tags: secretVersion.tags,
comment: secretVersion.secretComment, comment: secretVersion.secretComment,
+19 -5
View File
@@ -26,8 +26,13 @@ import {
export const secretKeys = { export const secretKeys = {
// this is also used in secretSnapshot part // this is also used in secretSnapshot part
getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) => getProjectSecret: ({
[{ workspaceId, environment, secretPath }, "secrets"] as const, workspaceId,
environment,
secretPath,
viewSecretValue
}: TGetProjectSecretsKey) =>
[{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const,
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
getSecretAccessList: ({ getSecretAccessList: ({
workspaceId, workspaceId,
@@ -44,13 +49,15 @@ export const fetchProjectSecrets = async ({
environment, environment,
secretPath, secretPath,
includeImports, includeImports,
expandSecretReferences expandSecretReferences,
viewSecretValue
}: TGetProjectSecretsKey) => { }: TGetProjectSecretsKey) => {
const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", { const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", {
params: { params: {
environment, environment,
workspaceId, workspaceId,
secretPath, secretPath,
viewSecretValue,
expandSecretReferences, expandSecretReferences,
include_imports: includeImports include_imports: includeImports
} }
@@ -68,6 +75,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
env: el.environment, env: el.environment,
key: el.secretKey, key: el.secretKey,
value: el.secretValue, value: el.secretValue,
secretValueHidden: el.secretValueHidden,
tags: el.tags || [], tags: el.tags || [],
comment: el.secretComment || "", comment: el.secretComment || "",
reminderRepeatDays: el.secretReminderRepeatDays, reminderRepeatDays: el.secretReminderRepeatDays,
@@ -107,6 +115,7 @@ export const useGetProjectSecrets = ({
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath,
viewSecretValue,
options options
}: TGetProjectSecretsDTO & { }: TGetProjectSecretsDTO & {
options?: Omit< options?: Omit<
@@ -123,8 +132,13 @@ export const useGetProjectSecrets = ({
...options, ...options,
// wait for all values to be available // wait for all values to be available
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }), queryKey: secretKeys.getProjectSecret({
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), workspaceId,
environment,
secretPath,
viewSecretValue
}),
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }),
select: useCallback( select: useCallback(
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets), (data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
[] []
+5
View File
@@ -19,6 +19,7 @@ export type EncryptedSecret = {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHidden: boolean;
__v: number; __v: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -37,6 +38,7 @@ export type SecretV3RawSanitized = {
version: number; version: number;
key: string; key: string;
value?: string; value?: string;
secretValueHidden: boolean;
comment?: string; comment?: string;
reminderRepeatDays?: number | null; reminderRepeatDays?: number | null;
reminderNote?: string | null; reminderNote?: string | null;
@@ -61,6 +63,7 @@ export type SecretV3Raw = {
environment: string; environment: string;
version: number; version: number;
type: string; type: string;
secretValueHidden: boolean;
secretKey: string; secretKey: string;
secretPath: string; secretPath: string;
secretValue?: string; secretValue?: string;
@@ -95,6 +98,7 @@ export type SecretVersions = {
envId: string; envId: string;
secretKey: string; secretKey: string;
secretValue?: string; secretValue?: string;
secretValueHidden: boolean;
secretComment?: string; secretComment?: string;
tags: WsTag[]; tags: WsTag[];
__v: number; __v: number;
@@ -115,6 +119,7 @@ export type TGetProjectSecretsKey = {
environment: string; environment: string;
secretPath?: string; secretPath?: string;
includeImports?: boolean; includeImports?: boolean;
viewSecretValue?: boolean;
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
}; };
+8 -1
View File
@@ -47,7 +47,8 @@ export enum ApiErrorTypes {
PermissionBoundaryError = "PermissionBoundaryError", PermissionBoundaryError = "PermissionBoundaryError",
BadRequestError = "BadRequest", BadRequestError = "BadRequest",
UnauthorizedError = "UnauthorizedError", UnauthorizedError = "UnauthorizedError",
ForbiddenError = "PermissionDenied" ForbiddenError = "PermissionDenied",
CustomForbiddenError = "ForbiddenError"
} }
export type TApiErrors = export type TApiErrors =
@@ -70,6 +71,12 @@ export type TApiErrors =
details: PureAbility["rules"]; details: PureAbility["rules"];
statusCode: 403; statusCode: 403;
} }
| {
reqId: string;
error: ApiErrorTypes.CustomForbiddenError;
message: string;
statusCode: 403;
}
| { | {
reqId: string; reqId: string;
statusCode: 400; statusCode: 400;
+36
View File
@@ -0,0 +1,36 @@
import { MongoAbility, subject } from "@casl/ability";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import {
ProjectPermissionSecretActions,
ProjectPermissionSub,
SecretSubjectFields
} from "@app/context/ProjectPermissionContext/types";
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;
}
+4
View File
@@ -7,3 +7,7 @@ export const formatReservedPaths = (secretPath: string) => {
} }
return secretPath; return secretPath;
}; };
export const camelCaseToSpaces = (input: string) => {
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};
@@ -58,6 +58,7 @@ const schema = z.object({
permissions: z permissions: z
.object({ .object({
read: z.boolean(), read: z.boolean(),
readValue: z.boolean(),
write: z.boolean() write: z.boolean()
}) })
.required() .required()
@@ -296,6 +297,7 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
name="permissions" name="permissions"
defaultValue={{ defaultValue={{
read: true, read: true,
readValue: false,
write: false write: false
}} }}
render={({ field: { onChange, value }, fieldState: { error } }) => { render={({ field: { onChange, value }, fieldState: { error } }) => {
@@ -35,12 +35,13 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
title, title,
isDisabled isDisabled
}: Props<T>) => { }: Props<T>) => {
const { control } = useFormContext<TFormSchema>(); const { control, watch } = useFormContext<TFormSchema>();
const items = useFieldArray({ const items = useFieldArray({
control, control,
name: `permissions.${subject}` name: `permissions.${subject}`
}); });
const [isOpen, setIsOpen] = useToggle(); const [isOpen, setIsOpen] = useToggle();
// const [hideFullReadAccess, setHideFullReadAccess] = useState(false);
if (!items.fields.length) return <div />; if (!items.fields.length) return <div />;
@@ -71,116 +72,138 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
</div> </div>
{isOpen && ( {isOpen && (
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6"> <div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6">
{items.fields.map((el, rootIndex) => ( {items.fields.map((el, rootIndex) => {
<div key={el.id} className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md"> let isFullReadAccessEnabled = false;
{isConditionalSubjects(subject) && (
<div className="mb-6 mt-4 flex w-full items-center text-gray-300"> if (subject === ProjectPermissionSub.Secrets) {
<div className="w-1/4">Permission</div> isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
<div className="mr-4 w-1/4"> }
<Controller
defaultValue={false as any} return (
name={`permissions.${subject}.${rootIndex}.inverted`} <div
render={({ field }) => ( key={el.id}
<Select className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md"
value={String(field.value)} >
onValueChange={(val) => field.onChange(val === "true")} {isConditionalSubjects(subject) && (
containerClassName="w-full" <div className="mb-6 mt-4 flex w-full items-center text-gray-300">
className="w-full" <div className="w-1/4">Permission</div>
isDisabled={isDisabled} <div className="mr-4 w-1/4">
>
<SelectItem value="false">Allow</SelectItem>
<SelectItem value="true">Forbid</SelectItem>
</Select>
)}
/>
</div>
<div>
<Tooltip
asChild
content={
<>
<p>
Whether to allow or forbid the selected actions when the following
conditions (if any) are met.
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
>
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="text-gray-400" />
</Tooltip>
</div>
</div>
)}
<div className="flex text-gray-300">
<div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }) => {
if (typeof value !== "string") return undefined;
return (
<Controller <Controller
key={`${el.id}-${label}`} defaultValue={false as any}
name={`permissions.${subject}.${rootIndex}.${value}` as any} name={`permissions.${subject}.${rootIndex}.inverted`}
control={control}
defaultValue={false}
render={({ field }) => ( render={({ field }) => (
<div className="flex items-center justify-center"> <Select
<Checkbox value={String(field.value)}
isDisabled={isDisabled} onValueChange={(val) => field.onChange(val === "true")}
isChecked={Boolean(field.value)} containerClassName="w-full"
onCheckedChange={field.onChange} className="w-full"
id={`permissions.${subject}.${rootIndex}.${String(value)}`} isDisabled={isDisabled}
> >
{label} <SelectItem value="false">Allow</SelectItem>
</Checkbox> <SelectItem value="true">Forbid</SelectItem>
</div> </Select>
)} )}
/> />
); </div>
<div>
<Tooltip
asChild
content={
<>
<p>
Whether to allow or forbid the selected actions when the following
conditions (if any) are met.
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
>
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="text-gray-400" />
</Tooltip>
</div>
</div>
)}
<div className="flex text-gray-300">
<div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }, index) => {
if (typeof value !== "string") return undefined;
if (
subject === ProjectPermissionSub.Secrets &&
value === "read" &&
!isFullReadAccessEnabled
) {
return null;
}
return (
<Controller
key={`${el.id}-${index + 1}`}
name={`permissions.${subject}.${rootIndex}.${value}` as any}
control={control}
defaultValue={false}
render={({ field }) => {
return (
<div className="flex items-center justify-center">
<Checkbox
isDisabled={isDisabled}
isChecked={Boolean(field.value)}
onCheckedChange={field.onChange}
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
>
{label}
</Checkbox>
</div>
);
}}
/>
);
})}
</div>
</div>
{children &&
cloneElement(children, {
position: rootIndex
})} })}
<div
className={twMerge(
"mt-4 flex justify-start space-x-4",
isConditionalSubjects(subject) && "justify-end"
)}
>
{!isDisabled && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-2"
onClick={() => {
items.insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add policy
</Button>
)}
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="mt-2 hover:border-red"
onClick={() => items.remove(rootIndex)}
isDisabled={isDisabled}
>
Remove policy
</Button>
)}{" "}
</div> </div>
</div> </div>
{children && );
cloneElement(children, { })}
position: rootIndex
})}
<div
className={twMerge(
"mt-4 flex justify-start space-x-4",
isConditionalSubjects(subject) && "justify-end"
)}
>
{!isDisabled && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-2"
onClick={() => {
items.insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add policy
</Button>
)}
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="mt-2 hover:border-red"
onClick={() => items.remove(rootIndex)}
isDisabled={isDisabled}
>
Remove policy
</Button>
)}{" "}
</div>
</div>
))}
</div> </div>
)} )}
</div> </div>
@@ -1,5 +1,9 @@
import { ReactNode } from "react";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod"; import { z } from "zod";
import { Tooltip } from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
@@ -9,6 +13,7 @@ import {
PermissionConditionOperators, PermissionConditionOperators,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
TPermissionCondition, TPermissionCondition,
TPermissionConditionOperators TPermissionConditionOperators
@@ -22,6 +27,15 @@ const GeneralPolicyActionSchema = z.object({
create: z.boolean().optional() create: z.boolean().optional()
}); });
const SecretPolicyActionSchema = z.object({
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
[ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(),
[ProjectPermissionSecretActions.Edit]: z.boolean().optional(),
[ProjectPermissionSecretActions.Delete]: z.boolean().optional(),
[ProjectPermissionSecretActions.Create]: z.boolean().optional()
});
const CmekPolicyActionSchema = z.object({ const CmekPolicyActionSchema = z.object({
read: z.boolean().optional(), read: z.boolean().optional(),
edit: z.boolean().optional(), edit: z.boolean().optional(),
@@ -114,7 +128,7 @@ export const projectRoleFormSchema = z.object({
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
permissions: z permissions: z
.object({ .object({
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ [ProjectPermissionSub.Secrets]: SecretPolicyActionSchema.extend({
inverted: z.boolean().optional(), inverted: z.boolean().optional(),
conditions: ConditionSchema conditions: ConditionSchema
}) })
@@ -283,6 +297,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
}); });
return; return;
} }
if (subject === ProjectPermissionSub.Secrets) {
const canDescribeAndReadValue = action.includes(
ProjectPermissionSecretActions.DescribeAndReadValue
);
const canDescribe = action.includes(ProjectPermissionSecretActions.DescribeSecret);
const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
const canEdit = action.includes(ProjectPermissionSecretActions.Edit);
const canDelete = action.includes(ProjectPermissionSecretActions.Delete);
const canCreate = action.includes(ProjectPermissionSecretActions.Create);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
describeSecret: canDescribe,
read: canDescribeAndReadValue,
readValue: canReadValue,
create: canCreate,
edit: canEdit,
delete: canDelete,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
// for other subjects // for other subjects
const canRead = action.includes(ProjectPermissionActions.Read); const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit); const canEdit = action.includes(ProjectPermissionActions.Edit);
@@ -470,7 +511,7 @@ export type TProjectPermissionObject = {
[K in ProjectPermissionSub]: { [K in ProjectPermissionSub]: {
title: string; title: string;
actions: { actions: {
label: string; label: string | ReactNode;
value: keyof Omit< value: keyof Omit<
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number], NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
"conditions" | "inverted" "conditions" | "inverted"
@@ -483,10 +524,35 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Secrets]: { [ProjectPermissionSub.Secrets]: {
title: "Secrets", title: "Secrets",
actions: [ actions: [
{ label: "Read", value: "read" }, {
{ label: "Create", value: "create" }, label: (
{ label: "Modify", value: "edit" }, <div className="flex items-center gap-1.5">
{ label: "Remove", value: "delete" } <p className="opacity-60">
Read <span className="text-xs opacity-80">(legacy)</span>
</p>
<Tooltip
className="overflow-hidden whitespace-normal"
content={
<div>
This is a legacy action and will be removed in the future.
<br />
<br /> You should instead use the{" "}
<strong className="font-semibold">Describe Secret</strong> and{" "}
<strong className="font-semibold">Read Value</strong> actions.
</div>
}
>
<FontAwesomeIcon icon={faWarning} className="mt-1 text-yellow-500" size="sm" />
</Tooltip>
</div>
),
value: ProjectPermissionSecretActions.DescribeAndReadValue
},
{ label: "Describe Secret", value: ProjectPermissionSecretActions.DescribeSecret },
{ label: "Read Value", value: ProjectPermissionSecretActions.ReadValue },
{ label: "Modify", value: ProjectPermissionSecretActions.Edit },
{ label: "Remove", value: ProjectPermissionSecretActions.Delete },
{ label: "Create", value: ProjectPermissionSecretActions.Create }
] ]
}, },
[ProjectPermissionSub.SecretFolders]: { [ProjectPermissionSub.SecretFolders]: {
@@ -19,6 +19,7 @@ import {
import { getKeyValue } from "@app/helpers/parseEnvVar"; import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api"; import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
import { SecretType } from "@app/hooks/api/types"; import { SecretType } from "@app/hooks/api/types";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
const typeSchema = z const typeSchema = z
.object({ .object({
@@ -275,7 +276,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
isMulti isMulti
options={environments.filter((environment) => options={environments.filter((environment) =>
permission.can( permission.can(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: environment.slug, environment: environment.slug,
secretPath, secretPath,
@@ -25,10 +25,13 @@ import {
ModalTrigger, ModalTrigger,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { SecretType } from "@app/hooks/api/types"; import { SecretType } from "@app/hooks/api/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
type Props = { type Props = {
defaultValue?: string | null; defaultValue?: string | null;
@@ -39,6 +42,7 @@ type Props = {
isVisible?: boolean; isVisible?: boolean;
isImportedSecret: boolean; isImportedSecret: boolean;
environment: string; environment: string;
secretValueHidden: boolean;
secretPath: string; secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>; onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: ( onSecretUpdate: (
@@ -58,6 +62,7 @@ export const SecretEditRow = ({
isImportedSecret, isImportedSecret,
onSecretUpdate, onSecretUpdate,
secretName, secretName,
secretValueHidden,
onSecretCreate, onSecretCreate,
onSecretDelete, onSecretDelete,
environment, environment,
@@ -76,6 +81,9 @@ export const SecretEditRow = ({
value: defaultValue || null value: defaultValue || null
} }
}); });
const { permission } = useProjectPermission();
const [isDeleting, setIsDeleting] = useToggle(); const [isDeleting, setIsDeleting] = useToggle();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
@@ -117,6 +125,11 @@ export const SecretEditRow = ({
reset({ value }); reset({ value });
}; };
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue
);
const handleDeleteSecret = useCallback(async () => { const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on(); setIsDeleting.on();
setIsModalOpen(false); setIsModalOpen(false);
@@ -140,24 +153,29 @@ export const SecretEditRow = ({
/> />
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2"> <div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
<Controller {secretValueHidden ? (
disabled={isImportedSecret && !defaultValue} <Blur tooltipText="You do not have permission to read the value of this secret." />
control={control} ) : (
name="value" <Controller
render={({ field }) => ( disabled={isImportedSecret && !defaultValue}
<InfisicalSecretInput control={control}
{...field} name="value"
isReadOnly={isImportedSecret} render={({ field }) => (
value={field.value as string} <InfisicalSecretInput
key="secret-input" {...field}
isVisible={isVisible} isReadOnly={isImportedSecret}
secretPath={secretPath} value={field.value as string}
environment={environment} key="secret-input"
isImport={isImportedSecret} isVisible={isVisible}
/> secretPath={secretPath}
)} environment={environment}
/> isImport={isImportedSecret}
/>
)}
/>
)}
</div> </div>
<div <div
className={twMerge( className={twMerge(
"flex w-24 justify-center space-x-3 pl-2 transition-all", "flex w-24 justify-center space-x-3 pl-2 transition-all",
@@ -211,6 +229,7 @@ export const SecretEditRow = ({
<div className="opacity-0 group-hover:opacity-100"> <div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Copy Secret"> <Tooltip content="Copy Secret">
<IconButton <IconButton
isDisabled={secretValueHidden}
ariaLabel="copy-value" ariaLabel="copy-value"
onClick={handleCopySecretToClipboard} onClick={handleCopySecretToClipboard}
variant="plain" variant="plain"
@@ -220,48 +239,43 @@ export const SecretEditRow = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
<ProjectPermissionCan
I={ProjectPermissionActions.Read} <div className="opacity-0 group-hover:opacity-100">
a={ProjectPermissionSub.Secrets} <Modal>
> <ModalTrigger asChild>
{(isAllowed) => ( <div className="opacity-0 group-hover:opacity-100">
<div className="opacity-0 group-hover:opacity-100"> <Tooltip
<Modal> content={
<ModalTrigger asChild> hasSecretReference(defaultValue || "")
<div className="opacity-0 group-hover:opacity-100"> ? "Secret Reference Tree"
<Tooltip : "Secret does not contain references"
content={ }
hasSecretReference(defaultValue || "")
? "Secret Reference Tree"
: "Secret does not contain references"
}
>
<IconButton
variant="plain"
ariaLabel="reference-tree"
className="h-full"
isDisabled={!hasSecretReference(defaultValue || "") || !isAllowed}
>
<FontAwesomeIcon icon={faProjectDiagram} />
</IconButton>
</Tooltip>
</div>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
> >
<SecretReferenceTree <IconButton
secretPath={secretPath} variant="plain"
environment={environment} ariaLabel="reference-tree"
secretKey={secretName} className="h-full"
/> isDisabled={!hasSecretReference(defaultValue || "") || !canReadSecretValue}
</ModalContent> >
</Modal> <FontAwesomeIcon icon={faProjectDiagram} />
</div> </IconButton>
)} </Tooltip>
</ProjectPermissionCan> </div>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={secretName}
/>
</ModalContent>
</Modal>
</div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { a={subject(ProjectPermissionSub.Secrets, {
@@ -3,6 +3,7 @@ import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Td, Tooltip, Tr } from "@app/components/v2"; import { Td, Tooltip, Tr } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
type Props = { type Props = {
environments: { name: string; slug: string }[]; environments: { name: string; slug: string }[];
@@ -25,7 +26,7 @@ export const SecretNoAccessOverviewTableRow = ({ environments = [], count }: Pro
<div className="text-bunker-300"> <div className="text-bunker-300">
<FontAwesomeIcon className="block" icon={faLock} /> <FontAwesomeIcon className="block" icon={faLock} />
</div> </div>
<div className="blur-sm">NO ACCESS</div> <Blur />
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
@@ -221,10 +221,13 @@ export const SecretOverviewTableRow = ({
secretPath={secretPath} secretPath={secretPath}
isVisible={isSecretVisible} isVisible={isSecretVisible}
secretName={secretKey} secretName={secretKey}
secretValueHidden={secret?.secretValueHidden || false}
defaultValue={ defaultValue={
secret?.valueOverride || secret?.secretValueHidden
secret?.value || ? ""
importedSecret?.secret?.value : secret?.valueOverride ||
secret?.value ||
importedSecret?.secret?.value
} }
secretId={secret?.id} secretId={secret?.id}
isOverride={Boolean(secret?.valueOverride)} isOverride={Boolean(secret?.valueOverride)}
@@ -10,15 +10,12 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2";
import { import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
ProjectPermissionActions, import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { useUpdateSecretV3 } from "@app/hooks/api"; import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
enum SecretActionType { enum SecretActionType {
Created = "created", Created = "created",
@@ -55,8 +52,11 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
secretTags: (secretDetails?.tags || []).map((i) => i.slug) secretTags: (secretDetails?.tags || []).map((i) => i.slug)
}); });
const isSecretInEnvReadOnly = const isSecretInEnvReadOnly =
permission.can(ProjectPermissionActions.Read, secretPermissionSubject) && hasSecretReadValueOrDescribePermission(
permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject); permission,
ProjectPermissionSecretActions.DescribeSecret,
secretPermissionSubject
) && permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject);
if (isSecretInEnvReadOnly) { if (isSecretInEnvReadOnly) {
return true; return true;
} }
@@ -110,21 +110,31 @@ export const QuickSearchSecretItem = ({
</Badge> </Badge>
)} )}
{isSingleEnv ? ( {isSingleEnv ? (
<IconButton <Tooltip
size="md" isDisabled={!groupSecret?.secretValueHidden}
variant="plain" content={
colorSchema="secondary" groupSecret?.secretValueHidden
ariaLabel="Copy secret value" ? "You do not have permission to view this secret value"
onClick={(e) => { : ""
e.stopPropagation(); }
const el = envSlugMap.get(groupSecret.env)?.name;
if (el) {
handleCopy(groupSecret.value!, el);
}
}}
> >
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} /> <IconButton
</IconButton> size="md"
isDisabled={groupSecret?.secretValueHidden}
variant="plain"
colorSchema="secondary"
ariaLabel="Copy secret value"
onClick={(e) => {
e.stopPropagation();
const el = envSlugMap.get(groupSecret.env)?.name;
if (el) {
handleCopy(groupSecret.value!, el);
}
}}
>
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
) : ( ) : (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -158,14 +168,24 @@ export const QuickSearchSecretItem = ({
)} )}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<IconButton <Tooltip
size="md" isDisabled={!groupSecret?.secretValueHidden}
variant="plain" content={
colorSchema="secondary" groupSecret?.secretValueHidden
ariaLabel="View secret value" ? "You do not have permission to view this secret value"
: ""
}
> >
<FontAwesomeIcon icon={faEye} /> <IconButton
</IconButton> size="md"
isDisabled={groupSecret?.secretValueHidden}
variant="plain"
colorSchema="secondary"
ariaLabel="View secret value"
>
<FontAwesomeIcon icon={faEye} />
</IconButton>
</Tooltip>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel>Hover to Reveal...</DropdownMenuLabel> <DropdownMenuLabel>Hover to Reveal...</DropdownMenuLabel>
@@ -11,6 +11,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { import {
@@ -58,7 +59,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
// user should have the ability to delete secrets/folders in at least one of the envs // user should have the ability to delete secrets/folders in at least one of the envs
const shouldShowDelete = userAvailableEnvs.some((env) => const shouldShowDelete = userAvailableEnvs.some((env) =>
permission.can( permission.can(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: env.slug, environment: env.slug,
secretPath, secretPath,
@@ -110,7 +111,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => { (accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
const entry = secretRecord[env.slug]; const entry = secretRecord[env.slug];
const canDeleteSecret = permission.can( const canDeleteSecret = permission.can(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: env.slug, environment: env.slug,
secretPath, secretPath,
@@ -25,7 +25,8 @@ import {
Spinner, Spinner,
Switch Switch
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce } from "@app/hooks"; import { useDebounce } from "@app/hooks";
import { useMoveSecrets } from "@app/hooks/api"; import { useMoveSecrets } from "@app/hooks/api";
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard"; import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
@@ -95,7 +96,7 @@ const Content = ({
env.slug, env.slug,
{ {
missingPermissions: permission.cannot( missingPermissions: permission.cannot(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: env.slug, environment: env.slug,
secretPath: sourceSecretPath, secretPath: sourceSecretPath,
@@ -25,6 +25,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { import {
useGetImportedSecretsSingleEnv, useGetImportedSecretsSingleEnv,
@@ -37,6 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount"; import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection"; import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection";
@@ -102,14 +104,27 @@ const Page = () => {
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || ""; const projectSlug = currentWorkspace?.slug || "";
const secretPath = (routerQueryParams.secretPath as string) || "/"; const secretPath = (routerQueryParams.secretPath as string) || "/";
const canReadSecret = permission.can(
ProjectPermissionActions.Read, const canReadSecret = hasSecretReadValueOrDescribePermission(
subject(ProjectPermissionSub.Secrets, { permission,
ProjectPermissionSecretActions.DescribeSecret,
{
environment, environment,
secretPath, secretPath,
secretName: "*", secretName: "*",
secretTags: ["*"] secretTags: ["*"]
}) }
);
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
}
); );
const canReadSecretImports = permission.can( const canReadSecretImports = permission.can(
@@ -176,6 +191,7 @@ const Page = () => {
orderDirection, orderDirection,
includeImports: canReadSecretImports && filter.include.import, includeImports: canReadSecretImports && filter.include.import,
includeFolders: filter.include.folder, includeFolders: filter.include.folder,
viewSecretValue: canReadSecretValue,
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
includeSecrets: canReadSecret && filter.include.secret, includeSecrets: canReadSecret && filter.include.secret,
tags: filter.tags tags: filter.tags
@@ -21,6 +21,7 @@ import {
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AxiosError } from "axios";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -54,7 +55,7 @@ import {
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
import { SecretType, WsTag } from "@app/hooks/api/types"; import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput";
import { import {
@@ -152,51 +153,71 @@ export const ActionBar = ({
}; };
const handleSecretDownload = async () => { const handleSecretDownload = async () => {
const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ try {
workspaceId, const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
expandSecretReferences: true, workspaceId,
includeImports: true, expandSecretReferences: true,
environment, includeImports: true,
secretPath environment,
}); secretPath
const secretsPicked = new Set<string>(); });
const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; const secretsPicked = new Set<string>();
localSecrets.forEach((el) => { const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
secretsPicked.add(el.secretKey); localSecrets.forEach((el) => {
secretsToDownload.push({ secretsPicked.add(el.secretKey);
key: el.secretKey, secretsToDownload.push({
value: el.secretValue, key: el.secretKey,
comment: el.secretComment value: el.secretValue,
comment: el.secretComment
});
}); });
});
for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
const secret = localImportedSecrets[i].secrets[j]; const secret = localImportedSecrets[i].secrets[j];
if (!secretsPicked.has(secret.secretKey)) { if (!secretsPicked.has(secret.secretKey)) {
secretsToDownload.push({ secretsToDownload.push({
key: secret.secretKey, key: secret.secretKey,
value: secret.secretValue, value: secret.secretValue,
comment: secret.secretComment comment: secret.secretComment
}); });
}
secretsPicked.add(secret.secretKey);
} }
secretsPicked.add(secret.secretKey);
} }
const file = secretsToDownload
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
.reduce(
(prev, { key, comment, value }, index) =>
prev +
(comment
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
: `${key}=${value}\n`),
""
);
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, `${environment}.env`);
} catch (err) {
if (err instanceof AxiosError) {
const error = err?.response?.data as TApiErrors;
if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) {
createNotification({
title: "You don't have permission to download secrets",
text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.",
type: "error"
});
return;
}
}
createNotification({
title: "Failed to download secrets",
text: "Please try again later.",
type: "error"
});
} }
const file = secretsToDownload
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
.reduce(
(prev, { key, comment, value }, index) =>
prev +
(comment
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
: `${key}=${value}\n`),
""
);
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, `${environment}.env`);
}; };
const handleSecretBulkDelete = async () => { const handleSecretBulkDelete = async () => {
@@ -20,8 +20,9 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce } from "@app/hooks"; import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api"; import { useGetAccessibleSecrets } from "@app/hooks/api/dashboard";
const formSchema = z.object({ const formSchema = z.object({
environment: z.object({ name: z.string(), slug: z.string() }), environment: z.object({ name: z.string(), slug: z.string() }),
@@ -32,7 +33,7 @@ const formSchema = z.object({
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
), ),
secrets: z secrets: z
.object({ key: z.string(), value: z.string().optional() }) .object({ secretKey: z.string(), secretValue: z.string().optional() })
.array() .array()
.min(1, "Select one or more secrets to copy") .min(1, "Select one or more secrets to copy")
}); });
@@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({
const selectedEnvSlug = watch("environment"); const selectedEnvSlug = watch("environment");
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath); const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({ const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } =
workspaceId, useGetAccessibleSecrets({
environment: selectedEnvSlug.slug, projectId: workspaceId,
secretPath: debouncedEnvCopySecretPath, secretPath: debouncedEnvCopySecretPath,
options: { environment: selectedEnvSlug.slug,
enabled: filterByAction: shouldIncludeValues
Boolean(workspaceId) && ? ProjectPermissionSecretActions.ReadValue
Boolean(selectedEnvSlug) && : ProjectPermissionSecretActions.DescribeSecret,
Boolean(debouncedEnvCopySecretPath) && options: {
isOpen enabled:
} Boolean(workspaceId) &&
}); Boolean(selectedEnvSlug) &&
Boolean(debouncedEnvCopySecretPath) &&
isOpen
}
});
useEffect(() => { useEffect(() => {
setValue("secrets", []); setValue("secrets", []);
}, [debouncedEnvCopySecretPath, selectedEnvSlug]); }, [debouncedEnvCopySecretPath, selectedEnvSlug]);
const handleSecSelectAll = () => { const handleSecSelectAll = () => {
if (secrets) { if (accessibleSecrets) {
setValue("secrets", secrets, { shouldDirty: true }); setValue("secrets", accessibleSecrets, { shouldDirty: true });
} }
}; };
const handleFormSubmit = async (data: TFormSchema) => { const handleFormSubmit = async (data: TFormSchema) => {
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {}; const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
data.secrets.forEach(({ key, value }) => { data.secrets.forEach(({ secretKey, secretValue }) => {
secretsToBePulled[key] = { secretsToBePulled[secretKey] = {
value: (shouldIncludeValues && value) || "", value: (shouldIncludeValues && secretValue) || "",
comments: [""] comments: [""]
}; };
}); });
@@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({
<FilterableSelect <FilterableSelect
placeholder={ placeholder={
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
isSecretsLoading isAccessibleSecretsLoading
? "Loading secrets..." ? "Loading secrets..."
: secrets?.length : accessibleSecrets?.length
? "Select secrets..." ? "Select secrets..."
: "No secrets found..." : "No secrets found..."
} }
isLoading={isSecretsLoading} isLoading={isAccessibleSecretsLoading}
options={secrets} options={accessibleSecrets}
value={value} value={value}
onChange={onChange} onChange={onChange}
isMulti isMulti
getOptionValue={(option) => option.key} getOptionValue={(option) => option.secretKey}
getOptionLabel={(option) => option.key} getOptionLabel={(option) => option.secretKey}
/> />
</FormControl> </FormControl>
)} )}
@@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({
<Switch <Switch
id="populate-include-value" id="populate-include-value"
isChecked={shouldIncludeValues} isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)} onCheckedChange={(isChecked) => {
setValue("secrets", []);
setShouldIncludeValues(isChecked as boolean);
}}
> >
Include secret values Include secret values
</Switch> </Switch>
@@ -13,12 +13,14 @@ import {
faShare, faShare,
faTag, faTag,
faTrash, faTrash,
faTriangleExclamation,
faUser faUser
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Link, useNavigate } from "@tanstack/react-router"; import { Link, useNavigate } from "@tanstack/react-router";
import { format } from "date-fns"; import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -48,15 +50,18 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api"; import { useGetSecretVersion } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { CreateReminderForm } from "./CreateReminderForm"; import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
import { camelCaseToSpaces } from "@app/lib/fn/string";
type Props = { type Props = {
isOpen?: boolean; isOpen?: boolean;
@@ -128,7 +133,7 @@ export const SecretDetailSidebar = ({
const navigate = useNavigate(); const navigate = useNavigate();
const cannotEditSecret = permission.cannot( const cannotEditSecret = permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -136,16 +141,31 @@ export const SecretDetailSidebar = ({
secretTags: selectTagSlugs secretTags: selectTagSlugs
}) })
); );
const cannotReadSecretValue = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
}
);
const isReadOnly = const isReadOnly =
permission.can( hasSecretReadValueOrDescribePermission(
ProjectPermissionActions.Read, permission,
subject(ProjectPermissionSub.Secrets, { ProjectPermissionSecretActions.DescribeSecret,
{
environment, environment,
secretPath, secretPath,
secretName: secretKey, secretName: secretKey,
secretTags: selectTagSlugs secretTags: selectTagSlugs
}) }
) && cannotEditSecret; ) &&
cannotEditSecret &&
cannotReadSecretValue;
const overrideAction = watch("overrideAction"); const overrideAction = watch("overrideAction");
const isOverridden = const isOverridden =
@@ -325,38 +345,63 @@ export const SecretDetailSidebar = ({
key="secret-value" key="secret-value"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormControl label="Value"> <div className="flex items-center gap-2">
<InfisicalSecretInput <FormControl
isReadOnly={isReadOnly} className="flex-1"
environment={environment} helperText={
secretPath={secretPath} cannotReadSecretValue ? (
key="secret-value" <div className="flex space-x-2">
isDisabled={isOverridden || !isAllowed} <FontAwesomeIcon
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5" icon={faTriangleExclamation}
{...field} className="mt-0.5 text-yellow-400"
autoFocus={false} />
/> <span>
</FormControl> The value of this secret is hidden because you do not have the
read secret value permission.
</span>
</div>
) : undefined
}
label="Value"
>
<div className="flex items-center gap-2">
<InfisicalSecretInput
isReadOnly={isReadOnly || !isAllowed}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden}
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
<Tooltip
content="You don't have permission to view the secret value."
isDisabled={!secret?.secretValueHidden}
>
<Button
isDisabled={secret?.secretValueHidden}
className="px-2 py-[0.43rem] font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share
</Button>
</Tooltip>
</div>
</FormControl>
</div>
)} )}
/> />
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
<div className="ml-1 mt-1.5 flex items-center">
<Button
className="w-full px-2 py-[0.43rem] font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share
</Button>
</div>
</div> </div>
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0"> <div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div className="mb-4 px-4"> <div className="mb-4 px-4">
@@ -683,77 +728,62 @@ export const SecretDetailSidebar = ({
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300"> <div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<div className="mb-2 pl-1">Version History</div> <div className="mb-2 pl-1">Version History</div>
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]"> <div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => ( {secretVersion?.map(
<div className="flex flex-row"> ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
<div key={id} className="flex w-full flex-col space-y-1"> <div className="flex flex-row" key={id}>
<div className="flex items-center"> <div className="flex w-full flex-col space-y-1">
<div className="w-10"> <div className="flex items-center">
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300"> <div className="w-10">
v{version} <div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
v{version}
</div>
</div> </div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div> </div>
<div>{format(new Date(createdAt), "Pp")}</div> <div className="flex w-full cursor-default">
</div> <div className="relative w-10">
<div className="flex w-full cursor-default"> <div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
<div className="relative w-10"> </div>
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" /> <div className="flex w-full cursor-default flex-col">
</div> {actor && (
<div className="flex w-full cursor-default flex-col"> <div className="flex flex-row">
{actor && ( <div className="flex w-fit flex-row text-sm">
<div className="flex flex-row"> Modified by:
<div className="flex w-fit flex-row text-sm"> <Tooltip
Modified by: content={getModifiedByName(actor.actorType, actor.name)}
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
onClick={() =>
onModifyHistoryClick(
actor.actorId,
actor.actorType,
actor.membershipId
)
}
className="cursor-pointer"
> >
<FontAwesomeIcon {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
icon={getModifiedByIcon(actor.actorType)} <div
className="ml-2" onClick={() =>
/> onModifyHistoryClick(
</div> actor.actorId,
</Tooltip> actor.actorType,
actor.membershipId
)
}
className="cursor-pointer"
>
<FontAwesomeIcon
icon={getModifiedByIcon(actor.actorType)}
className="ml-2"
/>
</div>
</Tooltip>
</div>
</div> </div>
</div> )}
)} <div className="flex flex-row">
<div className="flex flex-row"> <div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70"> Value:
Value: </div>
</div> <div className="group break-all pl-1 font-mono">
<div className="group break-all pl-1 font-mono"> <div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline"> <button
<button type="button"
type="button" className="select-none text-left"
className="select-none text-left" onClick={(e) => {
onClick={(e) => { if (secretValueHidden) return;
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || ""); navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget; const target = e.currentTarget;
target.style.borderBottom = "1px dashed"; target.style.borderBottom = "1px dashed";
@@ -771,75 +801,104 @@ export const SecretDetailSidebar = ({
popup.remove(); popup.remove();
target.style.borderBottom = "none"; target.style.borderBottom = "none";
}, 3000); }, 3000);
} }}
}} onKeyDown={(e) => {
> if (secretValueHidden) return;
{secretValue}
</button> if (e.key === "Enter" || e.key === " ") {
<button navigator.clipboard.writeText(secretValue || "");
type="button" const target = e.currentTarget;
className="ml-1 cursor-pointer" target.style.borderBottom = "1px dashed";
onClick={(e) => { target.style.paddingBottom = "-1px";
e.stopPropagation();
e.currentTarget // Create and insert popup
.closest(".group") const popup = document.createElement("div");
?.classList.remove("show-value"); popup.className =
}} "w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
onKeyDown={(e) => { popup.textContent = "Copied!";
if (e.key === "Enter" || e.key === " ") { target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
<span
className={twMerge(
secretValueHidden && "text-xs text-bunker-300 opacity-40"
)}
>
{secretValueHidden ? "Hidden" : secretValue}
</span>
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.currentTarget e.currentTarget
.closest(".group") .closest(".group")
?.classList.remove("show-value"); ?.classList.remove("show-value");
} }}
}} onKeyDown={(e) => {
> if (e.key === "Enter" || e.key === " ") {
<FontAwesomeIcon icon={faEyeSlash} /> e.stopPropagation();
</button> e.currentTarget
</div> .closest(".group")
<span className="group-[.show-value]:hidden"> ?.classList.remove("show-value");
{secretValue?.replace(/./g, "*")} }
<button }}
type="button" >
className="ml-1 cursor-pointer" <FontAwesomeIcon icon={faEyeSlash} />
onClick={(e) => { </button>
e.currentTarget </div>
.closest(".group") <span className="group-[.show-value]:hidden">
?.classList.add("show-value"); {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
}} <button
onKeyDown={(e) => { type="button"
if (e.key === "Enter" || e.key === " ") { className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget e.currentTarget
.closest(".group") .closest(".group")
?.classList.add("show-value"); ?.classList.add("show-value");
} }}
}} onKeyDown={(e) => {
> if (e.key === "Enter" || e.key === " ") {
<FontAwesomeIcon icon={faEye} /> e.currentTarget
</button> .closest(".group")
</span> ?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
>
<Tooltip content="Restore Secret Value">
<IconButton
ariaLabel="Restore"
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() => setValue("value", secretValue)}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
</div>
</div> </div>
<div )
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`} )}
>
<Tooltip content="Restore Secret Value">
<IconButton
ariaLabel="Restore"
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() => setValue("value", secretValue)}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
</div>
</div>
))}
</div> </div>
</div> </div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300"> <div className="dark mb-4 flex-grow text-sm text-bunker-300">
@@ -878,12 +937,9 @@ export const SecretDetailSidebar = ({
<div className="rounded-md bg-bunker-500"> <div className="rounded-md bg-bunker-500">
<Tooltip <Tooltip
content={user.allowedActions content={user.allowedActions
.map( .map((action) => camelCaseToSpaces(action))
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")} .join(", ")}
className="z-[100]" className="z-[100] capitalize"
> >
<Link <Link
to={ to={
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable simple-import-sort/imports */ /* eslint-disable simple-import-sort/imports */
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
@@ -44,6 +45,9 @@ import {
SecretReferenceTree SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails"; } from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { import {
FontAwesomeSpriteName, FontAwesomeSpriteName,
formSchema, formSchema,
@@ -99,8 +103,14 @@ export const SecretItem = memo(
trigger, trigger,
formState: { isDirty, isSubmitting, errors } formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: secret, defaultValues: {
values: secret, ...secret,
value: secret.secretValueHidden ? "" : secret.value
},
values: {
...secret,
value: secret.secretValueHidden ? "" : secret.value
},
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
@@ -122,17 +132,18 @@ export const SecretItem = memo(
}); });
const isReadOnly = const isReadOnly =
permission.can( hasSecretReadValueOrDescribePermission(
ProjectPermissionActions.Read, permission,
subject(ProjectPermissionSub.Secrets, { ProjectPermissionSecretActions.DescribeSecret,
{
environment, environment,
secretPath, secretPath,
secretName, secretName,
secretTags: selectedTagSlugs secretTags: selectedTagSlugs
}) }
) && ) &&
permission.cannot( permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -141,6 +152,8 @@ export const SecretItem = memo(
}) })
); );
const { secretValueHidden } = secret;
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
@@ -272,6 +285,8 @@ export const SecretItem = memo(
/> />
)} )}
/> />
) : secretValueHidden ? (
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : ( ) : (
<Controller <Controller
name="value" name="value"
@@ -285,6 +300,7 @@ export const SecretItem = memo(
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}
{...field} {...field}
defaultValue={secretValueHidden ? "" : undefined}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2" containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/> />
)} )}
@@ -293,6 +309,7 @@ export const SecretItem = memo(
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all"> <div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
<Tooltip content="Copy secret"> <Tooltip content="Copy secret">
<IconButton <IconButton
isDisabled={secret.secretValueHidden}
ariaLabel="copy-value" ariaLabel="copy-value"
variant="plain" variant="plain"
size="sm" size="sm"
@@ -500,6 +517,7 @@ export const SecretItem = memo(
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<IconButton <IconButton
isDisabled={secret.secretValueHidden}
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6" className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
variant="plain" variant="plain"
size="md" size="md"
@@ -1,4 +1,5 @@
import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2"; import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { FontAwesomeSpriteName } from "./SecretListView.utils"; import { FontAwesomeSpriteName } from "./SecretListView.utils";
@@ -34,13 +35,7 @@ export const SecretNoAccessListView = ({ count }: Props) => {
className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent" className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/> />
</div> </div>
<div <Blur />
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
</div> </div>
</Tooltip> </Tooltip>
))} ))}
@@ -20,6 +20,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
@@ -120,11 +121,25 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">Value</Td> <Td className="border-r border-mineshaft-600">Value</Td>
{isModified && ( {isModified && (
<Td className="border-r border-mineshaft-600"> <Td className="border-r border-mineshaft-600">
<SecretInput value={preSecret?.value} /> {preSecret?.secretValueHidden ? (
<Blur
className="w-min"
tooltipText="You do not have permission to read the value of this secret."
/>
) : (
<SecretInput value={preSecret?.value} />
)}
</Td> </Td>
)} )}
<Td> <Td>
<SecretInput value={postSecret?.value} /> {postSecret?.secretValueHidden ? (
<Blur
className="w-min"
tooltipText="You do not have permission to read the value of this secret."
/>
) : (
<SecretInput value={postSecret?.value} />
)}
</Td> </Td>
</Tr> </Tr>
{Boolean(preSecret?.idOverride || postSecret?.idOverride) && ( {Boolean(preSecret?.idOverride || postSecret?.idOverride) && (