Merge pull request #3181 from Infisical/daniel/id-get-secret

feat: get secret by ID
This commit is contained in:
Daniel Hougaard
2025-03-07 19:35:52 +04:00
committed by GitHub
6 changed files with 190 additions and 26 deletions

View File

@@ -380,6 +380,48 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/raw/id/:secretId",
config: {
rateLimit: secretsLimit
},
schema: {
params: z.object({
secretId: z.string()
}),
response: {
200: z.object({
secret: secretRawSchema.extend({
secretPath: z.string(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
secretMetadata: ResourceMetadataSchema.optional()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { secretId } = req.params;
const secret = await server.services.secret.getSecretByIdRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretId
});
return { secret };
}
});
server.route({
method: "GET",
url: "/raw/:secretName",

View File

@@ -613,6 +613,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
@@ -622,12 +625,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
)
.select(db.ref("projectId").withSchema(TableName.Environment).as("projectId"));
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",

View File

@@ -28,6 +28,7 @@ import { KmsDataKey } from "../kms/kms-types";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretQueueFactory } from "../secret/secret-queue";
import { TGetASecretByIdDTO } from "../secret/secret-types";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
@@ -73,7 +74,13 @@ type TSecretV2BridgeServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
| "findBySecretPath"
| "updateById"
| "findById"
| "findByManySecretPath"
| "find"
| "findBySecretPathMultiEnv"
| "findSecretPathByFolderIds"
>;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
@@ -955,6 +962,73 @@ export const secretV2BridgeServiceFactory = ({
};
};
const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => {
const secret = await secretDAL.findOneWithTags({
[`${TableName.SecretV2}.id` as "id"]: secretId
});
if (!secret) {
throw new NotFoundError({
message: `Secret with ID '${secretId}' not found`,
name: "GetSecretById"
});
}
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secret.projectId, [secret.folderId]);
if (!folderWithPath) {
throw new NotFoundError({
message: `Folder with id '${secret.folderId}' not found`,
name: "GetSecretById"
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: secret.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: folderWithPath.environmentSlug,
secretPath: folderWithPath.path,
secretName: secret.key,
secretTags: secret.tags.map((i) => i.slug)
})
);
if (secret.type === SecretType.Personal && secret.userId !== actorId) {
throw new ForbiddenRequestError({
message: "You are not allowed to access this secret",
name: "GetSecretById"
});
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: secret.projectId
});
const secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
const secretComment = secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: "";
return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, {
...secret,
value: secretValue,
comment: secretComment
});
};
const getSecretByName = async ({
actorId,
actor,
@@ -2237,6 +2311,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsCountMultiEnv,
getSecretsMultiEnv,
getSecretReferenceTree,
getSecretsByFolderMappings
getSecretsByFolderMappings,
getSecretById
};
};

View File

@@ -71,6 +71,7 @@ import {
TDeleteManySecretRawDTO,
TDeleteSecretDTO,
TDeleteSecretRawDTO,
TGetASecretByIdRawDTO,
TGetASecretDTO,
TGetASecretRawDTO,
TGetSecretAccessListDTO,
@@ -95,7 +96,7 @@ type TSecretServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds"
>;
secretV2BridgeService: TSecretV2BridgeServiceFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
@@ -1382,6 +1383,18 @@ export const secretServiceFactory = ({
};
};
const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => {
const secret = await secretV2BridgeService.getSecretById({
secretId,
actorId,
actor,
actorOrgId,
actorAuthMethod
});
return secret;
};
const getSecretByNameRaw = async ({
type,
path,
@@ -3088,6 +3101,7 @@ export const secretServiceFactory = ({
getSecretsRawMultiEnv,
getSecretReferenceTree,
getSecretsRawByFolderMappings,
getSecretAccessList
getSecretAccessList,
getSecretByIdRaw
};
};

View File

@@ -121,6 +121,10 @@ export type TGetASecretDTO = {
version?: number;
} & TProjectPermission;
export type TGetASecretByIdDTO = {
secretId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateBulkSecretDTO = {
path: string;
environment: string;
@@ -213,6 +217,10 @@ export type TGetASecretRawDTO = {
projectId?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetASecretByIdRawDTO = {
secretId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateSecretRawDTO = TProjectPermission & {
secretName: string;
secretPath: string;

View File

@@ -5,6 +5,7 @@ import {
faArrowRotateRight,
faCheckCircle,
faClock,
faCopy,
faDesktop,
faEyeSlash,
faPlus,
@@ -990,29 +991,49 @@ export const SecretDetailSidebar = ({
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<div className="flex items-center gap-2">
<Tooltip content="Copy Secret ID">
<IconButton
colorSchema="danger"
ariaLabel="Delete Secret"
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
isDisabled={!isAllowed}
onClick={onDeleteSecret}
variant="outline_bg"
ariaLabel="Copy Secret ID"
onClick={async () => {
await navigator.clipboard.writeText(secret.id);
createNotification({
title: "Secret ID Copied",
text: "The secret ID has been copied to your clipboard.",
type: "success"
});
}}
>
<Tooltip content="Delete Secret">
<FontAwesomeIcon icon={faTrash} />
</Tooltip>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
)}
</ProjectPermissionCan>
</Tooltip>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Tooltip content="Delete Secret">
<IconButton
colorSchema="danger"
variant="outline_bg"
ariaLabel="Delete Secret"
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
isDisabled={!isAllowed}
onClick={onDeleteSecret}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
)}
</ProjectPermissionCan>
</div>
</div>
</div>
</div>