feat(api): view secret value, WIP

This commit is contained in:
Daniel Hougaard
2025-03-05 22:47:39 +04:00
parent 8c18753e3f
commit 4a66395ce6
40 changed files with 1042 additions and 303 deletions
@@ -0,0 +1,36 @@
import { selectAllTableCols } from "@app/lib/knex";
import { Knex } from "knex";
import { TableName } from "../schemas";
// [["read,create","secrets",{"environment":{"$eq":"dev"}}],
// ["read,edit,create","secrets",{"environment":{"$eq":"staging"}}],
// ["read,create","secrets",{"environment":{"$eq":"prod"}},1],
// ["edit,delete,create","secret-folders",{}],
// ["read,edit,delete,create","secret-imports",{}],
// ["read,edit,delete,create","member"],
// ["read,edit,delete,create","role"],
// ["read,edit,delete,create","integrations"],
// ["read,edit","settings"],
// ["edit,delete","workspace"],
// ["read,edit,delete,create","tags"],
// ["read,create,edit,delete,sync-secrets,import-secrets,remove-secrets","secret-syncs"]]
// enum ProjectPermissionSub {
// Secrets = "secrets"
// }
export async function up(knex: Knex): Promise<void> {
const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles));
for (const projectRole of projectRoles) {
const { _, permissions } = projectRole;
const parsedPermissions = JSON.parse(permissions as string) as Record<string, string>[]; // contains array of permissions.
for (const parsedPermission of parsedPermissions) {
console.log(parsedPermission);
}
}
}
export async function down(knex: Knex): Promise<void> {}
+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()
@@ -17,6 +17,14 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionSecretActions {
DescribeSecret = "read",
ReadValue = "readValue",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionCmekActions { export enum ProjectPermissionCmekActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -115,7 +123,7 @@ export type IdentityManagementSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionSecretActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
] ]
| [ | [
@@ -433,7 +441,7 @@ 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."),
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: SecretConditionV1Schema.describe( conditions: SecretConditionV1Schema.describe(
@@ -460,7 +468,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 +525,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 +557,21 @@ const buildAdminPermissionRules = () => {
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionActions.Delete ProjectPermissionActions.Delete
], ],
el as ProjectPermissionSub el
); );
}); });
can(
[
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can( can(
[ [
ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionDynamicSecretActions.ReadRootCredential,
@@ -613,10 +631,11 @@ const buildMemberPermissionRules = () => {
can( can(
[ [
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Edit,
ProjectPermissionActions.Delete ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
], ],
ProjectPermissionSub.Secrets ProjectPermissionSub.Secrets
); );
@@ -788,7 +807,8 @@ 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); // ? Q(Daniel): Should the viewer role be allowed to read values? Currently not allowed in permission below.
can(ProjectPermissionSecretActions.DescribeSecret, 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);
@@ -831,6 +851,8 @@ export const buildServiceTokenProjectPermission = (
) => { ) => {
const canWrite = permission.includes("write"); const canWrite = permission.includes("write");
const canRead = permission.includes("read"); const canRead = permission.includes("read");
const canReadValue = permission.includes("readValue");
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
scopes.forEach(({ secretPath, environment }) => { scopes.forEach(({ secretPath, environment }) => {
[ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach(
@@ -860,6 +882,14 @@ export const buildServiceTokenProjectPermission = (
environment environment
}); });
} }
if (subject === ProjectPermissionSub.Secrets && canReadValue) {
// @ts-expect-error type
can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, {
secretPath: { $glob: secretPath },
environment
});
}
} }
); );
}); });
@@ -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";
@@ -88,7 +88,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">;
@@ -914,7 +919,7 @@ export const secretApprovalRequestServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -1001,6 +1006,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 +1369,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,
@@ -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 })
); );
@@ -22,7 +22,11 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
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 { import {
TGetSnapshotDataDTO, TGetSnapshotDataDTO,
TProjectSnapshotCountDTO, TProjectSnapshotCountDTO,
@@ -97,7 +101,7 @@ export const secretSnapshotServiceFactory = ({
// 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -134,7 +138,7 @@ export const secretSnapshotServiceFactory = ({
// 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -224,7 +228,7 @@ export const secretSnapshotServiceFactory = ({
// 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug, environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath secretPath: fullFolderPath
+3
View File
@@ -666,6 +666,7 @@ export const SECRETS = {
secretPath: "The path of the secret to attach tags to.", secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)", type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located", environment: "The slug of the environment where the secret is located",
viewSecretValue: "Whether or not to retrieve the secret value.",
projectSlug: "The slug of the project where the secret is located.", projectSlug: "The slug of the project where the secret is located.",
tagSlugs: "An array of existing tag slugs to attach to the secret." tagSlugs: "An array of existing tag slugs to attach to the secret."
}, },
@@ -689,6 +690,7 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.", "The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.", secretPath: "The secret path to list secrets from.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not.", includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets.", tagSlugs: "The comma separated tag slugs to filter secrets.",
metadataFilter: metadataFilter:
@@ -717,6 +719,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: {
+10
View File
@@ -1,3 +1,6 @@
// eslint-disable-next-line max-classes-per-file
import { AnyAbility, ForbiddenError } from "@casl/ability";
/* 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;
@@ -59,6 +62,13 @@ export class ForbiddenRequestError extends Error {
} }
} }
export class ForbiddenReadSecretError extends ForbiddenRequestError {
constructor({ error, message }: { message?: string; error?: unknown } = {}) {
super({ message, error });
this.name = "ForbiddenReadSecretError";
}
}
export class BadRequestError extends Error { export class BadRequestError extends Error {
name: string; name: string;
@@ -116,6 +116,7 @@ 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: SecretTagsSchema.pick({
@@ -294,6 +295,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 +395,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,6 +413,7 @@ 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: SecretTagsSchema.pick({
@@ -600,10 +604,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
tagSlugs: tags tagSlugs: tags
}); });
console.log("totalSecretCount", totalSecretCount);
console.log("adjustedOffset", adjustedOffset);
console.log("remainingLimit", remainingLimit);
console.log("will resolve to true", remainingLimit > 0 && totalSecretCount > adjustedOffset);
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
console.log("before running");
const secretsRaw = await server.services.secret.getSecretsRaw({ const secretsRaw = await server.services.secret.getSecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
viewSecretValue: req.query.viewSecretValue,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environment, environment,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -617,6 +629,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
tagSlugs: tags tagSlugs: tags
}); });
console.log("secretsRaw", secretsRaw);
secrets = secretsRaw.secrets; secrets = secretsRaw.secrets;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -649,6 +662,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
} catch (error) { } catch (error) {
console.log(error);
if (!(error instanceof ForbiddenError)) { if (!(error instanceof ForbiddenError)) {
throw error; throw error;
} }
@@ -862,12 +876,14 @@ 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(true)
}), }),
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: SecretTagsSchema.pick({
@@ -886,7 +902,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 +911,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,
@@ -77,6 +77,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED secure: cfg.HTTPS_ENABLED
}); });
console.log("access token", tokens.access);
return { token: tokens.access, isMfaEnabled: false }; return { token: tokens.access, isMfaEnabled: false };
} }
}); });
+66 -34
View File
@@ -31,6 +31,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({
@@ -45,6 +53,7 @@ const SecretNameSchema = BaseSecretNameSchema.refine(
).refine((el) => !el.includes(":"), "Secret name cannot contain colon."); ).refine((el) => !el.includes(":"), "Secret name cannot contain colon.");
export const registerSecretRouter = async (server: FastifyZodProvider) => { export const registerSecretRouter = async (server: FastifyZodProvider) => {
// ! Note(Daniel): (Tags) Does not support secrets v2. Request will fail if user doesn't have read value permission.
server.route({ server.route({
method: "POST", method: "POST",
url: "/tags/:secretName", url: "/tags/:secretName",
@@ -64,6 +73,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug), projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment), environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment),
viewSecretValue: convertStringBoolean(true).describe(SECRETS.ATTACH_TAGS.viewSecretValue),
secretPath: z secretPath: z
.string() .string()
.trim() .trim()
@@ -108,6 +118,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ! Note(Daniel): (Tags) Does not support secrets v2. Request will fail if user doesn't have read value permission.
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/tags/:secretName", url: "/tags/:secretName",
@@ -169,6 +180,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Note(Daniel): (Secrets) Done for v2 secrets AND normal secrets GET /raw
server.route({ server.route({
method: "GET", method: "GET",
url: "/raw", url: "/raw",
@@ -247,21 +259,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,6 +275,7 @@ 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: SecretTagsSchema.pick({
id: true, id: true,
@@ -293,6 +295,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 +345,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences, expandSecretReferences: req.query.expandSecretReferences,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: workspaceId, projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
path: secretPath, path: secretPath,
metadataFilter: req.query.metadataFilter, metadataFilter: req.query.metadataFilter,
includeImports: req.query.include_imports, includeImports: req.query.include_imports,
@@ -376,10 +380,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
} }
return { secrets, imports }; return { secrets, imports };
} }
}); });
// !!!!!!!!!!!!!!!!!!!!! Note(Daniel): (Secrets) Done for v2 secrets -- GET /raw/:secretName
server.route({ server.route({
method: "GET", method: "GET",
url: "/raw/:secretName", url: "/raw/:secretName",
@@ -403,20 +409,14 @@ 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({
secretValueHidden: z.boolean(),
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
@@ -456,6 +456,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,
@@ -498,6 +499,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ? Note(Daniel): No modify, if user has Create permissions it will return the value they created for this secret --- POST /raw/:secretName
server.route({ server.route({
method: "POST", method: "POST",
url: "/raw/:secretName", url: "/raw/:secretName",
@@ -611,6 +613,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ? Note(Daniel): Will NOT throw an error. If the user has access to read value, it will return value.
// ? Note(Daniel): If user does NOT have access to read value, it will return <hidden-by-infisical> for the value, but succeed with update.
// !!!!! Done for both secret types. For legacy secrets, it will return <hidden-by-infisical> if no read value permission is present.
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/raw/:secretName", url: "/raw/:secretName",
@@ -728,6 +733,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ? Note(Daniel): Will NOT throw an error. If the user has access to read value, it will return the deleted value
// ? Note(Daniel): If user does NOT have access to read value, it will return <hidden> for the value, but succeed with delete.
// !!!!! Done for both secret types. For legacy secrets, it will return <hidden> if no read value permission is present. --- /raw/:secretName
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/raw/:secretName", url: "/raw/:secretName",
@@ -758,7 +766,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: secretRawSchema secret: secretRawSchema.extend({
secretValueHidden: z.boolean()
})
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -780,6 +790,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({
@@ -814,6 +825,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. Will throw without the `readValue` permission, just like before.
server.route({ server.route({
method: "GET", method: "GET",
url: "/", url: "/",
@@ -928,6 +940,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. Will throw without the `readValue` permission, just like before.
server.route({ server.route({
method: "GET", method: "GET",
url: "/:secretName", url: "/:secretName",
@@ -942,12 +955,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceId: z.string().trim(), workspaceId: 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),
viewSecretValue: convertStringBoolean(true),
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({
@@ -1009,6 +1020,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. Will work exactly like before. It will not attempt to hide the secret value, because the user creating this secret will already know the value upon creation.
server.route({ server.route({
url: "/:secretName", url: "/:secretName",
method: "POST", method: "POST",
@@ -1180,6 +1192,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. Will work like before, EXCEPT, if the user doesn't have the `readValue` permission, the secret value will be marked as "<hidden-by-infisical>"
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:secretName", url: "/:secretName",
@@ -1218,6 +1231,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()
@@ -1367,6 +1381,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. Will work like before, EXCEPT, if the user doesn't have the `readValue` permission, the secret value will be marked as "<hidden-by-infisical>"
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:secretName", url: "/:secretName",
@@ -1491,6 +1506,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ? No need for update, as this endpoint does not expose any values.
server.route({ server.route({
method: "POST", method: "POST",
url: "/move", url: "/move",
@@ -1546,6 +1562,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. This will works exactly like before. It will not attempt to hide the secret value, because the user creating this secret will already know the value(s) upon creation.
server.route({ server.route({
method: "POST", method: "POST",
url: "/batch", url: "/batch",
@@ -1672,6 +1689,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. Works as before, EXCEPT if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "<hidden-by-infisical>"
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/batch", url: "/batch",
@@ -1705,7 +1723,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")
]) ])
@@ -1798,6 +1816,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// !!!! Done. Works as before, EXCEPT if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "<hidden-by-infisical>"
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/batch", url: "/batch",
@@ -1820,7 +1839,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")
]) ])
@@ -1912,6 +1935,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ! (Daniel): Done. Will not attempt to hide secret value because this is a create operation.
server.route({ server.route({
method: "POST", method: "POST",
url: "/batch/raw", url: "/batch/raw",
@@ -2018,6 +2042,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ! Done. Works as before, except if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "<hidden-by-infisical>"
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/batch/raw", url: "/batch/raw",
@@ -2082,7 +2107,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")
]) ])
@@ -2170,6 +2195,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ! (Daniel): Done. Works as before, except if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "<hidden-by-infisical>"
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/batch/raw", url: "/batch/raw",
@@ -2204,7 +2230,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")
]) ])
@@ -2262,6 +2292,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ! IMPORTANT: CHANGED BEHAVIOR -> Now this endpoint will throw a descriptive error if the user doesn't have access to the value of the secret itself.
server.route({ server.route({
method: "GET", method: "GET",
url: "/raw/:secretName/secret-reference-tree", url: "/raw/:secretName/secret-reference-tree",
@@ -2314,6 +2345,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
// ? No work needed, does not expose secret value.
server.route({ server.route({
method: "POST", method: "POST",
url: "/backfill-secret-references", url: "/backfill-secret-references",
@@ -33,7 +33,7 @@ export type TImportDataIntoInfisicalDTO = {
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
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">;
@@ -29,7 +29,7 @@ export type TExternalMigrationQueueFactoryDep = {
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
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">;
@@ -2,7 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability";
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 { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
@@ -92,7 +96,7 @@ export const integrationServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment, environment: sourceEnvironment,
secretPath secretPath
@@ -175,7 +179,7 @@ export const integrationServiceFactory = ({
if (environment || secretPath) { if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: newEnvironment, environment: newEnvironment,
secretPath: newSecretPath secretPath: newSecretPath
@@ -11,7 +11,11 @@ import {
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { 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";
@@ -747,7 +751,10 @@ export const projectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSub.Secrets
);
const project = await projectDAL.findProjectById(projectId); const project = await projectDAL.findProjectById(projectId);
@@ -5,7 +5,11 @@ 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 { 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";
@@ -90,7 +94,7 @@ 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: data.environment, environment: data.environment,
secretPath: data.path secretPath: data.path
@@ -402,7 +406,7 @@ 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: secretImportDoc.importEnv.slug, environment: secretImportDoc.importEnv.slug,
secretPath: secretImportDoc.importPath secretPath: secretImportDoc.importPath
@@ -596,7 +600,7 @@ export const secretImportServiceFactory = ({
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( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug, environment: el.importEnv.slug,
secretPath: el.importPath secretPath: el.importPath
@@ -647,7 +651,7 @@ export const secretImportServiceFactory = ({
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( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment, environment: expandEnvironment,
secretPath: expandSecretPath, secretPath: expandSecretPath,
@@ -667,7 +671,7 @@ export const secretImportServiceFactory = ({
const allowedImports = secretImports.filter((el) => const allowedImports = secretImports.filter((el) =>
permission.can( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug, environment: el.importEnv.slug,
secretPath: el.importPath secretPath: el.importPath
@@ -683,7 +687,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
)
) )
})); }));
}; };
@@ -3,7 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability";
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 { import {
ProjectPermissionActions, ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
@@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({
); );
ForbiddenError.from(projectPermission).throwUnlessCan( ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath secretPath
@@ -270,7 +270,7 @@ export const secretSyncServiceFactory = ({
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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: updatedEnvironment, environment: updatedEnvironment,
secretPath: updatedSecretPath secretPath: 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,
@@ -102,6 +102,19 @@ export const fnSecretBulkInsert = async ({
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
})) }))
); );
const secretTags = await secretTagDAL.find({
$in: {
id: newSecretTags.map((el) => el.secret_tagsId)
}
});
const secretTagsWithSlugs = await secretTagDAL.find({
$in: {
id: secretTags.map((el) => el.id)
}
});
const secretVersions = await secretVersionDAL.insertMany( const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ sanitizedInputSecrets.map((el) => ({
...el, ...el,
@@ -137,6 +150,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
@@ -623,13 +637,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,
@@ -643,5 +657,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: "<hidden-by-infisical>",
secretValueHidden: true
}
: {
secretValue: secret.value || "",
secretValueHidden: false
})
}); });
@@ -11,12 +11,16 @@ import {
TSecretsV2 TSecretsV2
} from "@app/db/schemas"; } 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 { 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";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenReadSecretError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { diff, groupBy } from "@app/lib/fn"; import { diff, groupBy } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex"; import { setKnexStringValue } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -252,7 +256,7 @@ export const secretV2BridgeServiceFactory = ({
const { secretName, type, ...inputSecretData } = inputSecret; const { secretName, type, ...inputSecretData } = inputSecret;
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -271,8 +275,8 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId projectId
}); });
const secret = await secretDAL.transaction((tx) => const secret = await secretDAL.transaction(async (tx) => {
fnSecretBulkInsert({ const [createdSecret] = await fnSecretBulkInsert({
folderId, folderId,
orgId: actorOrgId, orgId: actorOrgId,
inputSecrets: [ inputSecrets: [
@@ -302,8 +306,10 @@ export const secretV2BridgeServiceFactory = ({
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
tx tx
}) });
);
return createdSecret;
});
if (inputSecret.type === SecretType.Shared) { if (inputSecret.type === SecretType.Shared) {
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
@@ -318,7 +324,7 @@ export const secretV2BridgeServiceFactory = ({
} }
return reshapeBridgeSecret(projectId, environment, secretPath, { return reshapeBridgeSecret(projectId, environment, secretPath, {
...secret[0], ...secret,
value: inputSecret.secretValue, value: inputSecret.secretValue,
comment: inputSecret.secretComment || "" comment: inputSecret.secretComment || ""
}); });
@@ -390,7 +396,7 @@ export const secretV2BridgeServiceFactory = ({
} }
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -407,7 +413,7 @@ export const secretV2BridgeServiceFactory = ({
// now check with new ids // now check with new ids
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -424,7 +430,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -507,11 +513,27 @@ export const secretV2BridgeServiceFactory = ({
}); });
} }
return reshapeBridgeSecret(projectId, environment, secretPath, { const secretValueHidden = !permission.can(
...updatedSecret[0], ProjectPermissionSecretActions.ReadValue,
value: inputSecret.secretValue || "", subject(ProjectPermissionSub.Secrets, {
comment: inputSecret.secretComment || "" environment,
}); secretPath,
secretName: inputSecret.secretName, // ! Note(Daniel): We are checking for the EXISTING secret name, not the new secret name.
secretTags: tags?.map((el) => el.slug) // ! Note(Daniel): Same here
})
);
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...updatedSecret[0],
value: inputSecret.secretValue || "",
comment: inputSecret.secretComment || ""
},
secretValueHidden
);
}; };
const deleteSecret = async ({ const deleteSecret = async ({
@@ -557,7 +579,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -599,15 +621,32 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId projectId
}); });
return reshapeBridgeSecret(projectId, environment, secretPath, {
...deletedSecret[0], const secretValueHidden = !permission.can(
value: deletedSecret[0].encryptedValue ProjectPermissionSecretActions.ReadValue,
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() subject(ProjectPermissionSub.Secrets, {
: "", environment,
comment: deletedSecret[0].encryptedComment secretPath,
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() secretName: secretToDelete.key,
: "" secretTags: secretToDelete.tags?.map((el) => el.slug)
}); })
);
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...deletedSecret[0],
value: deletedSecret[0].encryptedValue
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
: "",
comment: deletedSecret[0].encryptedComment
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
: ""
},
secretValueHidden
);
}; };
// get unique secrets count for multiple envs // get unique secrets count for multiple envs
@@ -635,7 +674,10 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSub.Secrets
);
} }
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -682,7 +724,10 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSub.Secrets
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return 0; if (!folder) return 0;
@@ -693,7 +738,7 @@ export const secretV2BridgeServiceFactory = ({
}; };
const getSecretsByFolderMappings = async ( const getSecretsByFolderMappings = async (
{ projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO, { projectId, userId, filters, folderMappings, filterByAction }: TGetSecretsRawByFolderMappingsDTO,
projectPermission: Awaited<ReturnType<typeof permissionService.getProjectPermission>>["permission"] projectPermission: Awaited<ReturnType<typeof permissionService.getProjectPermission>>["permission"]
) => { ) => {
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
@@ -710,10 +755,13 @@ export const secretV2BridgeServiceFactory = ({
projectId projectId
}); });
// eslint-disable-next-line no-param-reassign
if (!filterByAction) filterByAction = ProjectPermissionSecretActions.ReadValue;
const decryptedSecrets = secrets const decryptedSecrets = secrets
.filter((el) => .filter((el) =>
projectPermission.can( projectPermission.can(
ProjectPermissionActions.Read, filterByAction as ProjectPermissionSecretActions, // ? Typescript assumes that filterByAction may be undefined, which is not true, so we are casting it to ProjectPermissionSecretActions
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: groupedFolderMappings[el.folderId][0].environment, environment: groupedFolderMappings[el.folderId][0].environment,
secretPath: groupedFolderMappings[el.folderId][0].path, secretPath: groupedFolderMappings[el.folderId][0].path,
@@ -722,8 +770,19 @@ export const secretV2BridgeServiceFactory = ({
}) })
) )
) )
.map((secret) => .map((secret) => {
reshapeBridgeSecret( // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend.
const secretValueHidden = !projectPermission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: groupedFolderMappings[secret.folderId][0].environment,
secretPath: groupedFolderMappings[secret.folderId][0].path,
secretName: secret.key,
secretTags: secret.tags.map((i) => i.slug)
})
);
return reshapeBridgeSecret(
projectId, projectId,
groupedFolderMappings[secret.folderId][0].environment, groupedFolderMappings[secret.folderId][0].environment,
groupedFolderMappings[secret.folderId][0].path, groupedFolderMappings[secret.folderId][0].path,
@@ -735,9 +794,10 @@ export const secretV2BridgeServiceFactory = ({
comment: secret.encryptedComment comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: "" : ""
} },
) secretValueHidden
); );
});
return decryptedSecrets; return decryptedSecrets;
}; };
@@ -766,7 +826,10 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
if (!isInternal) { if (!isInternal) {
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSub.Secrets
);
} }
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -786,7 +849,8 @@ export const secretV2BridgeServiceFactory = ({
projectId, projectId,
folderMappings, folderMappings,
filters: params, filters: params,
userId: actorId userId: actorId,
filterByAction: ProjectPermissionSecretActions.DescribeSecret
}, },
permission permission
); );
@@ -801,6 +865,7 @@ export const secretV2BridgeServiceFactory = ({
projectId, projectId,
actor, actor,
actorOrgId, actorOrgId,
viewSecretValue,
actorAuthMethod, actorAuthMethod,
includeImports, includeImports,
recursive, recursive,
@@ -816,7 +881,14 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: path,
secretTags: params.tagSlugs
})
);
let paths: { folderId: string; path: string }[] = []; let paths: { folderId: string; path: string }[] = [];
@@ -854,28 +926,58 @@ export const secretV2BridgeServiceFactory = ({
}); });
const decryptedSecrets = secrets const decryptedSecrets = secrets
.filter((el) => .filter((el) => {
permission.can( if (viewSecretValue) {
ProjectPermissionActions.Read, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
);
}
return permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath: groupedPaths[el.folderId][0].path, secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key, secretName: el.key,
secretTags: el.tags.map((i) => i.slug) secretTags: el.tags.map((i) => i.slug)
}) })
) );
) })
.map((secret) => .map((secret) => {
reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { const secretValueHidden =
...secret, !viewSecretValue ||
value: secret.encryptedValue !permission.can(
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() ProjectPermissionSecretActions.ReadValue,
: "", subject(ProjectPermissionSub.Secrets, {
comment: secret.encryptedComment environment,
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() secretPath: groupedPaths[secret.folderId][0].path,
: "" secretName: secret.key,
}) secretTags: secret.tags.map((i) => i.slug)
); })
);
return reshapeBridgeSecret(
projectId,
environment,
groupedPaths[secret.folderId][0].path,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
},
secretValueHidden
);
});
const { expandSecretReferences } = expandSecretReferencesFactory({ const { expandSecretReferences } = expandSecretReferencesFactory({
projectId, projectId,
@@ -883,8 +985,9 @@ export const secretV2BridgeServiceFactory = ({
secretDAL, secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
// ? Question(Daniel): Will throw an error if the user doesn't have access to any of the expanded secrets.
permission.can( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment, environment: expandEnvironment,
secretPath: expandSecretPath, secretPath: expandSecretPath,
@@ -929,16 +1032,28 @@ export const secretV2BridgeServiceFactory = ({
secretImportDAL, secretImportDAL,
expandSecretReferences, expandSecretReferences,
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( return (
ProjectPermissionActions.Read, permission.can(
subject(ProjectPermissionSub.Secrets, { ProjectPermissionSecretActions.ReadValue,
environment: expandEnvironment, subject(ProjectPermissionSub.Secrets, {
secretPath: expandSecretPath, environment: expandEnvironment,
secretName: expandSecretKey, secretPath: expandSecretPath,
secretTags: expandSecretTags secretName: expandSecretKey,
}) secretTags: expandSecretTags
) })
) &&
permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
);
}
}); });
return { return {
@@ -958,6 +1073,7 @@ export const secretV2BridgeServiceFactory = ({
type, type,
secretName, secretName,
version, version,
viewSecretValue,
includeImports, includeImports,
expandSecretReferences: shouldExpandSecretReferences expandSecretReferences: shouldExpandSecretReferences
}: TGetASecretDTO) => { }: TGetASecretDTO) => {
@@ -1019,7 +1135,7 @@ export const secretV2BridgeServiceFactory = ({
)); ));
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath: path, secretPath: path,
@@ -1028,26 +1144,31 @@ export const secretV2BridgeServiceFactory = ({
}) })
); );
// this will throw if the user doesn't have read value permission no matter what
// because if its an expansion, it will fully depend on the value.
const { expandSecretReferences } = expandSecretReferencesFactory({ const { expandSecretReferences } = expandSecretReferencesFactory({
projectId, projectId,
folderDAL, folderDAL,
secretDAL, secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
permission.can( return permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment, environment: expandEnvironment,
secretPath: expandSecretPath, secretPath: expandSecretPath,
secretName: expandSecretKey, secretName: expandSecretKey,
secretTags: expandSecretTags secretTags: expandSecretTags
}) })
) );
}
}); });
// now if secret is not found // now if secret is not found
// then search for imported secrets // then search for imported secrets
// here we consider the import order also thus starting from bottom // here we consider the import order also thus starting from bottom
// currently filters out the secrets that the user doesn't have access to read value on
if (!secret && includeImports) { if (!secret && includeImports) {
const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
@@ -1057,16 +1178,28 @@ export const secretV2BridgeServiceFactory = ({
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
permission.can( return (
ProjectPermissionActions.Read, permission.can(
subject(ProjectPermissionSub.Secrets, { ProjectPermissionSecretActions.ReadValue,
environment: expandEnvironment, subject(ProjectPermissionSub.Secrets, {
secretPath: expandSecretPath, environment: expandEnvironment,
secretName: expandSecretKey, secretPath: expandSecretPath,
secretTags: expandSecretTags secretName: expandSecretKey,
}) secretTags: expandSecretTags
) })
) &&
permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
);
}
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -1099,13 +1232,41 @@ export const secretV2BridgeServiceFactory = ({
secretValue = expandedSecretValue || ""; secretValue = expandedSecretValue || "";
} }
return reshapeBridgeSecret(projectId, environment, path, { let secretValueHidden = true;
...secret,
value: secretValue, if (viewSecretValue) {
comment: secret.encryptedComment if (
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() !permission.can(
: "" ProjectPermissionSecretActions.ReadValue,
}); subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: path,
secretName,
secretTags: (secret?.tags || []).map((el) => el.slug)
})
)
) {
throw new ForbiddenReadSecretError({
message: `You do not have permission to view secret value on secret with name '${secretName}'`
});
}
secretValueHidden = false;
}
return reshapeBridgeSecret(
projectId,
environment,
path,
{
...secret,
value: secretValue,
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
},
secretValueHidden
);
}; };
const createManySecret = async ({ const createManySecret = async ({
@@ -1173,7 +1334,7 @@ export const secretV2BridgeServiceFactory = ({
inputSecrets.forEach((el) => { inputSecrets.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -1201,8 +1362,8 @@ export const secretV2BridgeServiceFactory = ({
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
const newSecrets = await secretDAL.transaction(async (tx) => const newSecrets = await secretDAL.transaction(async (tx) => {
fnSecretBulkInsert({ const createdSecrets = await fnSecretBulkInsert({
inputSecrets: inputSecrets.map((el) => { inputSecrets: inputSecrets.map((el) => {
const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences;
@@ -1231,8 +1392,19 @@ export const secretV2BridgeServiceFactory = ({
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
tx tx
}) });
);
const secs = await secretDAL.find(
{
$in: {
id: createdSecrets.map((el) => el.id)
}
},
{ tx }
);
return secs;
});
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -1244,13 +1416,29 @@ export const secretV2BridgeServiceFactory = ({
environmentSlug: folder.environment.slug environmentSlug: folder.environment.slug
}); });
return newSecrets.map((el) => return newSecrets.map((el) => {
reshapeBridgeSecret(projectId, environment, secretPath, { const secretValueHidden = !permission.can(
...el, ProjectPermissionSecretActions.ReadValue,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", subject(ProjectPermissionSub.Secrets, {
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" environment,
}) secretPath,
); secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
);
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
},
secretValueHidden
);
});
}; };
const updateManySecret = async ({ const updateManySecret = async ({
@@ -1293,7 +1481,17 @@ export const secretV2BridgeServiceFactory = ({
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
const updatedSecrets: Array<TSecretsV2 & { secretPath: string }> = []; const updatedSecrets: Array<
TSecretsV2 & {
secretPath: string;
tags: {
id: string;
slug: string;
color?: string | null;
name: string;
}[];
}
> = [];
await secretDAL.transaction(async (tx) => { await secretDAL.transaction(async (tx) => {
for await (const folder of folders) { for await (const folder of folders) {
if (!folder) throw new NotFoundError({ message: "Folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
@@ -1344,7 +1542,7 @@ export const secretV2BridgeServiceFactory = ({
secretsToUpdateInDB.forEach((el) => { secretsToUpdateInDB.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -1364,7 +1562,7 @@ export const secretV2BridgeServiceFactory = ({
if (updateMode === SecretUpdateMode.Upsert) { if (updateMode === SecretUpdateMode.Upsert) {
secretsToCreate.forEach((el) => { secretsToCreate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -1378,7 +1576,7 @@ export const secretV2BridgeServiceFactory = ({
// check again to avoid non authorized tags are removed // check again to avoid non authorized tags are removed
secretsToUpdate.forEach((el) => { secretsToUpdate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -1430,7 +1628,7 @@ export const secretV2BridgeServiceFactory = ({
secretsWithNewName.forEach((el) => { secretsWithNewName.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -1455,7 +1653,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
await $validateSecretReferences(projectId, permission, secretReferences, tx); await $validateSecretReferences(projectId, permission, secretReferences, tx);
const bulkUpdatedSecrets = await fnSecretBulkUpdate({ const bulkUpdatedSecretsRes = await fnSecretBulkUpdate({
folderId, folderId,
orgId: actorOrgId, orgId: actorOrgId,
tx, tx,
@@ -1492,9 +1690,21 @@ export const secretV2BridgeServiceFactory = ({
secretVersionTagDAL, secretVersionTagDAL,
resourceMetadataDAL resourceMetadataDAL
}); });
const bulkUpdatedSecrets = await secretDAL.find(
{
$in: {
id: bulkUpdatedSecretsRes.map((el) => el.id)
}
},
{
tx
}
);
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
if (updateMode === SecretUpdateMode.Upsert) { if (updateMode === SecretUpdateMode.Upsert) {
const bulkInsertedSecrets = await fnSecretBulkInsert({ const bulkInsertedSecretsRes = await fnSecretBulkInsert({
inputSecrets: secretsToCreate.map((el) => { inputSecrets: secretsToCreate.map((el) => {
const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences;
@@ -1524,6 +1734,16 @@ export const secretV2BridgeServiceFactory = ({
secretVersionTagDAL, secretVersionTagDAL,
tx tx
}); });
const bulkInsertedSecrets = await secretDAL.find(
{
$in: {
id: bulkInsertedSecretsRes.map((el) => el.id)
}
},
{ tx }
);
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
} }
} }
@@ -1545,13 +1765,33 @@ export const secretV2BridgeServiceFactory = ({
) )
); );
return updatedSecrets.map((el) => return updatedSecrets.map((el) => {
reshapeBridgeSecret(projectId, environment, el.secretPath, { const secretValueHidden = !permission.can(
...el, ProjectPermissionSecretActions.ReadValue,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", subject(ProjectPermissionSub.Secrets, {
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" environment,
}) secretPath: el.secretPath,
); secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
);
return {
...reshapeBridgeSecret(
projectId,
environment,
el.secretPath,
{
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: ""
},
secretValueHidden
)
};
});
}; };
const deleteManySecret = async ({ const deleteManySecret = async ({
@@ -1613,7 +1853,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
secretsToDelete.forEach((el) => { secretsToDelete.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -1652,13 +1892,35 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId projectId
}); });
return secretsDeleted.map((el) => return secretsDeleted.map((el) => {
reshapeBridgeSecret(projectId, environment, secretPath, { const secretToDeleteMatch = secretsToDelete.find(
...el, (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", );
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
}) const secretValueHidden =
); !secretToDeleteMatch ||
!permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.key,
secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
})
);
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
},
secretValueHidden
);
});
}; };
const getSecretVersions = async ({ const getSecretVersions = async ({
@@ -1689,6 +1951,7 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId: folder.projectId projectId: folder.projectId
}); });
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
return secretVersions.map((el) => return secretVersions.map((el) =>
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
@@ -1793,7 +2056,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
sourceSecrets.forEach((secret) => { sourceSecrets.forEach((secret) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment, environment: sourceEnvironment,
secretPath: sourceSecretPath, secretPath: sourceSecretPath,
@@ -1876,7 +2139,7 @@ export const secretV2BridgeServiceFactory = ({
// permission check whether can create or edit the ones in the destination folder // permission check whether can create or edit the ones in the destination folder
locallyCreatedSecrets.forEach((secret) => { locallyCreatedSecrets.forEach((secret) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment, environment: destinationEnvironment,
secretPath: destinationEnvironment, secretPath: destinationEnvironment,
@@ -1888,7 +2151,7 @@ export const secretV2BridgeServiceFactory = ({
locallyUpdatedSecrets.forEach((secret) => { locallyUpdatedSecrets.forEach((secret) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment, environment: destinationEnvironment,
secretPath: destinationEnvironment, secretPath: destinationEnvironment,
@@ -2125,7 +2388,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2149,7 +2412,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -2158,7 +2421,7 @@ export const secretV2BridgeServiceFactory = ({
}) })
); );
const secretValue = secret.encryptedValue const decryptedSecretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: ""; : "";
@@ -2169,7 +2432,7 @@ export const secretV2BridgeServiceFactory = ({
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) =>
permission.can( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment, environment: expandEnvironment,
secretPath: expandSecretPath, secretPath: expandSecretPath,
@@ -2179,10 +2442,26 @@ export const secretV2BridgeServiceFactory = ({
) )
}); });
if (
!permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: (secret?.tags || []).map((el) => el.slug)
})
)
) {
throw new ForbiddenRequestError({
message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.`
});
}
const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({
environment, environment,
secretPath, secretPath,
value: secretValue value: decryptedSecretValue
}); });
return { tree: stackTrace, value: expandedValue }; return { tree: stackTrace, value: expandedValue };
@@ -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,7 @@ export type TGetSecretsDTO = {
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
viewSecretValue: boolean;
metadataFilter?: { metadataFilter?: {
key?: string; key?: string;
value?: string; value?: string;
@@ -57,6 +59,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 & {
@@ -166,7 +169,7 @@ export type TFnSecretBulkInsert = {
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">;
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">;
}; };
@@ -190,7 +193,7 @@ export type TFnSecretBulkUpdate = {
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">; secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">;
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">;
tx?: Knex; tx?: Knex;
}; };
@@ -332,4 +335,5 @@ 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;
}; };
+34 -8
View File
@@ -13,7 +13,7 @@ import {
TSecrets TSecrets
} from "@app/db/schemas"; } 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 { ProjectPermissionSecretActions, ProjectPermissionSub } 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,
@@ -190,7 +190,7 @@ export const recursivelyGetSecretPaths = ({
const allowedPaths = paths.filter( const allowedPaths = paths.filter(
(folder) => (folder) =>
permission.can( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath: folder.path secretPath: folder.path
@@ -344,6 +344,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 +363,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
})
: "<hidden-by-infisical>";
let secretComment = ""; let secretComment = "";
@@ -385,6 +388,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,
@@ -1197,3 +1201,25 @@ export const fnDeleteProjectSecretReminders = async (
} }
} }
}; };
export const conditionallyHideSecretValue = (
shouldHideValue: boolean,
{
secretValueCiphertext,
secretValueIV,
secretValueTag
}: {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
) => {
const hiddenPlaceholder = "hidden-by-infisical>";
return {
secretValueCiphertext: shouldHideValue ? hiddenPlaceholder : secretValueCiphertext,
secretValueIV: shouldHideValue ? hiddenPlaceholder : secretValueIV,
secretValueTag: shouldHideValue ? hiddenPlaceholder : secretValueTag,
secretValueHidden: shouldHideValue
};
};
+139 -52
View File
@@ -13,7 +13,11 @@ 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 { 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 +52,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,
@@ -204,7 +209,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -322,7 +327,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -444,7 +449,22 @@ export const secretServiceFactory = ({
environmentSlug: folder.environment.slug environmentSlug: folder.environment.slug
}); });
} }
return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
const secretValueHidden = !permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: path
})
);
return {
...updatedSecret[0],
...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]),
workspace: projectId,
environment,
secretPath: path
};
}; };
const deleteSecret = async ({ const deleteSecret = async ({
@@ -467,7 +487,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -540,7 +560,19 @@ export const secretServiceFactory = ({
}); });
} }
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; const secretValueHidden = !permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { 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,7 +621,7 @@ 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -614,7 +646,7 @@ export const secretServiceFactory = ({
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug, environment: importEnv.slug,
secretPath: importPath secretPath: importPath
@@ -671,7 +703,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
@@ -721,7 +753,7 @@ export const secretServiceFactory = ({
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug, environment: importEnv.slug,
secretPath: importPath secretPath: importPath
@@ -739,6 +771,7 @@ export const secretServiceFactory = ({
if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) {
return { return {
...importedSecrets[i].secrets[j], ...importedSecrets[i].secrets[j],
secretValueHidden: false,
workspace: projectId, workspace: projectId,
environment: importedSecrets[i].environment, environment: importedSecrets[i].environment,
secretPath: importedSecrets[i].secretPath secretPath: importedSecrets[i].secretPath
@@ -749,7 +782,13 @@ export const secretServiceFactory = ({
} }
if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` });
return { ...secret, workspace: projectId, environment, secretPath: path }; return {
...secret,
secretValueHidden: false, // Always false because we check permission at the beginning of the function
workspace: projectId,
environment,
secretPath: path
};
}; };
const createManySecret = async ({ const createManySecret = async ({
@@ -771,7 +810,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -859,7 +898,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -901,8 +940,8 @@ export const secretServiceFactory = ({
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" });
const references = await getSecretReference(projectId); const references = await getSecretReference(projectId);
const secrets = await secretDAL.transaction(async (tx) => const secrets = await secretDAL.transaction(async (tx) => {
fnSecretBulkUpdate({ const updatedSecrets = await fnSecretBulkUpdate({
folderId, folderId,
projectId, projectId,
tx, tx,
@@ -932,8 +971,18 @@ export const secretServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL
}) });
);
const secretValueHidden = !permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
return updatedSecrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
});
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -967,7 +1016,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,7 +1068,15 @@ export const secretServiceFactory = ({
} }
} }
return secrets; const secretValueHidden = !permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
return secrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
}); });
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
@@ -1180,6 +1237,7 @@ export const secretServiceFactory = ({
secretName, secretName,
path: secretPath, path: secretPath,
environment, environment,
viewSecretValue: false,
type: "shared" type: "shared"
}); });
@@ -1194,10 +1252,11 @@ 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,
ProjectPermissionSecretActions.Edit
].filter((action) => ].filter((action) =>
entityPermission.permission.can( entityPermission.permission.can(
action, action,
@@ -1234,6 +1293,7 @@ export const secretServiceFactory = ({
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
viewSecretValue,
environment, environment,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
@@ -1249,6 +1309,7 @@ export const secretServiceFactory = ({
actorId, actorId,
actor, actor,
actorOrgId, actorOrgId,
viewSecretValue,
environment, environment,
path, path,
recursive, recursive,
@@ -1285,14 +1346,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
) )
); );
@@ -1387,6 +1454,7 @@ export const secretServiceFactory = ({
path, path,
actor, actor,
environment, environment,
viewSecretValue,
projectId: workspaceId, projectId: workspaceId,
expandSecretReferences, expandSecretReferences,
projectSlug, projectSlug,
@@ -1406,6 +1474,7 @@ export const secretServiceFactory = ({
includeImports, includeImports,
actorAuthMethod, actorAuthMethod,
path, path,
viewSecretValue,
actorOrgId, actorOrgId,
actor, actor,
actorId, actorId,
@@ -1436,6 +1505,7 @@ export const secretServiceFactory = ({
message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`,
name: "bot_not_found_error" name: "bot_not_found_error"
}); });
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) { if (expandSecretReferences) {
@@ -1454,7 +1524,10 @@ export const secretServiceFactory = ({
decryptedSecret.secretValue = expandedSecretValue || ""; decryptedSecret.secretValue = expandedSecretValue || "";
} }
return { secretMetadata: undefined, ...decryptedSecret }; return {
secretMetadata: undefined,
...decryptedSecret
};
}; };
const createSecretRaw = async ({ const createSecretRaw = async ({
@@ -1605,7 +1678,16 @@ export const secretServiceFactory = ({
tags: tagIds tags: tagIds
}); });
return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; return {
type: SecretProtectionType.Direct as const,
secret: decryptSecretRaw(
{
...secret,
secretValueHidden: false
},
botKey
)
};
}; };
const updateSecretRaw = async ({ const updateSecretRaw = async ({
@@ -2001,7 +2083,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)
) )
}; };
}; };
@@ -2307,6 +2389,7 @@ export const secretServiceFactory = ({
return secretVersions.map((el) => return secretVersions.map((el) =>
decryptSecretRaw( decryptSecretRaw(
{ {
secretValueHidden: false,
...el, ...el,
workspace: folder.projectId, workspace: folder.projectId,
environment: folder.environment.envSlug, environment: folder.environment.envSlug,
@@ -2340,7 +2423,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2446,7 +2529,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 })
); );
@@ -2637,29 +2720,33 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( const permissionChecks = [
ProjectPermissionActions.Delete, {
subject(ProjectPermissionSub.Secrets, { action: ProjectPermissionSecretActions.Delete,
environment: sourceEnvironment, subject: {
secretPath: sourceSecretPath environment: sourceEnvironment,
}) secretPath: sourceSecretPath
); }
},
{
action: ProjectPermissionSecretActions.Create,
subject: {
environment: destinationEnvironment,
secretPath: destinationSecretPath
}
},
{
action: ProjectPermissionSecretActions.Edit,
subject: {
environment: destinationEnvironment,
secretPath: destinationSecretPath
}
}
] as const;
ForbiddenError.from(permission).throwUnlessCan( for (const { action, subject: permissionSubject } of permissionChecks) {
ProjectPermissionActions.Create, ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, permissionSubject));
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) {
@@ -180,6 +180,7 @@ export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
path: string; path: string;
environment: string; environment: string;
viewSecretValue: boolean;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
@@ -205,6 +206,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;
@@ -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 })
); );
}); });
@@ -2,6 +2,7 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useSt
import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { twMerge } from "tailwind-merge";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useDebounce, useToggle } from "@app/hooks"; import { useDebounce, useToggle } from "@app/hooks";
@@ -54,6 +55,7 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
secretPath?: string; secretPath?: string;
environment?: string; environment?: string;
containerClassName?: string; containerClassName?: string;
secretValueHidden?: boolean;
}; };
type ReferenceItem = { type ReferenceItem = {
@@ -70,6 +72,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
containerClassName, containerClassName,
secretPath: propSecretPath, secretPath: propSecretPath,
environment: propEnvironment, environment: propEnvironment,
secretValueHidden,
...props ...props
}, },
ref ref
@@ -275,6 +278,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
ref={handleRef} ref={handleRef}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
value={value} value={value}
valueHidden={secretValueHidden}
onFocus={() => setIsFocused.on()} onFocus={() => setIsFocused.on()}
onBlur={(evt) => { onBlur={(evt) => {
// should not on blur when its mouse down selecting a item from suggestion // should not on blur when its mouse down selecting a item from suggestion
@@ -282,7 +286,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
setIsFocused.off(); setIsFocused.off();
}} }}
onChange={(e) => onChange?.(e.target.value)} onChange={(e) => onChange?.(e.target.value)}
containerClassName={containerClassName} containerClassName={twMerge(containerClassName)}
/> />
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
@@ -1,9 +1,13 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import { forwardRef, TextareaHTMLAttributes } from "react"; import { forwardRef, TextareaHTMLAttributes } from "react";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { Tooltip } from "../Tooltip";
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g; const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
const replaceContentWithDot = (str: string) => { const replaceContentWithDot = (str: string) => {
let finalStr = ""; let finalStr = "";
@@ -14,7 +18,26 @@ const replaceContentWithDot = (str: string) => {
return finalStr; return finalStr;
}; };
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => { const syntaxHighlight = (
content?: string | null,
isVisible?: boolean,
isImport?: boolean,
valueHidden?: boolean
) => {
if (valueHidden && !content)
return (
<div className="flex items-center gap-2">
<span className="opacity-60">VALUE HIDDEN</span>
<div className="relative z-50">
<Tooltip
className="!relative !z-50 !break-normal !font-inter"
content="You do not have permission to read the value of this secret."
>
<FontAwesomeIcon icon={faQuestionCircle} className="text-xs" />
</Tooltip>
</div>
</div>
);
if (isImport && !content) return "IMPORTED"; if (isImport && !content) return "IMPORTED";
if (content === "") return "EMPTY"; if (content === "") return "EMPTY";
if (!content) return "EMPTY"; if (!content) return "EMPTY";
@@ -50,6 +73,7 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
isImport?: boolean; isImport?: boolean;
isReadOnly?: boolean; isReadOnly?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
valueHidden?: boolean;
containerClassName?: string; containerClassName?: string;
}; };
@@ -61,6 +85,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
value, value,
isVisible, isVisible,
isImport, isImport,
valueHidden,
containerClassName, containerClassName,
onBlur, onBlur,
isDisabled, isDisabled,
@@ -81,7 +106,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
<pre aria-hidden className="m-0"> <pre aria-hidden className="m-0">
<code className={`inline-block w-full ${commonClassName}`}> <code className={`inline-block w-full ${commonClassName}`}>
<span style={{ whiteSpace: "break-spaces" }}> <span style={{ whiteSpace: "break-spaces" }}>
{syntaxHighlight(value, isVisible || isSecretFocused, isImport)} {syntaxHighlight(value, isVisible || isSecretFocused, isImport, valueHidden)}
</span> </span>
</code> </code>
</pre> </pre>
@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionSecretActions {
DescribeSecret = "read",
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 +146,7 @@ export type SecretImportSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionSecretActions,
( (
| ProjectPermissionSub.Secrets | ProjectPermissionSub.Secrets
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
@@ -207,6 +207,7 @@ export const useGetProjectSecretsDetails = (
search = "", search = "",
includeSecrets, includeSecrets,
includeFolders, includeFolders,
viewSecretValue,
includeImports, includeImports,
includeDynamicSecrets, includeDynamicSecrets,
tags tags
@@ -231,6 +232,7 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -247,6 +249,7 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -69,6 +69,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>;
@@ -176,6 +176,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,
@@ -68,6 +68,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,
+2
View File
@@ -37,6 +37,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 +62,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;
@@ -35,6 +35,11 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
title, title,
isDisabled isDisabled
}: Props<T>) => { }: Props<T>) => {
if (subject === "secrets") {
console.log("secret subject");
console.log(actions);
}
const { control } = useFormContext<TFormSchema>(); const { control } = useFormContext<TFormSchema>();
const items = useFieldArray({ const items = useFieldArray({
control, control,
@@ -116,25 +121,36 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
<div className="w-1/4">Actions</div> <div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8"> <div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }) => { {actions.map(({ label, value }) => {
if (subject === "secrets") {
console.log("value", value);
}
if (typeof value !== "string") return undefined; if (typeof value !== "string") return undefined;
return ( return (
<Controller <Controller
key={`${el.id}-${label}`} key={`${el.id}-${label}`}
name={`permissions.${subject}.${rootIndex}.${value}` as any} name={`permissions.${subject}.${rootIndex}.${value}` as any}
control={control} control={control}
defaultValue={false} defaultValue={false}
render={({ field }) => ( render={({ field }) => {
<div className="flex items-center justify-center"> if (subject === "secrets") {
<Checkbox console.log("field", field);
isDisabled={isDisabled} }
isChecked={Boolean(field.value)}
onCheckedChange={field.onChange} return (
id={`permissions.${subject}.${rootIndex}.${String(value)}`} <div className="flex items-center justify-center">
> <Checkbox
{label} isDisabled={isDisabled}
</Checkbox> isChecked={Boolean(field.value)}
</div> onCheckedChange={field.onChange}
)} id={`permissions.${subject}.${rootIndex}.${String(value)}`}
>
{label}
</Checkbox>
</div>
);
}}
/> />
); );
})} })}
@@ -9,6 +9,7 @@ import {
PermissionConditionOperators, PermissionConditionOperators,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
TPermissionCondition, TPermissionCondition,
TPermissionConditionOperators TPermissionConditionOperators
@@ -22,6 +23,14 @@ const GeneralPolicyActionSchema = z.object({
create: z.boolean().optional() create: z.boolean().optional()
}); });
const SecretPolicyActionSchema = z.object({
read: z.boolean().optional(), // describe secret
edit: z.boolean().optional(),
delete: z.boolean().optional(),
create: z.boolean().optional(),
readValue: 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 +123,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 +292,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
}); });
return; return;
} }
if (subject === ProjectPermissionSub.Secrets) {
const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret);
const canEdit = action.includes(ProjectPermissionSecretActions.Edit);
const canDelete = action.includes(ProjectPermissionSecretActions.Delete);
const canCreate = action.includes(ProjectPermissionSecretActions.Create);
const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
read: canRead,
create: canCreate,
edit: canEdit,
delete: canDelete,
readValue: canReadValue,
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);
@@ -483,8 +514,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Secrets]: { [ProjectPermissionSub.Secrets]: {
title: "Secrets", title: "Secrets",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Describe Secret", value: "read" },
{ label: "Create", value: "create" }, { label: "Create", value: "create" },
{ label: "Read Value", value: "readValue" },
{ label: "Modify", value: "edit" }, { label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" } { label: "Remove", value: "delete" }
] ]
@@ -39,6 +39,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 +59,7 @@ export const SecretEditRow = ({
isImportedSecret, isImportedSecret,
onSecretUpdate, onSecretUpdate,
secretName, secretName,
secretValueHidden,
onSecretCreate, onSecretCreate,
onSecretDelete, onSecretDelete,
environment, environment,
@@ -151,6 +153,7 @@ export const SecretEditRow = ({
value={field.value as string} value={field.value as string}
key="secret-input" key="secret-input"
isVisible={isVisible} isVisible={isVisible}
secretValueHidden={secretValueHidden}
secretPath={secretPath} secretPath={secretPath}
environment={environment} environment={environment}
isImport={isImportedSecret} isImport={isImportedSecret}
@@ -158,6 +161,7 @@ export const SecretEditRow = ({
)} )}
/> />
</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",
@@ -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)}
@@ -58,6 +58,7 @@ import {
useSelectedSecrets useSelectedSecrets
} from "./SecretMainPage.store"; } from "./SecretMainPage.store";
import { Filter, RowType } from "./SecretMainPage.types"; import { Filter, RowType } from "./SecretMainPage.types";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
const LOADER_TEXT = [ const LOADER_TEXT = [
"Retrieving your encrypted secrets...", "Retrieving your encrypted secrets...",
@@ -103,7 +104,7 @@ const Page = () => {
const projectSlug = currentWorkspace?.slug || ""; const projectSlug = currentWorkspace?.slug || "";
const secretPath = (routerQueryParams.secretPath as string) || "/"; const secretPath = (routerQueryParams.secretPath as string) || "/";
const canReadSecret = permission.can( const canReadSecret = permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -111,6 +112,17 @@ const Page = () => {
secretTags: ["*"] secretTags: ["*"]
}) })
); );
const canReadSecretValue = permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
);
console.log("Can read secret value", canReadSecret);
const canReadSecretImports = permission.can( const canReadSecretImports = permission.can(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
@@ -176,6 +188,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
@@ -52,6 +52,8 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
import { CreateReminderForm } from "./CreateReminderForm"; import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useEffect } from "react";
type Props = { type Props = {
isOpen?: boolean; isOpen?: boolean;
@@ -122,7 +124,7 @@ export const SecretDetailSidebar = ({
const selectTagSlugs = selectedTags.map((i) => i.slug); const selectTagSlugs = selectedTags.map((i) => i.slug);
const cannotEditSecret = permission.cannot( const cannotEditSecret = permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -130,16 +132,31 @@ export const SecretDetailSidebar = ({
secretTags: selectTagSlugs secretTags: selectTagSlugs
}) })
); );
const cannotReadSecretValue = permission.cannot(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
);
const isReadOnly = const isReadOnly =
permission.can( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
secretName: secretKey, secretName: secretKey,
secretTags: selectTagSlugs secretTags: selectTagSlugs
}) })
) && cannotEditSecret; ) &&
cannotEditSecret &&
cannotReadSecretValue;
console.log("cannotReadSecretValue", cannotReadSecretValue);
const overrideAction = watch("overrideAction"); const overrideAction = watch("overrideAction");
const isOverridden = const isOverridden =
@@ -261,7 +278,14 @@ export const SecretDetailSidebar = ({
key="secret-value" key="secret-value"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormControl label="Value"> <FormControl
helperText={
cannotReadSecretValue
? "The value of this secret is hidden because you don't have the read secret value permission."
: undefined
}
label="Value"
>
<InfisicalSecretInput <InfisicalSecretInput
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
environment={environment} environment={environment}
@@ -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,7 @@ import {
SecretReferenceTree SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails"; } from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { import {
FontAwesomeSpriteName, FontAwesomeSpriteName,
formSchema, formSchema,
@@ -99,8 +101,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)
}); });
@@ -123,7 +131,7 @@ export const SecretItem = memo(
const isReadOnly = const isReadOnly =
permission.can( permission.can(
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -132,7 +140,7 @@ export const SecretItem = memo(
}) })
) && ) &&
permission.cannot( permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -141,6 +149,9 @@ export const SecretItem = memo(
}) })
); );
const { secretValueHidden } = secret;
console.log(`Secret Key: ${secret.key}, hidden: ${secretValueHidden}`);
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
@@ -279,12 +290,14 @@ export const SecretItem = memo(
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<InfisicalSecretInput <InfisicalSecretInput
secretValueHidden={secretValueHidden}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
key="secret-value" key="secret-value"
isVisible={isVisible} isVisible={isVisible}
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"
/> />
)} )}