mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 20:27:12 +00:00
feat: allow secret approval reviewers to read secrets (#4411)
* feat: allow secret approval reviewers to read secrets * feat: allow secret approval reviewers to read secrets * fix: backfill migrations * lint: fix * revert: license file * Update backend/src/db/migrations/20250824192801_backfill-secret-read-compat-flag.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: rename to `shouldCheckSecretPermission` * lint: fix --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
greptile-apps[bot]
parent
bceaac844f
commit
af2f21fe93
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
t.boolean("shouldCheckSecretPermission").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
t.dropColumn("shouldCheckSecretPermission");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||||
|
// find all existing SecretApprovalPolicy rows to backfill shouldCheckSecretPermission flag
|
||||||
|
const rows = await knex(TableName.SecretApprovalPolicy).select(selectAllTableCols(TableName.SecretApprovalPolicy));
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||||
|
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex(TableName.SecretApprovalPolicy)
|
||||||
|
.whereIn(
|
||||||
|
"id",
|
||||||
|
batch.map((row) => row.id)
|
||||||
|
)
|
||||||
|
.update({ shouldCheckSecretPermission: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {}
|
||||||
@@ -17,7 +17,8 @@ export const SecretApprovalPoliciesSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
enforcementLevel: z.string().default("hard"),
|
enforcementLevel: z.string().default("hard"),
|
||||||
deletedAt: z.date().nullable().optional(),
|
deletedAt: z.date().nullable().optional(),
|
||||||
allowedSelfApprovals: z.boolean().default(true)
|
allowedSelfApprovals: z.boolean().default(true),
|
||||||
|
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
enforcementLevel: z.string(),
|
enforcementLevel: z.string(),
|
||||||
deletedAt: z.date().nullish(),
|
deletedAt: z.date().nullish(),
|
||||||
allowedSelfApprovals: z.boolean()
|
allowedSelfApprovals: z.boolean(),
|
||||||
|
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||||
}),
|
}),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
|
|||||||
@@ -180,7 +180,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
|
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt")
|
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt"),
|
||||||
|
tx
|
||||||
|
.ref("shouldCheckSecretPermission")
|
||||||
|
.withSchema(TableName.SecretApprovalPolicy)
|
||||||
|
.as("policySecretReadAccessCompat")
|
||||||
);
|
);
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
@@ -220,7 +224,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
enforcementLevel: el.policyEnforcementLevel,
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
envId: el.policyEnvId,
|
envId: el.policyEnvId,
|
||||||
deletedAt: el.policyDeletedAt,
|
deletedAt: el.policyDeletedAt,
|
||||||
allowedSelfApprovals: el.policyAllowedSelfApprovals
|
allowedSelfApprovals: el.policyAllowedSelfApprovals,
|
||||||
|
shouldCheckSecretPermission: el.policySecretReadAccessCompat
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
|
|||||||
@@ -281,13 +281,22 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
) {
|
) {
|
||||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||||
}
|
}
|
||||||
const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => {
|
const getHasSecretReadAccess = (
|
||||||
|
shouldCheckSecretPermission: boolean | null | undefined,
|
||||||
|
environment: string,
|
||||||
|
tags: { slug: string }[],
|
||||||
|
secretPath?: string
|
||||||
|
) => {
|
||||||
|
if (shouldCheckSecretPermission) {
|
||||||
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
environment,
|
environment,
|
||||||
secretPath: secretPath || "/",
|
secretPath: secretPath || "/",
|
||||||
secretTags: tags.map((i) => i.slug)
|
secretTags: tags.map((i) => i.slug)
|
||||||
});
|
});
|
||||||
return canRead;
|
return canRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
let secrets;
|
let secrets;
|
||||||
@@ -309,8 +318,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
version: el.version,
|
version: el.version,
|
||||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
),
|
||||||
|
secretValue: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
)
|
||||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||||
: el.secret && el.secret.isRotatedSecret
|
: el.secret && el.secret.isRotatedSecret
|
||||||
? undefined
|
? undefined
|
||||||
@@ -326,11 +345,17 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
id: el.secret.id,
|
id: el.secret.id,
|
||||||
version: el.secret.version,
|
version: el.secret.version,
|
||||||
secretValueHidden: !getHasSecretReadAccess(
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
secretApprovalRequest.environment,
|
secretApprovalRequest.environment,
|
||||||
el.tags,
|
el.tags,
|
||||||
secretPath?.[0]?.path
|
secretPath?.[0]?.path
|
||||||
),
|
),
|
||||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
secretValue: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
)
|
||||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||||
: el.secret.encryptedValue
|
: el.secret.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||||
@@ -346,11 +371,17 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
id: el.secretVersion.id,
|
id: el.secretVersion.id,
|
||||||
version: el.secretVersion.version,
|
version: el.secretVersion.version,
|
||||||
secretValueHidden: !getHasSecretReadAccess(
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
secretApprovalRequest.environment,
|
secretApprovalRequest.environment,
|
||||||
el.tags,
|
el.tags,
|
||||||
secretPath?.[0]?.path
|
secretPath?.[0]?.path
|
||||||
),
|
),
|
||||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
secretValue: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
)
|
||||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||||
: el.secretVersion.encryptedValue
|
: el.secretVersion.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||||
@@ -368,7 +399,12 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||||
secrets = encryptedSecrets.map((el) => ({
|
secrets = encryptedSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
),
|
||||||
...decryptSecretWithBot(el, botKey),
|
...decryptSecretWithBot(el, botKey),
|
||||||
secret: el.secret
|
secret: el.secret
|
||||||
? {
|
? {
|
||||||
|
|||||||
Reference in New Issue
Block a user