Revert "feat(api/secrets): view secret value permission"

This commit is contained in:
Maidul Islam
2025-03-05 14:15:02 -05:00
committed by GitHub
parent 8e82813894
commit bf6cfbac7a
70 changed files with 806 additions and 2202 deletions
@@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12
const createServiceToken = async ( const createServiceToken = async (
scopes: { environment: string; secretPath: string }[], scopes: { environment: string; secretPath: string }[],
permissions: ("read" | "write" | "readValue")[] permissions: ("read" | "write")[]
) => { ) => {
const projectKeyRes = await testServer.inject({ const projectKeyRes = await testServer.inject({
method: "GET", method: "GET",
@@ -139,7 +139,7 @@ describe("Service token secret ops", async () => {
beforeAll(async () => { beforeAll(async () => {
serviceToken = await createServiceToken( serviceToken = await createServiceToken(
[{ secretPath: "/**", environment: seedData1.environment.slug }], [{ secretPath: "/**", environment: seedData1.environment.slug }],
["read", "write", "readValue"] ["read", "write"]
); );
// this is ensure cli service token decryptiong working fine // this is ensure cli service token decryptiong working fine
@@ -496,7 +496,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized secret path access", async () => { test("Unauthorized secret path access", async () => {
const serviceToken = await createServiceToken( const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }], [{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "readValue", "write"] ["read", "write"]
); );
const fetchSecrets = await testServer.inject({ const fetchSecrets = await testServer.inject({
method: "GET", method: "GET",
@@ -518,7 +518,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized secret environment access", async () => { test("Unauthorized secret environment access", async () => {
const serviceToken = await createServiceToken( const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }], [{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "readValue", "write"] ["read", "write"]
); );
const fetchSecrets = await testServer.inject({ const fetchSecrets = await testServer.inject({
method: "GET", method: "GET",
@@ -540,7 +540,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized write operation", async () => { test("Unauthorized write operation", async () => {
const serviceToken = await createServiceToken( const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }], [{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "readValue"] ["read"]
); );
const writeSecrets = await testServer.inject({ const writeSecrets = await testServer.inject({
method: "POST", method: "POST",
@@ -120,3 +120,4 @@ export default {
}; };
} }
}; };
@@ -1,313 +0,0 @@
import { MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { Knex } from "knex";
import { z } from "zod";
import { selectAllTableCols } from "@app/lib/knex";
import { TableName } from "../schemas";
enum ProjectPermissionSub {
Secrets = "secrets"
}
enum SecretActions {
Read = "read",
ReadValue = "readValue"
}
const UnpackedPermissionSchema = z.object({
subject: z
.union([z.string().min(1), z.string().array()])
.transform((el) => (typeof el !== "string" ? el[0] : el))
.optional(),
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
conditions: z.unknown().optional(),
inverted: z.boolean().optional()
});
const $unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility>>[]));
const $updatePermissionsUp = (permissions: unknown) => {
const parsedPermissions = $unpackPermissions(permissions);
let shouldUpdate = false;
for (let i = 0; i < parsedPermissions.length; i += 1) {
const parsedPermission = parsedPermissions[i];
const { subject, action } = parsedPermission;
if (subject === ProjectPermissionSub.Secrets) {
if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) {
action.push(SecretActions.ReadValue);
parsedPermissions[i] = { ...parsedPermission, action };
shouldUpdate = true;
}
}
}
return {
parsedPermissions,
shouldUpdate
};
};
const $updatePermissionsDown = (permissions: unknown) => {
const parsedPermissions = $unpackPermissions(permissions);
let shouldUpdate = false;
for (let i = 0; i < parsedPermissions.length; i += 1) {
const parsedPermission = parsedPermissions[i];
const { subject, action } = parsedPermission;
if (subject === ProjectPermissionSub.Secrets) {
const readValueIndex = action.indexOf(SecretActions.ReadValue);
if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) {
action.splice(readValueIndex, 1);
parsedPermissions[i] = { ...parsedPermission, action };
shouldUpdate = true;
}
}
}
const repackedPermissions = packRules(parsedPermissions);
return {
repackedPermissions,
shouldUpdate
};
};
const CHUNK_SIZE = 1000;
export async function up(knex: Knex): Promise<void> {
const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles));
const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select(
selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege)
);
const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select(
selectAllTableCols(TableName.ProjectUserAdditionalPrivilege)
);
const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken));
const updatedServiceTokens = serviceTokens.reduce<typeof serviceTokens>((acc, serviceToken) => {
const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only.
if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) {
permissions.push(SecretActions.ReadValue);
acc.push({
...serviceToken,
permissions
});
}
return acc;
}, []);
if (updatedServiceTokens.length > 0) {
for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) {
const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.ServiceToken)
.whereIn(
"id",
chunk.map((t) => t.id)
)
.update({
// @ts-expect-error -- raw query
permissions: knex.raw(
`CASE id
${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
END`,
chunk.map((t) => t.permissions)
)
});
}
}
const updatedRoles = projectRoles.reduce<typeof projectRoles>((acc, projectRole) => {
const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions);
if (shouldUpdate) {
acc.push({
...projectRole,
permissions: JSON.stringify(packRules(parsedPermissions))
});
}
return acc;
}, []);
const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce<
typeof projectIdentityAdditionalPrivileges
>((acc, identityAdditionalPrivilege) => {
const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions);
if (shouldUpdate) {
acc.push({
...identityAdditionalPrivilege,
permissions: JSON.stringify(packRules(parsedPermissions))
});
}
return acc;
}, []);
const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce<
typeof projectUserAdditionalPrivileges
>((acc, userAdditionalPrivilege) => {
const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions);
if (shouldUpdate) {
acc.push({
...userAdditionalPrivilege,
permissions: JSON.stringify(packRules(parsedPermissions))
});
}
return acc;
}, []);
if (updatedRoles.length > 0) {
for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) {
const chunk = updatedRoles.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]);
}
}
if (updatedIdentityAdditionalPrivileges.length > 0) {
for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) {
const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
}
}
if (updatedUserAdditionalPrivileges.length > 0) {
for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) {
const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
}
}
}
export async function down(knex: Knex): Promise<void> {
const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles));
const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select(
selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege)
);
const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select(
selectAllTableCols(TableName.ProjectUserAdditionalPrivilege)
);
const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken));
const updatedServiceTokens = serviceTokens.reduce<typeof serviceTokens>((acc, serviceToken) => {
const { permissions } = serviceToken;
if (permissions.includes(SecretActions.ReadValue)) {
permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1);
acc.push({
...serviceToken,
permissions
});
}
return acc;
}, []);
if (updatedServiceTokens.length > 0) {
for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) {
const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.ServiceToken)
.whereIn(
"id",
chunk.map((t) => t.id)
)
.update({
// @ts-expect-error -- raw query
permissions: knex.raw(
`CASE id
${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
END`,
chunk.map((t) => t.permissions)
)
});
}
}
const updatedRoles = projectRoles.reduce<typeof projectRoles>((acc, projectRole) => {
const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions);
if (shouldUpdate) {
acc.push({
...projectRole,
permissions: JSON.stringify(repackedPermissions)
});
}
return acc;
}, []);
const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce<typeof identityAdditionalPrivileges>(
(acc, identityAdditionalPrivilege) => {
const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions);
if (shouldUpdate) {
acc.push({
...identityAdditionalPrivilege,
permissions: JSON.stringify(repackedPermissions)
});
}
return acc;
},
[]
);
const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce<typeof userAdditionalPrivileges>(
(acc, userAdditionalPrivilege) => {
const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions);
if (shouldUpdate) {
acc.push({
...userAdditionalPrivilege,
permissions: JSON.stringify(repackedPermissions)
});
}
return acc;
},
[]
);
if (updatedRoles.length > 0) {
for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) {
const chunk = updatedRoles.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]);
}
}
if (updatedIdentityAdditionalPrivileges.length > 0) {
for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) {
const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
}
}
if (updatedUserAdditionalPrivileges.length > 0) {
for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) {
const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
}
}
}
@@ -1,11 +1,16 @@
import { z } from "zod"; import { z } from "zod";
import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas"; import {
SecretApprovalRequestsReviewersSchema,
SecretApprovalRequestsSchema,
SecretTagsSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
@@ -245,6 +250,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
} }
}); });
const tagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.array()
.optional();
server.route({ server.route({
method: "GET", method: "GET",
url: "/:id", url: "/:id",
@@ -278,7 +291,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .omit({ _id: true, environment: true, workspace: true, type: true, version: true })
.extend({ .extend({
op: z.string(), op: z.string(),
tags: SanitizedTagSchema.array().optional(), tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish(), secretMetadata: ResourceMetadataSchema.nullish(),
secret: z secret: z
.object({ .object({
@@ -297,7 +310,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
secretComment: z.string().optional(), secretComment: z.string().optional(),
tags: SanitizedTagSchema.array().optional(), tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish() secretMetadata: ResourceMetadataSchema.nullish()
}) })
.optional() .optional()
+2 -2
View File
@@ -1,6 +1,6 @@
import z from "zod"; import z from "zod";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions } 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(ProjectPermissionSecretActions).array(), allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
id: z.string(), id: z.string(),
membershipId: z.string(), membershipId: z.string(),
name: z.string() name: z.string()
@@ -22,11 +22,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 200: z.object({
secretVersions: secretRawSchema secretVersions: secretRawSchema.array()
.extend({
secretValueHidden: z.boolean()
})
.array()
}) })
} }
}, },
@@ -41,7 +37,6 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
offset: req.query.offset, offset: req.query.offset,
secretId: req.params.secretId secretId: req.params.secretId
}); });
return { secretVersions }; return { secretVersions };
} }
}); });
+7 -5
View File
@@ -1,10 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSnapshotsSchema } from "@app/db/schemas"; import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => { export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@@ -31,9 +31,12 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
secretVersions: secretRawSchema secretVersions: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true }) .omit({ _id: true, environment: true, workspace: true, type: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretId: z.string(), secretId: z.string(),
tags: SanitizedTagSchema.array() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
}).array()
}) })
.array(), .array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
@@ -52,7 +55,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId id: req.params.secretSnapshotId
}); });
return { secretSnapshot }; return { secretSnapshot };
} }
}); });
@@ -17,14 +17,6 @@ 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",
@@ -123,7 +115,7 @@ export type IdentityManagementSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretActions, ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
] ]
| [ | [
@@ -437,7 +429,6 @@ const GeneralPermissionSchema = [
}) })
]; ];
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
@@ -469,7 +460,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(ProjectPermissionSecretActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
), ),
conditions: SecretConditionV2Schema.describe( conditions: SecretConditionV2Schema.describe(
@@ -526,6 +517,7 @@ 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,
@@ -558,21 +550,10 @@ const buildAdminPermissionRules = () => {
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionActions.Delete ProjectPermissionActions.Delete
], ],
el el as ProjectPermissionSub
); );
}); });
can(
[
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can( can(
[ [
ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionDynamicSecretActions.ReadRootCredential,
@@ -632,11 +613,10 @@ const buildMemberPermissionRules = () => {
can( can(
[ [
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Create,
ProjectPermissionSecretActions.Create, ProjectPermissionActions.Delete
ProjectPermissionSecretActions.Delete
], ],
ProjectPermissionSub.Secrets ProjectPermissionSub.Secrets
); );
@@ -808,8 +788,7 @@ 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(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
@@ -852,8 +831,6 @@ 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(
@@ -883,14 +860,6 @@ 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
});
}
} }
); );
}); });
@@ -947,17 +916,7 @@ export const backfillPermissionV1SchemaToV2Schema = (
subject: ProjectPermissionSub.SecretImports as const subject: ProjectPermissionSub.SecretImports as const
})); }));
const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({
subject: ProjectPermissionSub.Secrets as const,
...el,
action:
el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue)
? el.action.concat(ProjectPermissionSecretActions.ReadValue)
: el.action
}));
const secretFolderPolicies = secretSubjects const secretFolderPolicies = secretSubjects
.map(({ subject, ...el }) => ({ .map(({ subject, ...el }) => ({
...el, ...el,
// read permission is not needed anymore // read permission is not needed anymore
@@ -999,7 +958,6 @@ export const backfillPermissionV1SchemaToV2Schema = (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts // @ts-ignore-error this is valid ts
secretImportPolicies, secretImportPolicies,
secretPolicies,
dynamicSecretPolicies, dynamicSecretPolicies,
hasReadOnlyFolder.length ? [] : secretFolderPolicies hasReadOnlyFolder.length ? [] : secretFolderPolicies
); );
@@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, 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,12 +88,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretTagDAL: Pick< secretTagDAL: Pick<
TSecretTagDALFactory, TSecretTagDALFactory,
| "findManyTagsById" "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
| "saveTagsToSecret"
| "deleteTagsManySecret"
| "saveTagsToSecretV2"
| "deleteTagsToSecretV2"
| "find"
>; >;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">; secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@@ -111,7 +106,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -919,7 +914,7 @@ export const secretApprovalRequestServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -1006,7 +1001,6 @@ 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,
@@ -1369,9 +1363,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 = ProjectPermissionSecretActions.Create; let action = ProjectPermissionActions.Create;
if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit; if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
if (commit.op === SecretOperations.Delete) action = ProjectPermissionSecretActions.Delete; if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
action, action,
@@ -265,7 +265,6 @@ export const secretReplicationServiceFactory = ({
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
viewSecretValue: true,
hasSecretAccess: () => true hasSecretAccess: () => true
}); });
// secrets that gets replicated across imports // secrets that gets replicated across imports
@@ -15,11 +15,7 @@ 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 { import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
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";
@@ -110,7 +106,7 @@ export const secretRotationServiceFactory = ({
}); });
} }
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -22,11 +22,7 @@ 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 { import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { import {
TGetSnapshotDataDTO, TGetSnapshotDataDTO,
TProjectSnapshotCountDTO, TProjectSnapshotCountDTO,
@@ -38,7 +34,6 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal"; import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal";
import { getFullFolderPath } from "./snapshot-service-fns"; import { getFullFolderPath } from "./snapshot-service-fns";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
type TSecretSnapshotServiceFactoryDep = { type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory; snapshotDAL: TSnapshotDALFactory;
@@ -102,7 +97,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(
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -139,7 +134,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(
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -166,7 +161,6 @@ export const secretSnapshotServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3; const shouldUseBridge = snapshot.projectVersion === 3;
let snapshotDetails; let snapshotDetails;
if (shouldUseBridge) { if (shouldUseBridge) {
@@ -175,110 +169,68 @@ export const secretSnapshotServiceFactory = ({
projectId: snapshot.projectId projectId: snapshot.projectId
}); });
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
snapshotDetails = { snapshotDetails = {
...encryptedSnapshotDetails, ...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
const canReadValue = permission.can( ...el,
ProjectPermissionSecretActions.ReadValue, secretKey: el.key,
subject(ProjectPermissionSub.Secrets, { secretValue: el.encryptedValue
environment: encryptedSnapshotDetails.environment.slug, ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
secretPath: fullFolderPath, : "",
secretName: el.key, secretComment: el.encryptedComment
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
}) : ""
); }))
let secretValue = "";
if (canReadValue) {
secretValue = el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "";
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey: el.key,
secretValueHidden: !canReadValue,
secretValue,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: ""
};
})
}; };
} else { } else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
const { botKey } = await projectBotService.getBotKey(snapshot.projectId); const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
if (!botKey) if (!botKey)
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` }); throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
snapshotDetails = { snapshotDetails = {
...encryptedSnapshotDetails, ...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
const secretKey = decryptSymmetric128BitHexKeyUTF8({ ...el,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext, ciphertext: el.secretKeyCiphertext,
iv: el.secretKeyIV, iv: el.secretKeyIV,
tag: el.secretKeyTag, tag: el.secretKeyTag,
key: botKey key: botKey
}); }),
secretValue: decryptSymmetric128BitHexKeyUTF8({
const canReadValue = permission.can( ciphertext: el.secretValueCiphertext,
ProjectPermissionSecretActions.ReadValue, iv: el.secretValueIV,
subject(ProjectPermissionSub.Secrets, { tag: el.secretValueTag,
environment: encryptedSnapshotDetails.environment.slug, key: botKey
secretPath: fullFolderPath, }),
secretName: secretKey, secretComment:
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
}) ? decryptSymmetric128BitHexKeyUTF8({
); ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
let secretValue = ""; tag: el.secretCommentTag,
key: botKey
if (canReadValue) { })
secretValue = decryptSymmetric128BitHexKeyUTF8({ : ""
ciphertext: el.secretValueCiphertext, }))
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
});
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey,
secretValueHidden: !canReadValue,
secretValue,
secretComment:
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
tag: el.secretCommentTag,
key: botKey
})
: ""
};
})
}; };
} }
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: snapshotDetails.folderId,
envId: snapshotDetails.environment.id
});
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath
})
);
return snapshotDetails; return snapshotDetails;
}; };
-3
View File
@@ -666,7 +666,6 @@ export const SECRETS = {
secretPath: "The path of the secret to attach tags to.", secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)", type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located", environment: "The slug of the environment where the secret is located",
viewSecretValue: "Whether or not to retrieve the secret value.",
projectSlug: "The slug of the project where the secret is located.", projectSlug: "The slug of the project where the secret is located.",
tagSlugs: "An array of existing tag slugs to attach to the secret." tagSlugs: "An array of existing tag slugs to attach to the secret."
}, },
@@ -690,7 +689,6 @@ 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:
@@ -719,7 +717,6 @@ export const RAW_SECRETS = {
secretPath: "The path of the secret to get.", secretPath: "The path of the secret to get.",
version: "The version of the secret to get.", version: "The version of the secret to get.",
type: "The type of the secret to get.", type: "The type of the secret to get.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not." includeImports: "Weather to include imported secrets or not."
}, },
UPDATE: { UPDATE: {
-1
View File
@@ -1,5 +1,4 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
export class DatabaseError extends Error { export class DatabaseError extends Error {
name: string; name: string;
@@ -7,7 +7,6 @@ import {
ProjectRolesSchema, ProjectRolesSchema,
ProjectsSchema, ProjectsSchema,
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
SecretTagsSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -233,11 +232,3 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
kmsCertificateKeyId: true, kmsCertificateKeyId: true,
auditLogsRetentionDays: true auditLogsRetentionDays: true
}); });
export const SanitizedTagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
}).extend({
name: z.string()
});
@@ -1,11 +1,10 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { import {
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionSecretActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { DASHBOARD } from "@app/lib/api-docs"; import { DASHBOARD } from "@app/lib/api-docs";
@@ -16,7 +15,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -117,10 +116,16 @@ 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: SanitizedTagSchema.array().optional() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional(), .optional(),
@@ -289,7 +294,6 @@ 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,
@@ -389,7 +393,6 @@ 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),
@@ -407,10 +410,16 @@ 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: SanitizedTagSchema.array().optional() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional(), .optional(),
@@ -592,25 +601,23 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = ( const secretsRaw = await server.services.secret.getSecretsRaw({
await server.services.secret.getSecretsRaw({ actorId: req.permission.id,
actorId: req.permission.id, actor: req.permission.type,
actor: req.permission.type, actorOrgId: req.permission.orgId,
viewSecretValue: req.query.viewSecretValue, environment,
throwOnMissingReadValuePermission: false, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, projectId,
environment, path: secretPath,
actorAuthMethod: req.permission.authMethod, orderBy,
projectId, orderDirection,
path: secretPath, search,
orderBy, limit: remainingLimit,
orderDirection, offset: adjustedOffset,
search, tagSlugs: tags
limit: remainingLimit, });
offset: adjustedOffset,
tagSlugs: tags secrets = secretsRaw.secrets;
})
).secrets;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
@@ -689,10 +696,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SanitizedTagSchema.array().optional() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional() .optional()
@@ -736,7 +749,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
const secrets = await server.services.secret.getSecretsRawByFolderMappings( const secrets = await server.services.secret.getSecretsRawByFolderMappings(
{ {
filterByAction: ProjectPermissionSecretActions.DescribeSecret,
projectId, projectId,
folderMappings, folderMappings,
filters: { filters: {
@@ -850,17 +862,22 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(), projectId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
keys: z.string().trim().transform(decodeURIComponent), keys: z.string().trim().transform(decodeURIComponent)
viewSecretValue: booleanSchema.default(false)
}), }),
response: { response: {
200: z.object({ 200: z.object({
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SanitizedTagSchema.array().optional() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional() .optional()
@@ -869,7 +886,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, viewSecretValue } = req.query; const { secretPath, projectId, environment } = 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" });
@@ -878,7 +895,6 @@ 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,
@@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
iv: z.string().trim(), iv: z.string().trim(),
tag: z.string().trim(), tag: z.string().trim(),
expiresIn: z.number().nullable(), expiresIn: z.number().nullable(),
permissions: z.enum(["read", "write", "readValue"]).array() permissions: z.enum(["read", "write"]).array()
}), }),
response: { response: {
200: z.object({ 200: z.object({
+91 -57
View File
@@ -1,7 +1,13 @@
import picomatch from "picomatch"; import picomatch from "picomatch";
import { z } from "zod"; import { z } from "zod";
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; import {
SecretApprovalRequestsSchema,
SecretsSchema,
SecretTagsSchema,
SecretType,
ServiceTokenScopes
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -17,7 +23,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; import { secretRawSchema } from "../sanitizedSchemas";
const SecretReferenceNode = z.object({ const SecretReferenceNode = z.object({
key: z.string(), key: z.string(),
@@ -25,14 +31,6 @@ 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({
@@ -77,9 +75,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
tags: SanitizedTagSchema.array() z.object({
}) tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
})
)
}) })
} }
}, },
@@ -133,7 +139,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
tags: SanitizedTagSchema.array() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
}) })
}) })
} }
@@ -235,10 +247,21 @@ 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),
viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), expandSecretReferences: z
expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), .enum(["true", "false"])
recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), .default("false")
include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), .transform((value) => value === "true")
.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)
@@ -251,9 +274,15 @@ 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: SanitizedTagSchema.array().optional() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array(), .array(),
imports: z imports: z
@@ -264,7 +293,6 @@ 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()
@@ -314,7 +342,6 @@ 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,
@@ -349,7 +376,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
} }
return { secrets, imports }; return { secrets, imports };
} }
}); });
@@ -377,15 +403,28 @@ 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),
viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), expandSecretReferences: z
expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), .enum(["true", "false"])
include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) .default("false")
.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: SanitizedTagSchema.array().optional(), id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
secretMetadata: ResourceMetadataSchema.optional() secretMetadata: ResourceMetadataSchema.optional()
}) })
}) })
@@ -417,7 +456,6 @@ 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,
@@ -624,9 +662,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: secretRawSchema.extend({ secret: secretRawSchema
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")
]) ])
@@ -722,9 +758,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: secretRawSchema.extend({ secret: secretRawSchema
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")
]) ])
@@ -746,7 +780,6 @@ 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({
@@ -809,7 +842,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspace: z.string(), workspace: z.string(),
environment: z.string(), environment: z.string(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
tags: SanitizedTagSchema.array() tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
}) })
.array(), .array(),
imports: z imports: z
@@ -905,7 +944,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
type: z.nativeEnum(SecretType).default(SecretType.Shared), type: z.nativeEnum(SecretType).default(SecretType.Shared),
version: z.coerce.number().optional(), version: z.coerce.number().optional(),
include_imports: convertStringBoolean() include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1176,7 +1218,6 @@ 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()
@@ -1346,12 +1387,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
_id: z.string(), z.object({
secretValueHidden: z.boolean(), _id: z.string(),
workspace: z.string(), workspace: z.string(),
environment: z.string() environment: z.string()
}) })
)
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -1663,7 +1705,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array() secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -1778,11 +1820,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }) secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
.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")
]) ])
@@ -2044,7 +2082,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() secrets: secretRawSchema.array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -2166,11 +2204,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: secretRawSchema secrets: secretRawSchema.array()
.extend({
secretValueHidden: z.boolean()
})
.array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
@@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">; folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">;
@@ -68,8 +68,7 @@ const getIntegrationSecretsV2 = async (
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true, hasSecretAccess: () => true
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -2,11 +2,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, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
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";
@@ -96,7 +92,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(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment, environment: sourceEnvironment,
secretPath secretPath
@@ -179,7 +175,7 @@ export const integrationServiceFactory = ({
if (environment || secretPath) { if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: newEnvironment, environment: newEnvironment,
secretPath: newSecretPath secretPath: newSecretPath
@@ -11,11 +11,7 @@ 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 { import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
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";
@@ -751,10 +747,7 @@ export const projectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSub.Secrets
);
const project = await projectDAL.findProjectById(projectId); const project = await projectDAL.findProjectById(projectId);
@@ -3,7 +3,6 @@ import { groupBy, unique } from "@app/lib/fn";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretDALFactory } from "../secret/secret-dal";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretImportDALFactory } from "./secret-import-dal"; import { TSecretImportDALFactory } from "./secret-import-dal";
@@ -33,12 +32,6 @@ type TSecretImportSecretsV2 = {
folderId: string | undefined; folderId: string | undefined;
importFolderId: string; importFolderId: string;
secrets: (TSecretsV2 & { secrets: (TSecretsV2 & {
secretTags: {
slug: string;
name: string;
color?: string | null;
id: string;
}[];
workspace: string; workspace: string;
environment: string; environment: string;
_id: string; _id: string;
@@ -46,7 +39,6 @@ type TSecretImportSecretsV2 = {
// akhilmhdh: yes i know you can put ?. // akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things // But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string; secretValue: string;
secretValueHidden: boolean;
secretComment: string; secretComment: string;
secretMetadata?: ResourceMetadataDTO; secretMetadata?: ResourceMetadataDTO;
})[]; })[];
@@ -158,14 +150,12 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL, secretImportDAL,
decryptor, decryptor,
expandSecretReferences, expandSecretReferences,
hasSecretAccess, hasSecretAccess
viewSecretValue
}: { }: {
secretImports: (Omit<TSecretImports, "importEnv"> & { secretImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string }; importEnv: { id: string; slug: string; name: string };
})[]; })[];
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
viewSecretValue: boolean;
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">; secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
decryptor: (value?: Buffer | null) => string; decryptor: (value?: Buffer | null) => string;
@@ -178,14 +168,9 @@ export const fnSecretsV2FromImports = async ({
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => { }) => {
const cyclicDetector = new Set(); const cyclicDetector = new Set();
const stack: { const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [
secretImports: typeof rootSecretImports; { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }
depth: number; ];
parentImportedSecrets: (TSecretsV2 & {
secretValueHidden: boolean;
secretTags: { slug: string; name: string; id: string; color?: string | null }[];
})[];
}[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }];
const processedImports: TSecretImportSecretsV2[] = []; const processedImports: TSecretImportSecretsV2[] = [];
@@ -244,9 +229,7 @@ export const fnSecretsV2FromImports = async ({
.map((item) => ({ .map((item) => ({
...item, ...item,
secretKey: item.key, secretKey: item.key,
secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK, secretValue: decryptor(item.encryptedValue),
secretValueHidden: !viewSecretValue,
secretTags: item.tags,
secretComment: decryptor(item.encryptedComment), secretComment: decryptor(item.encryptedComment),
environment: importEnv.slug, environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
@@ -284,8 +267,6 @@ export const fnSecretsV2FromImports = async ({
processedImport.secrets = unique(processedImport.secrets, (i) => i.key); processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
return Promise.allSettled( return Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => { processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecretReferences({ const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath, secretPath: processedImport.secretPath,
@@ -5,11 +5,7 @@ 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 { import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
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";
@@ -94,7 +90,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(
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: data.environment, environment: data.environment,
secretPath: data.path secretPath: data.path
@@ -406,7 +402,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(
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: secretImportDoc.importEnv.slug, environment: secretImportDoc.importEnv.slug,
secretPath: secretImportDoc.importPath secretPath: secretImportDoc.importPath
@@ -600,7 +596,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(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug, environment: el.importEnv.slug,
secretPath: el.importPath secretPath: el.importPath
@@ -646,13 +642,12 @@ export const secretImportServiceFactory = ({
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
secretImports, secretImports,
folderDAL, folderDAL,
viewSecretValue: true,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can( permission.can(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment, environment: expandEnvironment,
secretPath: expandSecretPath, secretPath: expandSecretPath,
@@ -672,7 +667,7 @@ export const secretImportServiceFactory = ({
const allowedImports = secretImports.filter((el) => const allowedImports = secretImports.filter((el) =>
permission.can( permission.can(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug, environment: el.importEnv.slug,
secretPath: el.importPath secretPath: el.importPath
@@ -688,10 +683,7 @@ export const secretImportServiceFactory = ({
return importedSecrets.map((el) => ({ return importedSecrets.map((el) => ({
...el, ...el,
secrets: el.secrets.map((encryptedSecret) => secrets: el.secrets.map((encryptedSecret) =>
decryptSecretRaw( decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey)
{ ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false },
botKey
)
) )
})); }));
}; };
@@ -249,8 +249,7 @@ export const secretSyncQueueFactory = ({
expandSecretReferences, expandSecretReferences,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true, hasSecretAccess: () => true
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -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 {
ProjectPermissionSecretActions, ProjectPermissionActions,
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(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
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(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: updatedEnvironment, environment: updatedEnvironment,
secretPath: updatedSecretPath secretPath: updatedSecretPath
@@ -47,7 +47,6 @@ 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,
@@ -7,7 +7,6 @@ import { logger } from "@app/lib/logger";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@@ -103,7 +102,6 @@ export const fnSecretBulkInsert = async ({
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
})) }))
); );
const secretVersions = await secretVersionDAL.insertMany( const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ sanitizedInputSecrets.map((el) => ({
...el, ...el,
@@ -139,7 +137,6 @@ 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
@@ -148,16 +145,7 @@ export const fnSecretBulkInsert = async ({
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
} }
const secretsWithTags = await secretDAL.find( return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
}; };
export const fnSecretBulkUpdate = async ({ export const fnSecretBulkUpdate = async ({
@@ -295,15 +283,7 @@ export const fnSecretBulkUpdate = async ({
tx tx
); );
const secretsWithTags = await secretDAL.find( return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
}; };
export const fnSecretBulkDelete = async ({ export const fnSecretBulkDelete = async ({
@@ -536,7 +516,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey); const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.` message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.`
}); });
const cacheKey = getCacheUniqueKey(environment, secretPath); const cacheKey = getCacheUniqueKey(environment, secretPath);
@@ -555,7 +535,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.` message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.`
}); });
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@@ -643,13 +623,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,
@@ -663,15 +643,5 @@ export const reshapeBridgeSecret = (
metadata: secret.metadata, metadata: secret.metadata,
secretMetadata: secret.secretMetadata, secretMetadata: secret.secretMetadata,
createdAt: secret.createdAt, createdAt: secret.createdAt,
updatedAt: secret.updatedAt, updatedAt: secret.updatedAt
...(secretValueHidden
? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: true
}
: {
secretValue: secret.value || "",
secretValueHidden: false
})
}); });
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,6 @@
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";
@@ -37,8 +36,6 @@ export type TGetSecretsDTO = {
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
metadataFilter?: { metadataFilter?: {
key?: string; key?: string;
value?: string; value?: string;
@@ -51,11 +48,6 @@ export type TGetSecretsDTO = {
keys?: string[]; keys?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetSecretsMissingReadValuePermissionDTO = Omit<
TGetSecretsDTO,
"viewSecretValue" | "recursive" | "expandSecretReferences"
>;
export type TGetASecretDTO = { export type TGetASecretDTO = {
secretName: string; secretName: string;
path: string; path: string;
@@ -65,7 +57,6 @@ 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 & {
@@ -173,9 +164,9 @@ export type TFnSecretBulkInsert = {
} }
>; >;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "find">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
}; };
@@ -197,9 +188,9 @@ export type TFnSecretBulkUpdate = {
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
}[]; }[];
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences" | "find">; secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
tx?: Knex; tx?: Knex;
}; };
@@ -341,5 +332,4 @@ 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;
}; };
@@ -1,9 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -12,58 +12,6 @@ export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2Bridge
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersionsV2> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersionV2)
.where(`${TableName.SecretVersionV2}.secretId`, secretId)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder // This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try { try {
@@ -176,7 +124,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
pruneExcessVersions, pruneExcessVersions,
findLatestVersionMany, findLatestVersionMany,
bulkUpdate, bulkUpdate,
findLatestVersionByFolderId, findLatestVersionByFolderId
findBySecretId
}; };
}; };
+1 -44
View File
@@ -169,48 +169,6 @@ export const secretDALFactory = (db: TDbClient) => {
} }
}; };
const findManySecretsWithTags = async (
filter: {
secretIds: string[];
type: SecretType;
},
tx?: Knex
) => {
try {
const secrets = await (tx || db.replicaNode())(TableName.Secret)
.whereIn(`${TableName.Secret}.id` as "id", filter.secretIds)
.where("type", filter.type)
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
const data = sqlNestRelationships({
data: secrets,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get many secrets with tags" });
}
};
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
try { try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
@@ -485,7 +443,6 @@ export const secretDALFactory = (db: TDbClient) => {
upsertSecretReferences, upsertSecretReferences,
findReferencedSecretReferences, findReferencedSecretReferences,
findAllProjectSecretValues, findAllProjectSecretValues,
pruneSecretReminders, pruneSecretReminders
findManySecretsWithTags
}; };
}; };
+8 -34
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 { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, 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,
@@ -51,8 +51,6 @@ import {
TUpdateManySecretsRawFnFactory TUpdateManySecretsRawFnFactory
} from "./secret-types"; } from "./secret-types";
export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = "<hidden-by-infisical>";
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
const appCfg = getConfig(); const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({ const secretBlindIndex = await buildSecretBlindIndexFromName({
@@ -192,7 +190,7 @@ export const recursivelyGetSecretPaths = ({
const allowedPaths = paths.filter( const allowedPaths = paths.filter(
(folder) => (folder) =>
permission.can( permission.can(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath: folder.path secretPath: folder.path
@@ -346,7 +344,6 @@ 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;
@@ -365,14 +362,12 @@ export const decryptSecretRaw = (
key key
}); });
const secretValue = !secret.secretValueHidden const secretValue = decryptSymmetric128BitHexKeyUTF8({
? decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretValueCiphertext,
ciphertext: secret.secretValueCiphertext, iv: secret.secretValueIV,
iv: secret.secretValueIV, tag: secret.secretValueTag,
tag: secret.secretValueTag, key
key });
})
: INFISICAL_SECRET_VALUE_HIDDEN_MASK;
let secretComment = ""; let secretComment = "";
@@ -390,7 +385,6 @@ 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,
@@ -1203,23 +1197,3 @@ export const fnDeleteProjectSecretReminders = async (
} }
} }
}; };
export const conditionallyHideSecretValue = (
shouldHideValue: boolean,
{
secretValueCiphertext,
secretValueIV,
secretValueTag
}: {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
) => {
return {
secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext,
secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV,
secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag,
secretValueHidden: shouldHideValue
};
};
+1 -2
View File
@@ -402,8 +402,7 @@ export const secretQueueFactory = ({
expandSecretReferences, expandSecretReferences,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true, hasSecretAccess: () => true
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
+77 -201
View File
@@ -6,7 +6,6 @@ import {
ActionProjectType, ActionProjectType,
ProjectMembershipRole, ProjectMembershipRole,
ProjectUpgradeStatus, ProjectUpgradeStatus,
ProjectVersion,
SecretEncryptionAlgo, SecretEncryptionAlgo,
SecretKeyEncoding, SecretKeyEncoding,
SecretsSchema, SecretsSchema,
@@ -14,11 +13,7 @@ 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 { import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
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";
@@ -53,7 +48,6 @@ 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,
@@ -101,7 +95,7 @@ type TSecretServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds" "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
>; >;
secretV2BridgeService: TSecretV2BridgeServiceFactory; secretV2BridgeService: TSecretV2BridgeServiceFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
@@ -210,7 +204,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -328,7 +322,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -450,22 +444,7 @@ 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 ({
@@ -488,7 +467,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -561,19 +540,7 @@ export const secretServiceFactory = ({
}); });
} }
const secretValueHidden = !permission.can( return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
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 ({
@@ -622,7 +589,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(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -647,7 +614,7 @@ export const secretServiceFactory = ({
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : permission.can(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug, environment: importEnv.slug,
secretPath: importPath secretPath: importPath
@@ -704,7 +671,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
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);
@@ -754,7 +721,7 @@ export const secretServiceFactory = ({
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : permission.can(
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug, environment: importEnv.slug,
secretPath: importPath secretPath: importPath
@@ -772,7 +739,6 @@ 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
@@ -783,13 +749,7 @@ 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 { return { ...secret, workspace: projectId, environment, secretPath: path };
...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 ({
@@ -811,7 +771,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -899,7 +859,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -941,8 +901,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) =>
const updatedSecrets = await fnSecretBulkUpdate({ fnSecretBulkUpdate({
folderId, folderId,
projectId, projectId,
tx, tx,
@@ -972,18 +932,8 @@ export const secretServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL
}); })
);
const secretValueHidden = !permission.can(
ProjectPermissionSecretActions.ReadValue,
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({
@@ -1017,7 +967,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -1069,15 +1019,7 @@ export const secretServiceFactory = ({
} }
} }
const secretValueHidden = !permission.can( return secrets;
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
return secrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
}); });
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
@@ -1238,7 +1180,6 @@ export const secretServiceFactory = ({
secretName, secretName,
path: secretPath, path: secretPath,
environment, environment,
viewSecretValue: false,
type: "shared" type: "shared"
}); });
@@ -1253,11 +1194,10 @@ export const secretServiceFactory = ({
| (typeof groupPermissions)[number] | (typeof groupPermissions)[number]
) => { ) => {
const allowedActions = [ const allowedActions = [
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
ProjectPermissionSecretActions.ReadValue, ProjectPermissionActions.Delete,
ProjectPermissionSecretActions.Delete, ProjectPermissionActions.Create,
ProjectPermissionSecretActions.Create, ProjectPermissionActions.Edit
ProjectPermissionSecretActions.Edit
].filter((action) => ].filter((action) =>
entityPermission.permission.can( entityPermission.permission.can(
action, action,
@@ -1294,13 +1234,11 @@ export const secretServiceFactory = ({
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
viewSecretValue,
environment, environment,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
recursive, recursive,
tagSlugs = [], tagSlugs = [],
throwOnMissingReadValuePermission = true,
...paramsV2 ...paramsV2
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@@ -1311,8 +1249,6 @@ export const secretServiceFactory = ({
actorId, actorId,
actor, actor,
actorOrgId, actorOrgId,
viewSecretValue,
throwOnMissingReadValuePermission,
environment, environment,
path, path,
recursive, recursive,
@@ -1321,7 +1257,6 @@ export const secretServiceFactory = ({
tagSlugs, tagSlugs,
...paramsV2 ...paramsV2
}); });
return { secrets, imports }; return { secrets, imports };
} }
@@ -1350,20 +1285,14 @@ export const secretServiceFactory = ({
recursive recursive
}); });
const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey)); const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, 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
) )
); );
@@ -1374,7 +1303,6 @@ export const secretServiceFactory = ({
const importedEntries = decryptedImportSecrets.reduce( const importedEntries = decryptedImportSecrets.reduce(
( (
accum: { accum: {
secretValueHidden: boolean;
secretKey: string; secretKey: string;
secretPath: string; secretPath: string;
workspace: string; workspace: string;
@@ -1418,7 +1346,6 @@ export const secretServiceFactory = ({
Object.keys(secretsGroupByPath).map((groupedPath) => Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled( Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({ const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: groupedPath, secretPath: groupedPath,
@@ -1435,7 +1362,6 @@ export const secretServiceFactory = ({
processedImports.map((processedImport) => processedImports.map((processedImport) =>
Promise.allSettled( Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => { processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({ const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: path, secretPath: path,
@@ -1461,7 +1387,6 @@ export const secretServiceFactory = ({
path, path,
actor, actor,
environment, environment,
viewSecretValue,
projectId: workspaceId, projectId: workspaceId,
expandSecretReferences, expandSecretReferences,
projectSlug, projectSlug,
@@ -1481,7 +1406,6 @@ export const secretServiceFactory = ({
includeImports, includeImports,
actorAuthMethod, actorAuthMethod,
path, path,
viewSecretValue,
actorOrgId, actorOrgId,
actor, actor,
actorId, actorId,
@@ -1512,7 +1436,6 @@ 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) {
@@ -1531,10 +1454,7 @@ export const secretServiceFactory = ({
decryptedSecret.secretValue = expandedSecretValue || ""; decryptedSecret.secretValue = expandedSecretValue || "";
} }
return { return { secretMetadata: undefined, ...decryptedSecret };
secretMetadata: undefined,
...decryptedSecret
};
}; };
const createSecretRaw = async ({ const createSecretRaw = async ({
@@ -1685,16 +1605,7 @@ export const secretServiceFactory = ({
tags: tagIds tags: tagIds
}); });
return { return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) };
type: SecretProtectionType.Direct as const,
secret: decryptSecretRaw(
{
...secret,
secretValueHidden: false
},
botKey
)
};
}; };
const updateSecretRaw = async ({ const updateSecretRaw = async ({
@@ -2090,7 +2001,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, secretValueHidden: false }, botKey) decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey)
) )
}; };
}; };
@@ -2379,12 +2290,6 @@ export const secretServiceFactory = ({
const folder = await folderDAL.findById(secret.folderId); const folder = await folderDAL.findById(secret.folderId);
if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` });
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
if (!folderWithPath) {
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
}
const { botKey } = await projectBotService.getBotKey(folder.projectId); const { botKey } = await projectBotService.getBotKey(folder.projectId);
if (!botKey) if (!botKey)
throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` });
@@ -2398,42 +2303,18 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const secretVersions = await secretVersionDAL.findBySecretId(secretId, { const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
offset, return secretVersions.map((el) =>
limit, decryptSecretRaw(
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
});
const secretValueHidden = permission.cannot(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: folder.environment.envSlug,
secretPath: folderWithPath.path,
secretName: secretKey,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
})
})
);
return decryptSecretRaw(
{ {
secretValueHidden,
...el, ...el,
workspace: folder.projectId, workspace: folder.projectId,
environment: folder.environment.envSlug, environment: folder.environment.envSlug,
secretPath: folderWithPath.path secretPath: "/"
}, },
botKey botKey
); )
}); );
}; };
const attachTags = async ({ const attachTags = async ({
@@ -2459,7 +2340,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2565,7 +2446,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2731,7 +2612,7 @@ export const secretServiceFactory = ({
message: `Project with slug '${projectSlug}' not found` message: `Project with slug '${projectSlug}' not found`
}); });
} }
if (project.version === ProjectVersion.V3) { if (project.version === 3) {
return secretV2BridgeService.moveSecrets({ return secretV2BridgeService.moveSecrets({
sourceEnvironment, sourceEnvironment,
sourceSecretPath, sourceSecretPath,
@@ -2756,6 +2637,30 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
const { botKey } = await projectBotService.getBotKey(project.id); const { botKey } = await projectBotService.getBotKey(project.id);
if (!botKey) { if (!botKey) {
throw new NotFoundError({ throw new NotFoundError({
@@ -2783,9 +2688,11 @@ export const secretServiceFactory = ({
}); });
} }
const sourceSecrets = await secretDAL.findManySecretsWithTags({ const sourceSecrets = await secretDAL.find({
type: SecretType.Shared, type: SecretType.Shared,
secretIds $in: {
id: secretIds
}
}); });
if (sourceSecrets.length !== secretIds.length) { if (sourceSecrets.length !== secretIds.length) {
@@ -2794,52 +2701,21 @@ export const secretServiceFactory = ({
}); });
} }
const sourceActions = [ const decryptedSourceSecrets = sourceSecrets.map((secret) => ({
ProjectPermissionSecretActions.Delete, ...secret,
ProjectPermissionSecretActions.DescribeSecret, secretKey: decryptSymmetric128BitHexKeyUTF8({
ProjectPermissionSecretActions.ReadValue
] as const;
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
const decryptedSourceSecrets = sourceSecrets.map((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
tag: secret.secretKeyTag, tag: secret.secretKeyTag,
key: botKey key: botKey
}); }),
secretValue: decryptSymmetric128BitHexKeyUTF8({
for (const destinationAction of destinationActions) { ciphertext: secret.secretValueCiphertext,
ForbiddenError.from(permission).throwUnlessCan( iv: secret.secretValueIV,
destinationAction, tag: secret.secretValueTag,
subject(ProjectPermissionSub.Secrets, { key: botKey
environment: destinationEnvironment, })
secretPath: destinationSecretPath }));
})
);
}
for (const sourceAction of sourceActions) {
ForbiddenError.from(permission).throwUnlessCan(
sourceAction,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
}
return {
...secret,
secretKey,
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: botKey
})
};
});
let isSourceUpdated = false; let isSourceUpdated = false;
let isDestinationUpdated = false; let isDestinationUpdated = false;
+2 -5
View File
@@ -180,8 +180,6 @@ export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
path: string; path: string;
environment: string; environment: string;
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
@@ -207,7 +205,6 @@ 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;
@@ -412,7 +409,7 @@ export type TCreateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -449,7 +446,7 @@ export type TUpdateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -1,9 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -12,50 +12,6 @@ export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory
export const secretVersionDALFactory = (db: TDbClient) => { export const secretVersionDALFactory = (db: TDbClient) => {
const secretVersionOrm = ormify(db, TableName.SecretVersion); const secretVersionOrm = ormify(db, TableName.SecretVersion);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersions> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersion)
.where(`${TableName.SecretVersion}.secretId`, secretId)
.leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`)
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.select(selectAllTableCols(TableName.SecretVersion))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder // This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try { try {
@@ -193,7 +149,6 @@ export const secretVersionDALFactory = (db: TDbClient) => {
findLatestVersionMany, findLatestVersionMany,
bulkUpdate, bulkUpdate,
findLatestVersionByFolderId, findLatestVersionByFolderId,
findBySecretId,
bulkUpdateNoVersionIncrement bulkUpdateNoVersionIncrement
}; };
}; };
@@ -5,11 +5,7 @@ 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 { import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
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";
@@ -71,7 +67,7 @@ export const serviceTokenServiceFactory = ({
scopes.forEach(({ environment, secretPath }) => { scopes.forEach(({ environment, secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
}); });
@@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = {
iv: string; iv: string;
tag: string; tag: string;
expiresIn?: number | null; expiresIn?: number | null;
permissions: ("read" | "write" | "readValue")[]; permissions: ("read" | "write")[];
} & TProjectPermission; } & TProjectPermission;
export type TGetServiceTokenInfoDTO = Omit<TProjectPermission, "projectId">; export type TGetServiceTokenInfoDTO = Omit<TProjectPermission, "projectId">;
@@ -1,21 +1,13 @@
/* eslint-disable no-nested-ternary */ import { useState } from "react";
import { useEffect, useState } from "react"; import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
import {
faChevronRight,
faExclamationTriangle,
faEye,
faEyeSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { AxiosError } from "axios";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useGetSecretReferenceTree } from "@app/hooks/api"; import { useGetSecretReferenceTree } from "@app/hooks/api";
import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types"; import { TSecretReferenceTraceNode } from "@app/hooks/api/types";
import style from "./SecretReferenceDetails.module.css"; import style from "./SecretReferenceDetails.module.css";
@@ -92,7 +84,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
const { data, isPending, isError, error } = useGetSecretReferenceTree({ const { data, isPending } = useGetSecretReferenceTree({
secretPath, secretPath,
environmentSlug: environment, environmentSlug: environment,
projectId, projectId,
@@ -102,26 +94,6 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const tree = data?.tree; const tree = data?.tree;
const secretValue = data?.value; const secretValue = data?.value;
useEffect(() => {
if (error instanceof AxiosError) {
const err = error?.response?.data as TApiErrors;
if (err?.error === ApiErrorTypes.CustomForbiddenError) {
createNotification({
title: "You don't have permission to view reference tree",
text: "You don't have permission to view one or more of the referenced secrets.",
type: "error"
});
return;
}
createNotification({
title: "Error fetching secret reference tree",
text: "Please try again later.",
type: "error"
});
}
}, [error]);
if (isPending) { if (isPending) {
return ( return (
<div className="flex items-center justify-center py-4"> <div className="flex items-center justify-center py-4">
@@ -142,16 +114,11 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
</FormControl> </FormControl>
<FormLabel className="mb-2" label="Reference Tree" /> <FormLabel className="mb-2" label="Reference Tree" />
<div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200"> <div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200">
{isError ? ( {tree && (
<div className="flex items-center justify-center py-4">
<FontAwesomeIcon icon={faExclamationTriangle} className="mr-2 text-red-500" />
<p className="text-red-500">Error fetching secret reference tree</p>
</div>
) : tree ? (
<ul className={style.tree}> <ul className={style.tree}>
<SecretReferenceNode node={tree} isRoot secretKey={secretKey} /> <SecretReferenceNode node={tree} isRoot secretKey={secretKey} />
</ul> </ul>
) : null} )}
</div> </div>
<div className="mt-2 text-sm text-mineshaft-400"> <div className="mt-2 text-sm text-mineshaft-400">
Click a secret key to view its sub-references. Click a secret key to view its sub-references.
-22
View File
@@ -1,22 +0,0 @@
import { twMerge } from "tailwind-merge";
import { Tooltip } from "../Tooltip/Tooltip";
interface IProps {
className?: string;
tooltipText?: string;
}
export const Blur = ({ className, tooltipText }: IProps) => {
return (
<Tooltip content={tooltipText} isDisabled={!tooltipText}>
<div
className={twMerge("flex w-80 flex-grow items-center py-1 pl-4 pr-2", className)}
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
</Tooltip>
);
};
@@ -1 +0,0 @@
export { Blur } from "./Blur";
@@ -120,7 +120,6 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused; const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
const { data: secrets } = useGetProjectSecrets({ const { data: secrets } = useGetProjectSecrets({
viewSecretValue: false,
environment: suggestionSource.environment || "", environment: suggestionSource.environment || "",
secretPath: suggestionSource.secretPath || "", secretPath: suggestionSource.secretPath || "",
workspaceId, workspaceId,
@@ -7,14 +7,6 @@ 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",
@@ -146,7 +138,7 @@ export type SecretImportSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretActions, ProjectPermissionActions,
( (
| ProjectPermissionSub.Secrets | ProjectPermissionSub.Secrets
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
@@ -207,7 +207,6 @@ export const useGetProjectSecretsDetails = (
search = "", search = "",
includeSecrets, includeSecrets,
includeFolders, includeFolders,
viewSecretValue,
includeImports, includeImports,
includeDynamicSecrets, includeDynamicSecrets,
tags tags
@@ -232,7 +231,6 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -249,7 +247,6 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -69,7 +69,6 @@ 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>;
@@ -79,7 +79,6 @@ export const decryptSecrets = (
id: encSecret.id, id: encSecret.id,
env: encSecret.environment, env: encSecret.environment,
key: secretKey, key: secretKey,
secretValueHidden: encSecret.secretValueHidden,
value: secretValue, value: secretValue,
tags: encSecret.tags, tags: encSecret.tags,
comment: secretComment, comment: secretComment,
@@ -137,7 +137,6 @@ export const useGetImportedSecretsSingleEnv = ({
env: encSecret.environment, env: encSecret.environment,
key: encSecret.secretKey, key: encSecret.secretKey,
value: encSecret.secretValue, value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags, tags: encSecret.tags,
comment: encSecret.secretComment, comment: encSecret.secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
@@ -177,7 +176,6 @@ export const useGetImportedSecretsAllEnvs = ({
env: encSecret.environment, env: encSecret.environment,
key: encSecret.secretKey, key: encSecret.secretKey,
value: encSecret.secretValue, value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags, tags: encSecret.tags,
comment: encSecret.secretComment, comment: encSecret.secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
@@ -75,7 +75,6 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
id: secretVersion.secretId, id: secretVersion.secretId,
env: data.environment.slug, env: data.environment.slug,
key: secretVersion.secretKey, key: secretVersion.secretKey,
secretValueHidden: secretVersion.secretValueHidden,
value: secretVersion.secretValue || "", value: secretVersion.secretValue || "",
tags: secretVersion.tags, tags: secretVersion.tags,
comment: secretVersion.secretComment, comment: secretVersion.secretComment,
+5 -19
View File
@@ -26,13 +26,8 @@ import {
export const secretKeys = { export const secretKeys = {
// this is also used in secretSnapshot part // this is also used in secretSnapshot part
getProjectSecret: ({ getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
workspaceId, [{ workspaceId, environment, secretPath }, "secrets"] as const,
environment,
secretPath,
viewSecretValue
}: TGetProjectSecretsKey) =>
[{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const,
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
getSecretAccessList: ({ getSecretAccessList: ({
workspaceId, workspaceId,
@@ -49,15 +44,13 @@ export const fetchProjectSecrets = async ({
environment, environment,
secretPath, secretPath,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences
viewSecretValue
}: TGetProjectSecretsKey) => { }: TGetProjectSecretsKey) => {
const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", { const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", {
params: { params: {
environment, environment,
workspaceId, workspaceId,
secretPath, secretPath,
viewSecretValue,
expandSecretReferences, expandSecretReferences,
include_imports: includeImports include_imports: includeImports
} }
@@ -75,7 +68,6 @@ 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,
@@ -115,7 +107,6 @@ export const useGetProjectSecrets = ({
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath,
viewSecretValue,
options options
}: TGetProjectSecretsDTO & { }: TGetProjectSecretsDTO & {
options?: Omit< options?: Omit<
@@ -132,13 +123,8 @@ export const useGetProjectSecrets = ({
...options, ...options,
// wait for all values to be available // wait for all values to be available
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
queryKey: secretKeys.getProjectSecret({ queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
workspaceId, queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
environment,
secretPath,
viewSecretValue
}),
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }),
select: useCallback( select: useCallback(
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets), (data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
[] []
-5
View File
@@ -19,7 +19,6 @@ export type EncryptedSecret = {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHidden: boolean;
__v: number; __v: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -38,7 +37,6 @@ 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;
@@ -63,7 +61,6 @@ 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;
@@ -98,7 +95,6 @@ export type SecretVersions = {
envId: string; envId: string;
secretKey: string; secretKey: string;
secretValue?: string; secretValue?: string;
secretValueHidden: boolean;
secretComment?: string; secretComment?: string;
tags: WsTag[]; tags: WsTag[];
__v: number; __v: number;
@@ -113,7 +109,6 @@ export type TGetProjectSecretsKey = {
environment: string; environment: string;
secretPath?: string; secretPath?: string;
includeImports?: boolean; includeImports?: boolean;
viewSecretValue?: boolean;
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
}; };
+1 -8
View File
@@ -46,8 +46,7 @@ export enum ApiErrorTypes {
ValidationError = "ValidationFailure", ValidationError = "ValidationFailure",
BadRequestError = "BadRequest", BadRequestError = "BadRequest",
UnauthorizedError = "UnauthorizedError", UnauthorizedError = "UnauthorizedError",
ForbiddenError = "PermissionDenied", ForbiddenError = "PermissionDenied"
CustomForbiddenError = "ForbiddenError"
} }
export type TApiErrors = export type TApiErrors =
@@ -70,12 +69,6 @@ export type TApiErrors =
details: PureAbility["rules"]; details: PureAbility["rules"];
statusCode: 403; statusCode: 403;
} }
| {
reqId: string;
error: ApiErrorTypes.CustomForbiddenError;
message: string;
statusCode: 403;
}
| { | {
reqId: string; reqId: string;
statusCode: 400; statusCode: 400;
@@ -58,7 +58,6 @@ const schema = z.object({
permissions: z permissions: z
.object({ .object({
read: z.boolean(), read: z.boolean(),
readValue: z.boolean(),
write: z.boolean() write: z.boolean()
}) })
.required() .required()
@@ -297,19 +296,14 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
name="permissions" name="permissions"
defaultValue={{ defaultValue={{
read: true, read: true,
readValue: false,
write: false write: false
}} }}
render={({ field: { onChange, value }, fieldState: { error } }) => { render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [ const options = [
{ {
label: "Describe Secret (default)", label: "Read (default)",
value: "read" value: "read"
}, },
{
label: "Read Value (optional)",
value: "readValue"
},
{ {
label: "Write (optional)", label: "Write (optional)",
value: "write" value: "write"
@@ -117,27 +117,24 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
<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 (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 }) => (
return ( <div className="flex items-center justify-center">
<div className="flex items-center justify-center"> <Checkbox
<Checkbox isDisabled={isDisabled}
isDisabled={isDisabled} isChecked={Boolean(field.value)}
isChecked={Boolean(field.value)} onCheckedChange={field.onChange}
onCheckedChange={field.onChange} id={`permissions.${subject}.${rootIndex}.${String(value)}`}
id={`permissions.${subject}.${rootIndex}.${String(value)}`} >
> {label}
{label} </Checkbox>
</Checkbox> </div>
</div> )}
);
}}
/> />
); );
})} })}
@@ -9,7 +9,6 @@ import {
PermissionConditionOperators, PermissionConditionOperators,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
TPermissionCondition, TPermissionCondition,
TPermissionConditionOperators TPermissionConditionOperators
@@ -23,14 +22,6 @@ 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(),
@@ -123,7 +114,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]: SecretPolicyActionSchema.extend({ [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(), inverted: z.boolean().optional(),
conditions: ConditionSchema conditions: ConditionSchema
}) })
@@ -292,28 +283,6 @@ 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);
@@ -514,9 +483,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Secrets]: { [ProjectPermissionSub.Secrets]: {
title: "Secrets", title: "Secrets",
actions: [ actions: [
{ label: "Describe Secret", value: "read" }, { label: "Read", 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" }
] ]
@@ -19,7 +19,6 @@ import {
import { getKeyValue } from "@app/helpers/parseEnvVar"; import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api"; import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
import { SecretType } from "@app/hooks/api/types"; import { SecretType } from "@app/hooks/api/types";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
const typeSchema = z const typeSchema = z
.object({ .object({
@@ -276,7 +275,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
isMulti isMulti
options={environments.filter((environment) => options={environments.filter((environment) =>
permission.can( permission.can(
ProjectPermissionSecretActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: environment.slug, environment: environment.slug,
secretPath, secretPath,
@@ -25,7 +25,6 @@ import {
ModalTrigger, ModalTrigger,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
@@ -40,7 +39,6 @@ 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: (
@@ -60,7 +58,6 @@ export const SecretEditRow = ({
isImportedSecret, isImportedSecret,
onSecretUpdate, onSecretUpdate,
secretName, secretName,
secretValueHidden,
onSecretCreate, onSecretCreate,
onSecretDelete, onSecretDelete,
environment, environment,
@@ -143,29 +140,24 @@ export const SecretEditRow = ({
/> />
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2"> <div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
{secretValueHidden ? ( <Controller
<Blur tooltipText="You do not have permission to read the value of this secret." /> disabled={isImportedSecret && !defaultValue}
) : ( control={control}
<Controller name="value"
disabled={isImportedSecret && !defaultValue} render={({ field }) => (
control={control} <InfisicalSecretInput
name="value" {...field}
render={({ field }) => ( isReadOnly={isImportedSecret}
<InfisicalSecretInput value={field.value as string}
{...field} key="secret-input"
isReadOnly={isImportedSecret} isVisible={isVisible}
value={field.value as string} secretPath={secretPath}
key="secret-input" environment={environment}
isVisible={isVisible} isImport={isImportedSecret}
secretPath={secretPath} />
environment={environment} )}
isImport={isImportedSecret} />
/>
)}
/>
)}
</div> </div>
<div <div
className={twMerge( className={twMerge(
"flex w-24 justify-center space-x-3 pl-2 transition-all", "flex w-24 justify-center space-x-3 pl-2 transition-all",
@@ -219,7 +211,6 @@ export const SecretEditRow = ({
<div className="opacity-0 group-hover:opacity-100"> <div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Copy Secret"> <Tooltip content="Copy Secret">
<IconButton <IconButton
isDisabled={secretValueHidden}
ariaLabel="copy-value" ariaLabel="copy-value"
onClick={handleCopySecretToClipboard} onClick={handleCopySecretToClipboard}
variant="plain" variant="plain"
@@ -3,7 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Td, Tooltip, Tr } from "@app/components/v2"; import { Td, Tooltip, Tr } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
type Props = { type Props = {
environments: { name: string; slug: string }[]; environments: { name: string; slug: string }[];
@@ -26,7 +25,7 @@ export const SecretNoAccessOverviewTableRow = ({ environments = [], count }: Pro
<div className="text-bunker-300"> <div className="text-bunker-300">
<FontAwesomeIcon className="block" icon={faLock} /> <FontAwesomeIcon className="block" icon={faLock} />
</div> </div>
<Blur /> <div className="blur-sm">NO ACCESS</div>
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
@@ -221,13 +221,10 @@ export const SecretOverviewTableRow = ({
secretPath={secretPath} secretPath={secretPath}
isVisible={isSecretVisible} isVisible={isSecretVisible}
secretName={secretKey} secretName={secretKey}
secretValueHidden={secret?.secretValueHidden || false}
defaultValue={ defaultValue={
secret?.secretValueHidden secret?.valueOverride ||
? "" secret?.value ||
: secret?.valueOverride || importedSecret?.secret?.value
secret?.value ||
importedSecret?.secret?.value
} }
secretId={secret?.id} secretId={secret?.id}
isOverride={Boolean(secret?.valueOverride)} isOverride={Boolean(secret?.valueOverride)}
@@ -10,8 +10,12 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2";
import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; import {
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { useUpdateSecretV3 } from "@app/hooks/api"; import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
@@ -51,8 +55,8 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
secretTags: (secretDetails?.tags || []).map((i) => i.slug) secretTags: (secretDetails?.tags || []).map((i) => i.slug)
}); });
const isSecretInEnvReadOnly = const isSecretInEnvReadOnly =
permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) && permission.can(ProjectPermissionActions.Read, secretPermissionSubject) &&
permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject);
if (isSecretInEnvReadOnly) { if (isSecretInEnvReadOnly) {
return true; return true;
} }
@@ -110,31 +110,21 @@ export const QuickSearchSecretItem = ({
</Badge> </Badge>
)} )}
{isSingleEnv ? ( {isSingleEnv ? (
<Tooltip <IconButton
isDisabled={!groupSecret?.secretValueHidden} size="md"
content={ variant="plain"
groupSecret?.secretValueHidden colorSchema="secondary"
? "You do not have permission to view this secret value" ariaLabel="Copy secret value"
: "" onClick={(e) => {
} e.stopPropagation();
const el = envSlugMap.get(groupSecret.env)?.name;
if (el) {
handleCopy(groupSecret.value!, el);
}
}}
> >
<IconButton <FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
size="md" </IconButton>
isDisabled={groupSecret?.secretValueHidden}
variant="plain"
colorSchema="secondary"
ariaLabel="Copy secret value"
onClick={(e) => {
e.stopPropagation();
const el = envSlugMap.get(groupSecret.env)?.name;
if (el) {
handleCopy(groupSecret.value!, el);
}
}}
>
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
) : ( ) : (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -168,24 +158,14 @@ export const QuickSearchSecretItem = ({
)} )}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Tooltip <IconButton
isDisabled={!groupSecret?.secretValueHidden} size="md"
content={ variant="plain"
groupSecret?.secretValueHidden colorSchema="secondary"
? "You do not have permission to view this secret value" ariaLabel="View secret value"
: ""
}
> >
<IconButton <FontAwesomeIcon icon={faEye} />
size="md" </IconButton>
isDisabled={groupSecret?.secretValueHidden}
variant="plain"
colorSchema="secondary"
ariaLabel="View secret value"
>
<FontAwesomeIcon icon={faEye} />
</IconButton>
</Tooltip>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel>Hover to Reveal...</DropdownMenuLabel> <DropdownMenuLabel>Hover to Reveal...</DropdownMenuLabel>
@@ -11,7 +11,6 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { import {
@@ -59,7 +58,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
// user should have the ability to delete secrets/folders in at least one of the envs // user should have the ability to delete secrets/folders in at least one of the envs
const shouldShowDelete = userAvailableEnvs.some((env) => const shouldShowDelete = userAvailableEnvs.some((env) =>
permission.can( permission.can(
ProjectPermissionSecretActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: env.slug, environment: env.slug,
secretPath, secretPath,
@@ -111,7 +110,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => { (accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
const entry = secretRecord[env.slug]; const entry = secretRecord[env.slug];
const canDeleteSecret = permission.can( const canDeleteSecret = permission.can(
ProjectPermissionSecretActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: env.slug, environment: env.slug,
secretPath, secretPath,
@@ -25,8 +25,7 @@ import {
Spinner, Spinner,
Switch Switch
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionSub, useProjectPermission } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce } from "@app/hooks"; import { useDebounce } from "@app/hooks";
import { useMoveSecrets } from "@app/hooks/api"; import { useMoveSecrets } from "@app/hooks/api";
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard"; import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
@@ -96,7 +95,7 @@ const Content = ({
env.slug, env.slug,
{ {
missingPermissions: permission.cannot( missingPermissions: permission.cannot(
ProjectPermissionSecretActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: env.slug, environment: env.slug,
secretPath: sourceSecretPath, secretPath: sourceSecretPath,
@@ -25,7 +25,6 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { import {
useGetImportedSecretsSingleEnv, useGetImportedSecretsSingleEnv,
@@ -104,16 +103,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(
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
);
const canReadSecretValue = permission.can(
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -186,7 +176,6 @@ const Page = () => {
orderDirection, orderDirection,
includeImports: canReadSecretImports && filter.include.import, includeImports: canReadSecretImports && filter.include.import,
includeFolders: filter.include.folder, includeFolders: filter.include.folder,
viewSecretValue: canReadSecretValue,
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
includeSecrets: canReadSecret && filter.include.secret, includeSecrets: canReadSecret && filter.include.secret,
tags: filter.tags tags: filter.tags
@@ -21,7 +21,6 @@ import {
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AxiosError } from "axios";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -55,7 +54,7 @@ import {
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { SecretType, WsTag } from "@app/hooks/api/types";
import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput";
import { import {
@@ -153,71 +152,51 @@ export const ActionBar = ({
}; };
const handleSecretDownload = async () => { const handleSecretDownload = async () => {
try { const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ workspaceId,
workspaceId, expandSecretReferences: true,
expandSecretReferences: true, includeImports: true,
includeImports: true, environment,
environment, secretPath
secretPath });
}); const secretsPicked = new Set<string>();
const secretsPicked = new Set<string>(); const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; localSecrets.forEach((el) => {
localSecrets.forEach((el) => { secretsPicked.add(el.secretKey);
secretsPicked.add(el.secretKey); secretsToDownload.push({
secretsToDownload.push({ key: el.secretKey,
key: el.secretKey, value: el.secretValue,
value: el.secretValue, comment: el.secretComment
comment: el.secretComment
});
}); });
});
for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
const secret = localImportedSecrets[i].secrets[j]; const secret = localImportedSecrets[i].secrets[j];
if (!secretsPicked.has(secret.secretKey)) { if (!secretsPicked.has(secret.secretKey)) {
secretsToDownload.push({ secretsToDownload.push({
key: secret.secretKey, key: secret.secretKey,
value: secret.secretValue, value: secret.secretValue,
comment: secret.secretComment comment: secret.secretComment
});
}
secretsPicked.add(secret.secretKey);
}
}
const file = secretsToDownload
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
.reduce(
(prev, { key, comment, value }, index) =>
prev +
(comment
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
: `${key}=${value}\n`),
""
);
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, `${environment}.env`);
} catch (err) {
if (err instanceof AxiosError) {
const error = err?.response?.data as TApiErrors;
if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) {
createNotification({
title: "You don't have permission to download secrets",
text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.",
type: "error"
}); });
return;
} }
secretsPicked.add(secret.secretKey);
} }
createNotification({
title: "Failed to download secrets",
text: "Please try again later.",
type: "error"
});
} }
const file = secretsToDownload
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
.reduce(
(prev, { key, comment, value }, index) =>
prev +
(comment
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
: `${key}=${value}\n`),
""
);
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, `${environment}.env`);
}; };
const handleSecretBulkDelete = async () => { const handleSecretBulkDelete = async () => {
@@ -9,14 +9,12 @@ import {
faPlus, faPlus,
faShare, faShare,
faTag, faTag,
faTrash, faTrash
faTriangleExclamation
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { format } from "date-fns"; import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -46,7 +44,6 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api"; import { useGetSecretVersion } from "@app/hooks/api";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
@@ -125,7 +122,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(
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -133,29 +130,16 @@ 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(
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
secretName: secretKey, secretName: secretKey,
secretTags: selectTagSlugs secretTags: selectTagSlugs
}) })
) && ) && cannotEditSecret;
cannotEditSecret &&
cannotReadSecretValue;
const overrideAction = watch("overrideAction"); const overrideAction = watch("overrideAction");
const isOverridden = const isOverridden =
@@ -208,16 +192,9 @@ export const SecretDetailSidebar = ({
await onSaveSecret(secret, { ...secret, ...data }, () => reset()); await onSaveSecret(secret, { ...secret, ...data }, () => reset());
}; };
const handleReminderSubmit = async ( const handleReminderSubmit = async (reminderRepeatDays: number | null | undefined, reminderNote: string | null | undefined) => {
reminderRepeatDays: number | null | undefined, await onSaveSecret(secret, { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, () => { });
reminderNote: string | null | undefined }
) => {
await onSaveSecret(
secret,
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true },
() => {}
);
};
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false); const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
@@ -236,7 +213,7 @@ export const SecretDetailSidebar = ({
if (data) { if (data) {
setValue("reminderRepeatDays", data.days, { shouldDirty: false }); setValue("reminderRepeatDays", data.days, { shouldDirty: false });
setValue("reminderNote", data.note, { shouldDirty: false }); setValue("reminderNote", data.note, { shouldDirty: false });
handleReminderSubmit(data.days, data.note); handleReminderSubmit(data.days, data.note)
} }
}} }}
/> />
@@ -284,63 +261,38 @@ export const SecretDetailSidebar = ({
key="secret-value" key="secret-value"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<div className="flex items-center gap-2"> <FormControl label="Value">
<FormControl <InfisicalSecretInput
className="flex-1" isReadOnly={isReadOnly}
helperText={ environment={environment}
cannotReadSecretValue ? ( secretPath={secretPath}
<div className="flex space-x-2"> key="secret-value"
<FontAwesomeIcon isDisabled={isOverridden || !isAllowed}
icon={faTriangleExclamation} containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
className="mt-0.5 text-yellow-400" {...field}
/> autoFocus={false}
<span> />
The value of this secret is hidden because you do not have the </FormControl>
read secret value permission.
</span>
</div>
) : undefined
}
label="Value"
>
<div className="flex items-center gap-2">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
<Tooltip
content="You don't have permission to view the secret value."
isDisabled={!secret?.secretValueHidden}
>
<Button
isDisabled={secret?.secretValueHidden}
className="px-2 py-[0.43rem] font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share
</Button>
</Tooltip>
</div>
</FormControl>
</div>
)} )}
/> />
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
<div className="ml-1 mt-1.5 flex items-center">
<Button
className="w-full px-2 py-[0.43rem] font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share
</Button>
</div>
</div> </div>
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0"> <div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div className="mb-4 px-4"> <div className="mb-4 px-4">
@@ -666,34 +618,51 @@ export const SecretDetailSidebar = ({
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300"> <div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<div className="mb-2 pl-1">Version History</div> <div className="mb-2 pl-1">Version History</div>
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]"> <div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretVersion?.map( {secretVersion?.map(({ createdAt, secretValue, version, id }) => (
({ createdAt, secretValue, version, id, secretValueHidden }, index) => ( <div className="flex flex-row">
<div key={`secret-version-${index + 1}`} className="flex flex-row"> <div key={id} className="flex w-full flex-col space-y-1">
<div key={id} className="flex w-full flex-col space-y-1"> <div className="flex items-center">
<div className="flex items-center"> <div className="w-10">
<div className="w-10"> <div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300"> v{version}
v{version}
</div>
</div> </div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div> </div>
<div className="flex w-full cursor-default"> <div>{format(new Date(createdAt), "Pp")}</div>
<div className="relative w-10"> </div>
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" /> <div className="flex w-full cursor-default">
<div className="relative w-10">
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
</div>
<div className="flex flex-row">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
Value:
</div> </div>
<div className="flex flex-row"> <div className="group break-all pl-1 font-mono">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70"> <div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
Value: <button
</div> type="button"
<div className="group break-all pl-1 font-mono"> className="select-none"
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline"> onClick={(e) => {
<button navigator.clipboard.writeText(secretValue || "");
type="button" const target = e.currentTarget;
className="select-none" target.style.borderBottom = "1px dashed";
onClick={(e) => { target.style.paddingBottom = "-1px";
if (secretValueHidden) return;
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || ""); navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget; const target = e.currentTarget;
target.style.borderBottom = "1px dashed"; target.style.borderBottom = "1px dashed";
@@ -711,109 +680,72 @@ export const SecretDetailSidebar = ({
popup.remove(); popup.remove();
target.style.borderBottom = "none"; target.style.borderBottom = "none";
}, 3000); }, 3000);
}} }
onKeyDown={(e) => { }}
if (secretValueHidden) return; >
{secretValue}
if (e.key === "Enter" || e.key === " ") { </button>
navigator.clipboard.writeText(secretValue || ""); <button
const target = e.currentTarget; type="button"
target.style.borderBottom = "1px dashed"; className="ml-1 cursor-pointer"
target.style.paddingBottom = "-1px"; onClick={(e) => {
e.stopPropagation();
// Create and insert popup e.currentTarget
const popup = document.createElement("div"); .closest(".group")
popup.className = ?.classList.remove("show-value");
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50"; }}
popup.textContent = "Copied!"; onKeyDown={(e) => {
target.parentElement?.appendChild(popup); if (e.key === "Enter" || e.key === " ") {
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
<Tooltip
className="break-normal text-xs"
content="You do not have permission to view this secret value"
isDisabled={!secretValueHidden}
>
<span
className={twMerge(
secretValueHidden && "text-xs text-bunker-300 opacity-40"
)}
>
{secretValueHidden ? "Hidden" : secretValue}
</span>
</Tooltip>
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.currentTarget e.currentTarget
.closest(".group") .closest(".group")
?.classList.remove("show-value"); ?.classList.remove("show-value");
}} }
onKeyDown={(e) => { }}
if (e.key === "Enter" || e.key === " ") { >
e.stopPropagation(); <FontAwesomeIcon icon={faEyeSlash} />
e.currentTarget </button>
.closest(".group") </div>
?.classList.remove("show-value"); <span className="group-[.show-value]:hidden">
} {secretValue?.replace(/./g, "*")}
}} <button
> type="button"
<FontAwesomeIcon icon={faEyeSlash} /> className="ml-1 cursor-pointer"
</button> onClick={(e) => {
</div> e.currentTarget.closest(".group")?.classList.add("show-value");
<span className="group-[.show-value]:hidden"> }}
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} onKeyDown={(e) => {
<button if (e.key === "Enter" || e.key === " ") {
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget e.currentTarget
.closest(".group") .closest(".group")
?.classList.add("show-value"); ?.classList.add("show-value");
}} }
onKeyDown={(e) => { }}
if (e.key === "Enter" || e.key === " ") { >
e.currentTarget <FontAwesomeIcon icon={faEye} />
.closest(".group") </button>
?.classList.add("show-value"); </span>
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<div
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
>
<Tooltip content="Restore Secret Value">
<IconButton
ariaLabel="Restore"
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() => setValue("value", secretValue)}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
</div>
</div> </div>
) <div
)} className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
>
<Tooltip content="Restore Secret Value">
<IconButton
ariaLabel="Restore"
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() => setValue("value", secretValue)}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
</div>
</div>
))}
</div> </div>
</div> </div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300"> <div className="dark mb-4 flex-grow text-sm text-bunker-300">
@@ -1,4 +1,3 @@
/* 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 {
@@ -45,8 +44,6 @@ import {
SecretReferenceTree SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails"; } from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import { import {
FontAwesomeSpriteName, FontAwesomeSpriteName,
formSchema, formSchema,
@@ -102,14 +99,8 @@ export const SecretItem = memo(
trigger, trigger,
formState: { isDirty, isSubmitting, errors } formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: { defaultValues: secret,
...secret, values: secret,
value: secret.secretValueHidden ? "" : secret.value
},
values: {
...secret,
value: secret.secretValueHidden ? "" : secret.value
},
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
@@ -132,7 +123,7 @@ export const SecretItem = memo(
const isReadOnly = const isReadOnly =
permission.can( permission.can(
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -141,7 +132,7 @@ export const SecretItem = memo(
}) })
) && ) &&
permission.cannot( permission.cannot(
ProjectPermissionSecretActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath, secretPath,
@@ -150,8 +141,6 @@ export const SecretItem = memo(
}) })
); );
const { secretValueHidden } = secret;
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
@@ -283,8 +272,6 @@ export const SecretItem = memo(
/> />
)} )}
/> />
) : secretValueHidden ? (
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : ( ) : (
<Controller <Controller
name="value" name="value"
@@ -298,7 +285,6 @@ export const SecretItem = memo(
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}
{...field} {...field}
defaultValue={secretValueHidden ? "" : undefined}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2" containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/> />
)} )}
@@ -307,7 +293,6 @@ export const SecretItem = memo(
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all"> <div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
<Tooltip content="Copy secret"> <Tooltip content="Copy secret">
<IconButton <IconButton
isDisabled={secret.secretValueHidden}
ariaLabel="copy-value" ariaLabel="copy-value"
variant="plain" variant="plain"
size="sm" size="sm"
@@ -515,7 +500,6 @@ export const SecretItem = memo(
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<IconButton <IconButton
isDisabled={secret.secretValueHidden}
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6" className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
variant="plain" variant="plain"
size="md" size="md"
@@ -1,5 +1,4 @@
import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2"; import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { FontAwesomeSpriteName } from "./SecretListView.utils"; import { FontAwesomeSpriteName } from "./SecretListView.utils";
@@ -35,7 +34,13 @@ export const SecretNoAccessListView = ({ count }: Props) => {
className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent" className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/> />
</div> </div>
<Blur /> <div
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
</div> </div>
</Tooltip> </Tooltip>
))} ))}
@@ -20,7 +20,6 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
@@ -121,25 +120,11 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">Value</Td> <Td className="border-r border-mineshaft-600">Value</Td>
{isModified && ( {isModified && (
<Td className="border-r border-mineshaft-600"> <Td className="border-r border-mineshaft-600">
{preSecret?.secretValueHidden ? ( <SecretInput value={preSecret?.value} />
<Blur
className="w-min"
tooltipText="You do not have permission to read the value of this secret."
/>
) : (
<SecretInput value={preSecret?.value} />
)}
</Td> </Td>
)} )}
<Td> <Td>
{postSecret?.secretValueHidden ? ( <SecretInput value={postSecret?.value} />
<Blur
className="w-min"
tooltipText="You do not have permission to read the value of this secret."
/>
) : (
<SecretInput value={postSecret?.value} />
)}
</Td> </Td>
</Tr> </Tr>
{Boolean(preSecret?.idOverride || postSecret?.idOverride) && ( {Boolean(preSecret?.idOverride || postSecret?.idOverride) && (