mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-22 13:29:55 +00:00
Compare commits
37 Commits
daniel/api
...
daniel/sho
Author | SHA1 | Date | |
---|---|---|---|
2273c21eb2 | |||
97c2b15e29 | |||
b65842f5c1 | |||
9c33251c44 | |||
1a0896475c | |||
7e820745a4 | |||
fa63c150dd | |||
1a2495a95c | |||
d79099946a | |||
acde0867a0 | |||
d44f99bac2 | |||
2b35e20b1d | |||
da15957c3f | |||
208fc3452d | |||
ba1db870a4 | |||
7885a3b0ff | |||
66485f0464 | |||
0741058c1d | |||
3a6e79c575 | |||
7e11fbe7a3 | |||
a44b3efeb7 | |||
1992a09ac2 | |||
efa54e0c46 | |||
bde2d5e0a6 | |||
4090c894fc | |||
221bde01f8 | |||
b191a3c2f4 | |||
032197ee9f | |||
d5a4eb609a | |||
e7f1980b80 | |||
d430293c66 | |||
cd09f03f0b | |||
bc475e0f08 | |||
afd6dd5257 | |||
3a43d7c5d5 | |||
65375886bd | |||
8495107849 |
@ -3,34 +3,74 @@ import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||
// add column approverGroupId to AccessApprovalPolicyApprover
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||
// make nullable
|
||||
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
// add column approverGroupId to AccessApprovalPolicyApprover
|
||||
if (!hasAccessApproverGroupId) {
|
||||
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
}
|
||||
|
||||
// make approverUserId nullable
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
if (hasAccessApproverUserId) {
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
}
|
||||
});
|
||||
// add column approverGroupId to SecretApprovalPolicyApprover
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||
table.uuid("approverGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
// add column approverGroupId to SecretApprovalPolicyApprover
|
||||
if (!hasSecretApproverGroupId) {
|
||||
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
}
|
||||
|
||||
// make approverUserId nullable
|
||||
if (hasSecretApproverUserId) {
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
"approverGroupId"
|
||||
);
|
||||
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||
// remove
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||
table.dropColumn("approverGroupId");
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
if (hasAccessApproverGroupId) {
|
||||
table.dropColumn("approverGroupId");
|
||||
}
|
||||
// make approverUserId not nullable
|
||||
if (hasAccessApproverUserId) {
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
}
|
||||
});
|
||||
|
||||
// remove
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||
table.dropColumn("approverGroupId");
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
if (hasSecretApproverGroupId) {
|
||||
table.dropColumn("approverGroupId");
|
||||
}
|
||||
// make approverUserId not nullable
|
||||
if (hasSecretApproverUserId) {
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.string("iv").nullable().alter();
|
||||
t.string("tag").nullable().alter();
|
||||
t.string("encryptedValue").nullable().alter();
|
||||
|
||||
t.binary("encryptedSecret").nullable();
|
||||
t.string("hashedHex").nullable().alter();
|
||||
|
||||
t.string("identifier", 64).nullable();
|
||||
t.unique("identifier");
|
||||
t.index("identifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("encryptedSecret");
|
||||
|
||||
t.dropColumn("identifier");
|
||||
});
|
||||
}
|
||||
}
|
@ -5,14 +5,16 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedValue: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
encryptedValue: z.string().nullable().optional(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
hashedHex: z.string().nullable().optional(),
|
||||
expiresAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
@ -22,7 +24,9 @@ export const SecretSharingSchema = z.object({
|
||||
accessType: z.string().default("anyone"),
|
||||
name: z.string().nullable().optional(),
|
||||
lastViewedAt: z.date().nullable().optional(),
|
||||
password: z.string().nullable().optional()
|
||||
password: z.string().nullable().optional(),
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
@ -1,23 +1,21 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TVerifyApprovers, VerifyApproversError } from "./access-approval-policy-types";
|
||||
import { TIsApproversValid } from "./access-approval-policy-types";
|
||||
|
||||
export const verifyApprovers = async ({
|
||||
export const isApproversValid = async ({
|
||||
userIds,
|
||||
projectId,
|
||||
orgId,
|
||||
envSlug,
|
||||
actorAuthMethod,
|
||||
secretPath,
|
||||
permissionService,
|
||||
error
|
||||
}: TVerifyApprovers) => {
|
||||
for await (const userId of userIds) {
|
||||
try {
|
||||
permissionService
|
||||
}: TIsApproversValid) => {
|
||||
try {
|
||||
for await (const userId of userIds) {
|
||||
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
userId,
|
||||
@ -30,17 +28,9 @@ export const verifyApprovers = async ({
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
|
||||
);
|
||||
} catch (err) {
|
||||
if (error === VerifyApproversError.BadRequestError) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more approvers doesn't have access to be specified secret path"
|
||||
});
|
||||
}
|
||||
if (error === VerifyApproversError.ForbiddenError) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "You don't have access to approve this request"
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
@ -11,7 +11,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
||||
import { isApproversValid } from "./access-approval-policy-fns";
|
||||
import {
|
||||
ApproverType,
|
||||
TCreateAccessApprovalPolicy,
|
||||
@ -19,8 +19,7 @@ import {
|
||||
TGetAccessApprovalPolicyByIdDTO,
|
||||
TGetAccessPolicyCountByEnvironmentDTO,
|
||||
TListAccessApprovalPoliciesDTO,
|
||||
TUpdateAccessApprovalPolicy,
|
||||
VerifyApproversError
|
||||
TUpdateAccessApprovalPolicy
|
||||
} from "./access-approval-policy-types";
|
||||
|
||||
type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
@ -133,17 +132,22 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
.map((user) => user.id);
|
||||
verifyAllApprovers.push(...verifyGroupApprovers);
|
||||
|
||||
await verifyApprovers({
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: project.id,
|
||||
orgId: actorOrgId,
|
||||
envSlug: environment,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: verifyAllApprovers,
|
||||
error: VerifyApproversError.BadRequestError
|
||||
userIds: verifyAllApprovers
|
||||
});
|
||||
|
||||
if (!approversValid) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more approvers doesn't have access to be specified secret path"
|
||||
});
|
||||
}
|
||||
|
||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.create(
|
||||
{
|
||||
@ -285,17 +289,22 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
await verifyApprovers({
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: userApproverIds,
|
||||
error: VerifyApproversError.BadRequestError
|
||||
userIds: userApproverIds
|
||||
});
|
||||
|
||||
if (!approversValid) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more approvers doesn't have access to be specified secret path"
|
||||
});
|
||||
}
|
||||
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
userApproverIds.map((userId) => ({
|
||||
approverUserId: userId,
|
||||
@ -325,16 +334,22 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
.filter((user) => user.isPartOfGroup)
|
||||
.map((user) => user.id);
|
||||
|
||||
await verifyApprovers({
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: verifyGroupApprovers,
|
||||
error: VerifyApproversError.BadRequestError
|
||||
userIds: verifyGroupApprovers
|
||||
});
|
||||
|
||||
if (!approversValid) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more approvers doesn't have access to be specified secret path"
|
||||
});
|
||||
}
|
||||
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((groupId) => ({
|
||||
approverGroupId: groupId,
|
||||
@ -398,7 +413,9 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new NotFoundError({ message: "User not found in project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
if (!environment) throw new NotFoundError({ message: "Environment not found" });
|
||||
|
@ -3,12 +3,7 @@ import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
|
||||
export enum VerifyApproversError {
|
||||
ForbiddenError = "ForbiddenError",
|
||||
BadRequestError = "BadRequestError"
|
||||
}
|
||||
|
||||
export type TVerifyApprovers = {
|
||||
export type TIsApproversValid = {
|
||||
userIds: string[];
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
envSlug: string;
|
||||
@ -16,7 +11,6 @@ export type TVerifyApprovers = {
|
||||
secretPath: string;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
error: VerifyApproversError;
|
||||
};
|
||||
|
||||
export enum ApproverType {
|
||||
|
@ -17,8 +17,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
||||
import { VerifyApproversError } from "../access-approval-policy/access-approval-policy-types";
|
||||
import { isApproversValid } from "../access-approval-policy/access-approval-policy-fns";
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
@ -100,7 +99,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
}: TCreateAccessApprovalRequestDTO) => {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new ForbiddenRequestError({ message: "Project not found" });
|
||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||
|
||||
// Anyone can create an access approval request.
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
@ -110,7 +109,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const requestedByUser = await userDAL.findById(actorId);
|
||||
if (!requestedByUser) throw new ForbiddenRequestError({ message: "User not found" });
|
||||
@ -272,7 +273,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new NotFoundError({ message: "You don't have a membership for the specified project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||
@ -308,7 +311,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!membership) throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
@ -320,17 +325,20 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
||||
|
||||
await verifyApprovers({
|
||||
const approversValid = await isApproversValid({
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalRequest.environment,
|
||||
secretPath: accessApprovalRequest.policy.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: [reviewerProjectMembership.userId],
|
||||
error: VerifyApproversError.ForbiddenError
|
||||
userIds: [reviewerProjectMembership.userId]
|
||||
});
|
||||
|
||||
if (!approversValid) {
|
||||
throw new ForbiddenRequestError({ message: "You don't have access to approve this request" });
|
||||
}
|
||||
|
||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
||||
@ -422,7 +430,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new NotFoundError({ message: "You don't have a membership for the specified project" });
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
||||
|
||||
|
@ -116,7 +116,7 @@ export const permissionServiceFactory = ({
|
||||
if (userOrgId && userOrgId !== orgId)
|
||||
throw new ForbiddenRequestError({ message: "Invalid user token. Scoped to different organization." });
|
||||
const membership = await permissionDAL.getOrgPermission(userId, orgId);
|
||||
if (!membership) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||
if (!membership) throw new ForbiddenRequestError({ name: "You are not apart of this organization" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom organization permission not found" });
|
||||
}
|
||||
@ -143,7 +143,7 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
|
||||
const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId);
|
||||
if (!membership) throw new ForbiddenRequestError({ name: "Identity is not a part of the specified organization" });
|
||||
if (!membership) throw new ForbiddenRequestError({ name: "Identity is not apart of this organization" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
throw new NotFoundError({ name: "Custom organization permission not found" });
|
||||
}
|
||||
|
@ -19,28 +19,47 @@ enum JWTErrors {
|
||||
InvalidAlgorithm = "invalid algorithm"
|
||||
}
|
||||
|
||||
enum HttpStatusCodes {
|
||||
BadRequest = 400,
|
||||
NotFound = 404,
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
InternalServerError = 500
|
||||
}
|
||||
|
||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||
server.setErrorHandler((error, req, res) => {
|
||||
req.log.error(error);
|
||||
if (error instanceof BadRequestError) {
|
||||
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name });
|
||||
void res
|
||||
.status(HttpStatusCodes.BadRequest)
|
||||
.send({ statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
|
||||
} else if (error instanceof NotFoundError) {
|
||||
void res.status(404).send({ statusCode: 404, message: error.message, error: error.name });
|
||||
void res
|
||||
.status(HttpStatusCodes.NotFound)
|
||||
.send({ statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
|
||||
} else if (error instanceof UnauthorizedError) {
|
||||
void res.status(401).send({ statusCode: 401, message: error.message, error: error.name });
|
||||
void res
|
||||
.status(HttpStatusCodes.Unauthorized)
|
||||
.send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name });
|
||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
||||
void res.status(500).send({ statusCode: 500, message: "Something went wrong", error: error.name });
|
||||
void res
|
||||
.status(HttpStatusCodes.InternalServerError)
|
||||
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
|
||||
} else if (error instanceof ZodError) {
|
||||
void res.status(401).send({ statusCode: 401, error: "ValidationFailure", message: error.issues });
|
||||
void res
|
||||
.status(HttpStatusCodes.Unauthorized)
|
||||
.send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues });
|
||||
} else if (error instanceof ForbiddenError) {
|
||||
void res.status(403).send({
|
||||
statusCode: 403,
|
||||
void res.status(HttpStatusCodes.Forbidden).send({
|
||||
statusCode: HttpStatusCodes.Forbidden,
|
||||
error: "PermissionDenied",
|
||||
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
||||
});
|
||||
} else if (error instanceof ForbiddenRequestError) {
|
||||
void res.status(403).send({
|
||||
statusCode: 403,
|
||||
void res.status(HttpStatusCodes.Forbidden).send({
|
||||
statusCode: HttpStatusCodes.Forbidden,
|
||||
message: error.message,
|
||||
error: error.name
|
||||
});
|
||||
@ -66,8 +85,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
return error.message;
|
||||
})();
|
||||
|
||||
void res.status(403).send({
|
||||
statusCode: 403,
|
||||
void res.status(HttpStatusCodes.Forbidden).send({
|
||||
statusCode: HttpStatusCodes.Forbidden,
|
||||
error: "TokenError",
|
||||
message
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CronJob } from "cron";
|
||||
import { Redis } from "ioredis";
|
||||
// import { Redis } from "ioredis";
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -74,7 +74,6 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
|
||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||
@ -918,7 +917,8 @@ export const registerRoutes = async (
|
||||
const secretSharingService = secretSharingServiceFactory({
|
||||
permissionService,
|
||||
secretSharingDAL,
|
||||
orgDAL
|
||||
orgDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||
@ -1308,33 +1308,33 @@ export const registerRoutes = async (
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (request, reply) => {
|
||||
handler: async () => {
|
||||
const cfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
try {
|
||||
await db.raw("SELECT NOW()");
|
||||
} catch (err) {
|
||||
logger.error("Health check: database connection failed", err);
|
||||
return reply.code(503).send({
|
||||
date: new Date(),
|
||||
message: "Service unavailable"
|
||||
});
|
||||
}
|
||||
// try {
|
||||
// await db.raw("SELECT NOW()");
|
||||
// } catch (err) {
|
||||
// logger.error("Health check: database connection failed", err);
|
||||
// return reply.code(503).send({
|
||||
// date: new Date(),
|
||||
// message: "Service unavailable"
|
||||
// });
|
||||
// }
|
||||
|
||||
if (cfg.isRedisConfigured) {
|
||||
const redis = new Redis(cfg.REDIS_URL);
|
||||
try {
|
||||
await redis.ping();
|
||||
redis.disconnect();
|
||||
} catch (err) {
|
||||
logger.error("Health check: redis connection failed", err);
|
||||
return reply.code(503).send({
|
||||
date: new Date(),
|
||||
message: "Service unavailable"
|
||||
});
|
||||
}
|
||||
}
|
||||
// if (cfg.isRedisConfigured) {
|
||||
// const redis = new Redis(cfg.REDIS_URL);
|
||||
// try {
|
||||
// await redis.ping();
|
||||
// redis.disconnect();
|
||||
// } catch (err) {
|
||||
// logger.error("Health check: redis connection failed", err);
|
||||
// return reply.code(503).send({
|
||||
// date: new Date(),
|
||||
// message: "Service unavailable"
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
date: new Date(),
|
||||
|
@ -40,12 +40,12 @@ export const DefaultResponseErrorsSchema = {
|
||||
}),
|
||||
401: z.object({
|
||||
statusCode: z.literal(401),
|
||||
message: z.string(),
|
||||
message: z.any(),
|
||||
error: z.string()
|
||||
}),
|
||||
403: z.object({
|
||||
statusCode: z.literal(403),
|
||||
message: z.any(),
|
||||
message: z.string(),
|
||||
error: z.string()
|
||||
}),
|
||||
500: z.object({
|
||||
|
@ -17,6 +17,20 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
// handle querystring boolean values
|
||||
const booleanSchema = z
|
||||
.union([z.boolean(), z.string().trim()])
|
||||
.transform((value) => {
|
||||
if (typeof value === "string") {
|
||||
// ie if not empty, 0 or false, return true
|
||||
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
.optional()
|
||||
.default(true);
|
||||
|
||||
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@ -57,21 +71,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
|
||||
.optional(),
|
||||
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
|
||||
includeSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||
includeFolders: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||
includeDynamicSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -354,26 +356,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.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(),
|
||||
includeSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||
includeFolders: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||
includeDynamicSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||
includeImports: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||
includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
@ -1,3 +1,5 @@
|
||||
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
||||
|
||||
import { registerAdminRouter } from "./admin-router";
|
||||
import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
@ -101,4 +103,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
||||
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
|
||||
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
||||
|
||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||
};
|
||||
|
@ -55,10 +55,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
hashedHex: z.string().min(1),
|
||||
hashedHex: z.string().min(1).optional(),
|
||||
password: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
@ -73,7 +73,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
accessType: true
|
||||
})
|
||||
.extend({
|
||||
orgName: z.string().optional()
|
||||
orgName: z.string().optional(),
|
||||
secretValue: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@ -99,17 +100,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
encryptedValue: z.string(),
|
||||
secretValue: z.string().max(10_000),
|
||||
password: z.string().optional(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number().min(1).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid()
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -132,17 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
body: z.object({
|
||||
name: z.string().max(50).optional(),
|
||||
password: z.string().optional(),
|
||||
encryptedValue: z.string(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
secretValue: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number().min(1).optional(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid()
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -168,7 +163,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sharedSecretId: z.string().uuid()
|
||||
sharedSecretId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: SecretSharingSchema
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { registerDashboardRouter } from "./dashboard-router";
|
||||
import { registerLoginRouter } from "./login-router";
|
||||
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
||||
import { registerSecretRouter } from "./secret-router";
|
||||
@ -11,5 +10,4 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerUserRouter, { prefix: "/users" });
|
||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
|
||||
import { TableName, TIdentityAccessTokens } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
|
||||
|
||||
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
|
||||
@ -39,7 +39,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
|
||||
if (accessTokenNumUsesLimit > 0 && accessTokenNumUses > 0 && accessTokenNumUses >= accessTokenNumUsesLimit) {
|
||||
await identityAccessTokenDAL.deleteById(tokenId);
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Unable to renew because access token number of uses limit reached"
|
||||
});
|
||||
}
|
||||
@ -55,7 +55,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
|
||||
if (currentDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(tokenId);
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token due to TTL expiration"
|
||||
});
|
||||
}
|
||||
@ -67,7 +67,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
|
||||
if (currentDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(tokenId);
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token due to TTL expiration"
|
||||
});
|
||||
}
|
||||
@ -82,7 +82,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) {
|
||||
throw new ForbiddenRequestError({ message: "Only identity access tokens can be renewed" });
|
||||
throw new BadRequestError({ message: "Only identity access tokens can be renewed" });
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
||||
@ -109,7 +109,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
|
||||
if (currentDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(identityAccessToken.id);
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token due to Max TTL expiration"
|
||||
});
|
||||
}
|
||||
@ -117,7 +117,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
const extendToDate = new Date(currentDate.getTime() + Number(accessTokenTTL * 1000));
|
||||
if (extendToDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(identityAccessToken.id);
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token past its Max TTL expiration"
|
||||
});
|
||||
}
|
||||
@ -137,7 +137,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) {
|
||||
throw new ForbiddenRequestError({ message: "Only identity access tokens can be revoked" });
|
||||
throw new UnauthorizedError({ message: "Only identity access tokens can be revoked" });
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
||||
@ -160,7 +160,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
});
|
||||
if (!identityAccessToken) throw new UnauthorizedError({ message: "No identity access token found" });
|
||||
if (identityAccessToken.isAccessTokenRevoked)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to authorize revoked access token, access token is revoked"
|
||||
});
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@ -81,7 +81,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
.some((accountId) => accountId === Account);
|
||||
|
||||
if (!isAccountAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: AWS account ID not allowed."
|
||||
});
|
||||
}
|
||||
@ -100,7 +100,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!isArnAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: AWS principal ARN not allowed."
|
||||
});
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export const identityAzureAuthServiceFactory = ({
|
||||
.map((servicePrincipalId) => servicePrincipalId.trim())
|
||||
.some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid);
|
||||
|
||||
if (!isServicePrincipalAllowed) throw new ForbiddenRequestError({ message: "Service principal not allowed" });
|
||||
if (!isServicePrincipalAllowed) throw new UnauthorizedError({ message: "Service principal not allowed" });
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||
@ -314,8 +314,7 @@ export const identityAzureAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke azure auth of identity with more privileged role"
|
||||
});
|
||||
|
@ -86,7 +86,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
|
||||
|
||||
if (!isServiceAccountAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: GCP service account not allowed."
|
||||
});
|
||||
}
|
||||
@ -100,7 +100,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
|
||||
|
||||
if (!isProjectAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: GCP project not allowed."
|
||||
});
|
||||
}
|
||||
@ -112,7 +112,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
|
||||
|
||||
if (!isZoneAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: GCP zone not allowed."
|
||||
});
|
||||
}
|
||||
@ -359,8 +359,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke gcp auth of identity with more privileged role"
|
||||
});
|
||||
|
@ -132,7 +132,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
|
||||
// check the response to determine if the token is valid
|
||||
if (!(data.status && data.status.authenticated))
|
||||
throw new ForbiddenRequestError({ message: "Kubernetes token not authenticated" });
|
||||
throw new UnauthorizedError({ message: "Kubernetes token not authenticated" });
|
||||
|
||||
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
|
||||
|
||||
@ -145,7 +145,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
.some((namespace) => namespace === targetNamespace);
|
||||
|
||||
if (!isNamespaceAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: K8s namespace not allowed."
|
||||
});
|
||||
}
|
||||
@ -159,7 +159,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
.some((name) => name === targetName);
|
||||
|
||||
if (!isNameAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: K8s name not allowed."
|
||||
});
|
||||
}
|
||||
@ -171,7 +171,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
);
|
||||
|
||||
if (!isAudienceAllowed)
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: K8s audience not allowed."
|
||||
});
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
.split(", ")
|
||||
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
|
||||
) {
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC audience not allowed."
|
||||
});
|
||||
}
|
||||
@ -161,7 +161,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC claim not allowed."
|
||||
});
|
||||
}
|
||||
@ -532,8 +532,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge) {
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke OIDC auth of identity with more privileged role"
|
||||
});
|
||||
|
@ -88,7 +88,7 @@ export const identityUaServiceFactory = ({
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to expired client secret"
|
||||
});
|
||||
}
|
||||
@ -100,7 +100,7 @@ export const identityUaServiceFactory = ({
|
||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to client secret usage limit reached"
|
||||
});
|
||||
}
|
||||
@ -368,8 +368,7 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke universal auth of identity with more privileged role"
|
||||
});
|
||||
@ -474,8 +473,8 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to add identity to project with more privileged role"
|
||||
});
|
||||
@ -521,8 +520,7 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to read identity client secret of project with more privileged role"
|
||||
});
|
||||
@ -561,8 +559,8 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke identity client secret with more privileged role"
|
||||
});
|
||||
|
@ -208,20 +208,20 @@ export const kmsServiceFactory = ({
|
||||
return org.kmsDefaultKeyId;
|
||||
};
|
||||
|
||||
const encryptWithRootKey = async () => {
|
||||
const encryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return ({ plainText }: { plainText: Buffer }) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
|
||||
return (plainTextBuffer: Buffer) => {
|
||||
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||
return encryptedBuffer;
|
||||
};
|
||||
};
|
||||
|
||||
const decryptWithRootKey = async () => {
|
||||
const decryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
|
||||
return Promise.resolve(decryptedBlob);
|
||||
|
||||
return (cipherTextBuffer: Buffer) => {
|
||||
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -892,7 +892,7 @@ export const orgServiceFactory = ({
|
||||
|
||||
const membership = await orgMembershipDAL.findOrgMembershipById(membershipId);
|
||||
if (!membership) {
|
||||
throw new NotFoundError({ message: "Failed to find organization membership" });
|
||||
throw new NotFoundError({ message: "Organization membership not found" });
|
||||
}
|
||||
if (membership.orgId !== orgId) {
|
||||
throw new ForbiddenRequestError({ message: "Membership does not belong to organization" });
|
||||
@ -937,7 +937,9 @@ export const orgServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
|
||||
const membership = await orgMembershipDAL.findOrgMembershipById(orgMembershipId);
|
||||
if (!membership) throw new NotFoundError({ message: "Failed to find organization membership" });
|
||||
if (!membership) {
|
||||
throw new NotFoundError({ message: "Organization membership not found" });
|
||||
}
|
||||
if (membership.orgId !== orgId) throw new NotFoundError({ message: "Failed to find organization membership" });
|
||||
|
||||
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, membership.user.id);
|
||||
|
@ -909,9 +909,7 @@ export const projectServiceFactory = ({
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "User is not a member of the project"
|
||||
});
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const kmsKeyId = await kmsService.getProjectSecretManagerKmsKeyId(projectId);
|
||||
|
@ -548,7 +548,7 @@ export const secretImportServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||
|
@ -1,10 +1,14 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import bcrypt from "bcrypt";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TSecretSharing } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { SecretSharingAccessType } from "@app/lib/types";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||
import {
|
||||
@ -19,14 +23,18 @@ type TSecretSharingServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
secretSharingDAL: TSecretSharingDALFactory;
|
||||
orgDAL: TOrgDALFactory;
|
||||
kmsService: TKmsServiceFactory;
|
||||
};
|
||||
|
||||
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||
|
||||
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
|
||||
|
||||
export const secretSharingServiceFactory = ({
|
||||
permissionService,
|
||||
secretSharingDAL,
|
||||
orgDAL
|
||||
orgDAL,
|
||||
kmsService
|
||||
}: TSecretSharingServiceFactoryDep) => {
|
||||
const createSharedSecret = async ({
|
||||
actor,
|
||||
@ -34,10 +42,7 @@ export const secretSharingServiceFactory = ({
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
secretValue,
|
||||
name,
|
||||
password,
|
||||
accessType,
|
||||
@ -59,19 +64,25 @@ export const secretSharingServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||
}
|
||||
|
||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||
if (encryptedValue.length > 13000) {
|
||||
if (secretValue.length > 10_000) {
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
|
||||
const id = crypto.randomBytes(32).toString("hex");
|
||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
identifier: id,
|
||||
iv: null,
|
||||
tag: null,
|
||||
encryptedValue: null,
|
||||
encryptedSecret,
|
||||
name,
|
||||
password: hashedPassword,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews,
|
||||
userId: actorId,
|
||||
@ -79,15 +90,14 @@ export const secretSharingServiceFactory = ({
|
||||
accessType
|
||||
});
|
||||
|
||||
return { id: newSharedSecret.id };
|
||||
const idToReturn = `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}`;
|
||||
|
||||
return { id: idToReturn };
|
||||
};
|
||||
|
||||
const createPublicSharedSecret = async ({
|
||||
password,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
secretValue,
|
||||
expiresAt,
|
||||
expiresAfterViews,
|
||||
accessType
|
||||
@ -104,24 +114,25 @@ export const secretSharingServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||
}
|
||||
|
||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||
if (encryptedValue.length > 13000) {
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
|
||||
const id = crypto.randomBytes(32).toString("hex");
|
||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
identifier: id,
|
||||
encryptedValue: null,
|
||||
iv: null,
|
||||
tag: null,
|
||||
encryptedSecret,
|
||||
password: hashedPassword,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews,
|
||||
accessType
|
||||
});
|
||||
|
||||
return { id: newSharedSecret.id };
|
||||
return { id: `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}` };
|
||||
};
|
||||
|
||||
const getSharedSecrets = async ({
|
||||
@ -162,25 +173,30 @@ export const secretSharingServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
|
||||
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing) => {
|
||||
const { expiresAfterViews } = sharedSecret;
|
||||
|
||||
if (expiresAfterViews) {
|
||||
// decrement view count if view count expiry set
|
||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||
await secretSharingDAL.updateById(sharedSecret.id, { $decr: { expiresAfterViews: 1 } });
|
||||
}
|
||||
|
||||
await secretSharingDAL.updateById(sharedSecretId, {
|
||||
await secretSharingDAL.updateById(sharedSecret.id, {
|
||||
lastViewedAt: new Date()
|
||||
});
|
||||
};
|
||||
|
||||
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
|
||||
/** Get's password-less secret. validates all secret's requested (must be fresh). */
|
||||
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
|
||||
const sharedSecret = await secretSharingDAL.findOne({
|
||||
id: sharedSecretId,
|
||||
hashedHex
|
||||
});
|
||||
const sharedSecret = isUuidV4(sharedSecretId)
|
||||
? await secretSharingDAL.findOne({
|
||||
id: sharedSecretId,
|
||||
hashedHex
|
||||
})
|
||||
: await secretSharingDAL.findOne({
|
||||
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
|
||||
});
|
||||
|
||||
if (!sharedSecret)
|
||||
throw new NotFoundError({
|
||||
message: "Shared secret not found"
|
||||
@ -222,13 +238,23 @@ export const secretSharingServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
// If encryptedSecret is set, we know that this secret has been encrypted using KMS, and we can therefore do server-side decryption.
|
||||
let decryptedSecretValue: Buffer | undefined;
|
||||
if (sharedSecret.encryptedSecret) {
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
decryptedSecretValue = decryptWithRoot(sharedSecret.encryptedSecret);
|
||||
}
|
||||
|
||||
// decrement when we are sure the user will view secret.
|
||||
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
|
||||
await $decrementSecretViewCount(sharedSecret);
|
||||
|
||||
return {
|
||||
isPasswordProtected,
|
||||
secret: {
|
||||
...sharedSecret,
|
||||
...(decryptedSecretValue && {
|
||||
secretValue: Buffer.from(decryptedSecretValue).toString()
|
||||
}),
|
||||
orgName:
|
||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||
? orgName
|
||||
@ -241,7 +267,16 @@ export const secretSharingServiceFactory = ({
|
||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
||||
|
||||
const sharedSecret = isUuidV4(sharedSecretId)
|
||||
? await secretSharingDAL.findById(sharedSecretId)
|
||||
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
|
||||
|
||||
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
|
||||
|
||||
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
|
||||
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
|
||||
|
||||
return deletedSharedSecret;
|
||||
};
|
||||
|
||||
|
@ -19,10 +19,7 @@ export type TSharedSecretPermission = {
|
||||
};
|
||||
|
||||
export type TCreatePublicSharedSecretDTO = {
|
||||
encryptedValue: string;
|
||||
hashedHex: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secretValue: string;
|
||||
expiresAt: string;
|
||||
expiresAfterViews?: number;
|
||||
password?: string;
|
||||
@ -31,7 +28,7 @@ export type TCreatePublicSharedSecretDTO = {
|
||||
|
||||
export type TGetActiveSharedSecretByIdDTO = {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string;
|
||||
hashedHex?: string;
|
||||
orgId?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
@ -835,7 +835,7 @@ export const createManySecretsRawFnFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const inputSecrets = secrets.map((secret) => {
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
||||
@ -1000,7 +1000,7 @@ export const updateManySecretsRawFnFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||
if (!blindIndexCfg) throw new NotFoundError({ message: "Blind index not found", name: "Update secret" });
|
||||
|
@ -930,6 +930,11 @@ export const secretQueueFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
// re-throw error to re-run job unless final attempt, then log and send fail email
|
||||
if (job.attemptsStarted !== job.opts.attempts) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastSyncJobId: job.id,
|
||||
syncMessage: message,
|
||||
|
@ -1105,7 +1105,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
const { secrets, imports } = await getSecrets({
|
||||
@ -1269,7 +1269,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
||||
|
||||
@ -1365,7 +1365,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||
@ -1507,7 +1507,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||
@ -1633,7 +1633,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
if (policy) {
|
||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||
@ -1737,7 +1737,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const sanitizedSecrets = inputSecrets.map(
|
||||
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
||||
@ -1863,7 +1863,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const sanitizedSecrets = inputSecrets.map(
|
||||
({
|
||||
@ -1995,7 +1995,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
if (policy) {
|
||||
@ -2332,7 +2332,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
await secretDAL.transaction(async (tx) => {
|
||||
@ -2418,7 +2418,7 @@ export const secretServiceFactory = ({
|
||||
if (!botKey) {
|
||||
throw new NotFoundError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "BotNotFoundError"
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import bcrypt from "bcrypt";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
@ -168,7 +168,7 @@ export const serviceTokenServiceFactory = ({
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(tokenSecret, serviceToken.secretHash);
|
||||
if (!isMatch) throw new ForbiddenRequestError();
|
||||
if (!isMatch) throw new UnauthorizedError({ message: "Invalid service token" });
|
||||
await accessTokenQueue.updateServiceTokenStatus(serviceToken.id);
|
||||
|
||||
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
|
||||
|
@ -141,16 +141,14 @@ export const slackServiceFactory = ({
|
||||
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
||||
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
||||
|
||||
const decrypt = await kmsService.decryptWithRootKey();
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString();
|
||||
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
slackClientSecret = (
|
||||
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
|
||||
).toString();
|
||||
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
|
||||
}
|
||||
|
||||
if (!slackClientId || !slackClientSecret) {
|
||||
|
@ -122,20 +122,16 @@ export const superAdminServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const encryptWithRoot = await kmsService.encryptWithRootKey();
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
if (data.slackClientId) {
|
||||
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({
|
||||
plainText: Buffer.from(data.slackClientId)
|
||||
});
|
||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.slackClientId));
|
||||
|
||||
updatedData.encryptedSlackClientId = encryptedClientId;
|
||||
updatedData.slackClientId = undefined;
|
||||
}
|
||||
|
||||
if (data.slackClientSecret) {
|
||||
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({
|
||||
plainText: Buffer.from(data.slackClientSecret)
|
||||
});
|
||||
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.slackClientSecret));
|
||||
|
||||
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
|
||||
updatedData.slackClientSecret = undefined;
|
||||
@ -270,14 +266,14 @@ export const superAdminServiceFactory = ({
|
||||
let clientId = "";
|
||||
let clientSecret = "";
|
||||
|
||||
const decrypt = await kmsService.decryptWithRootKey();
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString();
|
||||
clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString();
|
||||
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -24,7 +24,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(
|
||||
"z-30 min-w-[220px] rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
"z-30 min-w-[220px] overflow-y-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
@ -48,21 +48,15 @@ export const dashboardKeys = {
|
||||
};
|
||||
|
||||
export const fetchProjectSecretsOverview = async ({
|
||||
includeFolders,
|
||||
includeSecrets,
|
||||
includeDynamicSecrets,
|
||||
environments,
|
||||
...params
|
||||
}: TGetDashboardProjectSecretsOverviewDTO) => {
|
||||
const { data } = await apiRequest.get<DashboardProjectSecretsOverviewResponse>(
|
||||
"/api/v3/dashboard/secrets-overview",
|
||||
"/api/v1/dashboard/secrets-overview",
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
environments: encodeURIComponent(environments.join(",")),
|
||||
includeFolders: includeFolders ? "1" : "",
|
||||
includeSecrets: includeSecrets ? "1" : "",
|
||||
includeDynamicSecrets: includeDynamicSecrets ? "1" : ""
|
||||
environments: encodeURIComponent(environments.join(","))
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -71,22 +65,14 @@ export const fetchProjectSecretsOverview = async ({
|
||||
};
|
||||
|
||||
export const fetchProjectSecretsDetails = async ({
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeSecrets,
|
||||
includeDynamicSecrets,
|
||||
tags,
|
||||
...params
|
||||
}: TGetDashboardProjectSecretsDetailsDTO) => {
|
||||
const { data } = await apiRequest.get<DashboardProjectSecretsDetailsResponse>(
|
||||
"/api/v3/dashboard/secrets-details",
|
||||
"/api/v1/dashboard/secrets-details",
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
includeImports: includeImports ? "1" : "",
|
||||
includeFolders: includeFolders ? "1" : "",
|
||||
includeSecrets: includeSecrets ? "1" : "",
|
||||
includeDynamicSecrets: includeDynamicSecrets ? "1" : "",
|
||||
tags: encodeURIComponent(
|
||||
Object.entries(tags)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
@ -3,13 +3,21 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretSharingKeys } from "./queries";
|
||||
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
|
||||
import {
|
||||
TCreatedSharedSecret,
|
||||
TCreateSharedSecretRequest,
|
||||
TDeleteSharedSecretRequest,
|
||||
TSharedSecret
|
||||
} from "./types";
|
||||
|
||||
export const useCreateSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData);
|
||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||
"/api/v1/secret-sharing",
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||
@ -20,7 +28,7 @@ export const useCreatePublicSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>(
|
||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||
"/api/v1/secret-sharing/public",
|
||||
inputData
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ export const secretSharingKeys = {
|
||||
allSharedSecrets: () => ["sharedSecrets"] as const,
|
||||
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
|
||||
getSecretById: (arg: { id: string; hashedHex: string; password?: string }) => [
|
||||
getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
|
||||
"shared-secret",
|
||||
arg
|
||||
]
|
||||
@ -46,7 +46,7 @@ export const useGetActiveSharedSecretById = ({
|
||||
password
|
||||
}: {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string;
|
||||
hashedHex: string | null;
|
||||
password?: string;
|
||||
}) => {
|
||||
return useQuery<TViewSharedSecretResponse>(
|
||||
@ -55,7 +55,7 @@ export const useGetActiveSharedSecretById = ({
|
||||
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||
{
|
||||
hashedHex,
|
||||
...(hashedHex && { hashedHex }),
|
||||
password
|
||||
}
|
||||
);
|
||||
@ -63,7 +63,7 @@ export const useGetActiveSharedSecretById = ({
|
||||
return data;
|
||||
},
|
||||
{
|
||||
enabled: Boolean(sharedSecretId) && Boolean(hashedHex)
|
||||
enabled: Boolean(sharedSecretId)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -13,13 +13,14 @@ export type TSharedSecret = {
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type TCreatedSharedSecret = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretRequest = {
|
||||
name?: string;
|
||||
password?: string;
|
||||
encryptedValue: string;
|
||||
hashedHex: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secretValue: string;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews?: number;
|
||||
accessType?: SecretSharingAccessType;
|
||||
@ -28,6 +29,7 @@ export type TCreateSharedSecretRequest = {
|
||||
export type TViewSharedSecretResponse = {
|
||||
isPasswordProtected: boolean;
|
||||
secret: {
|
||||
secretValue?: string;
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
@ -44,4 +46,3 @@ export enum SecretSharingAccessType {
|
||||
Anyone = "anyone",
|
||||
Organization = "organization"
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
@ -245,8 +246,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{awsRegions.map((awsRegion) => (
|
||||
<SelectItem value={awsRegion.slug} key={`flyio-environment-${awsRegion.slug}`}>
|
||||
{awsRegion.name}
|
||||
<SelectItem value={awsRegion.slug} key={`aws-environment-${awsRegion.slug}`}>
|
||||
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
@ -18,6 +18,7 @@ import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
@ -267,8 +268,12 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{awsRegions.map((awsRegion) => (
|
||||
<SelectItem value={awsRegion.slug} key={`flyio-environment-${awsRegion.slug}`}>
|
||||
{awsRegion.name}
|
||||
<SelectItem
|
||||
value={awsRegion.slug}
|
||||
className="flex w-full justify-between"
|
||||
key={`aws-environment-${awsRegion.slug}`}
|
||||
>
|
||||
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
@ -351,7 +351,7 @@ export const SecretOverviewPage = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
await createSecretV3({
|
||||
const result = await createSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
@ -360,10 +360,18 @@ export const SecretOverviewPage = () => {
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created secret"
|
||||
});
|
||||
|
||||
if ("approval" in result) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Requested change has been sent for review"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created secret"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
@ -388,7 +396,7 @@ export const SecretOverviewPage = () => {
|
||||
type = SecretType.Shared
|
||||
) => {
|
||||
try {
|
||||
await updateSecretV3({
|
||||
const result = await updateSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
@ -396,10 +404,18 @@ export const SecretOverviewPage = () => {
|
||||
secretValue: value,
|
||||
type
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated secret"
|
||||
});
|
||||
|
||||
if ("approval" in result) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Requested change has been sent for review"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated secret"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
@ -411,7 +427,7 @@ export const SecretOverviewPage = () => {
|
||||
|
||||
const handleSecretDelete = async (env: string, key: string, secretId?: string) => {
|
||||
try {
|
||||
await deleteSecretV3({
|
||||
const result = await deleteSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
@ -419,10 +435,18 @@ export const SecretOverviewPage = () => {
|
||||
secretId,
|
||||
type: SecretType.Shared
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted secret"
|
||||
});
|
||||
|
||||
if ("approval" in result) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Requested change has been sent for review"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted secret"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
|
@ -122,42 +122,76 @@ export const CreateSecretForm = ({
|
||||
|
||||
const isEdit = getSecretByKey(environment, key) !== undefined;
|
||||
if (isEdit) {
|
||||
return updateSecretV3({
|
||||
return {
|
||||
...(await updateSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
environment
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...(await createSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
});
|
||||
}
|
||||
|
||||
return createSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
});
|
||||
})),
|
||||
environment
|
||||
};
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const isSecretsAdded = results.some((result) => result.status === "fulfilled");
|
||||
const forApprovalEnvs = results
|
||||
.map((result) =>
|
||||
result.status === "fulfilled" && "approval" in result.value
|
||||
? result.value.environment
|
||||
: undefined
|
||||
)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (isSecretsAdded) {
|
||||
const updatedEnvs = results
|
||||
.map((result) =>
|
||||
result.status === "fulfilled" && !("approval" in result.value)
|
||||
? result.value.environment
|
||||
: undefined
|
||||
)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (forApprovalEnvs.length) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: `Change request submitted for ${
|
||||
forApprovalEnvs.length > 1 ? "environments" : "environment"
|
||||
}: ${forApprovalEnvs.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
if (updatedEnvs.length) {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Secrets created successfully"
|
||||
text: `Secrets created in ${
|
||||
updatedEnvs.length > 1 ? "environments" : "environment"
|
||||
}: ${updatedEnvs.join(", ")}`
|
||||
});
|
||||
onClose();
|
||||
reset();
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!updatedEnvs.length && !forApprovalEnvs.length) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create secrets"
|
||||
});
|
||||
} else {
|
||||
onClose();
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -73,7 +73,7 @@ export const SecretOverviewTableRow = ({
|
||||
>
|
||||
<div className="h-full w-full border-r border-mineshaft-600 py-2.5 px-5">
|
||||
<div className="flex items-center space-x-5">
|
||||
<div className="text-blue-300/70">
|
||||
<div className="text-bunker-300">
|
||||
<Checkbox
|
||||
id={`checkbox-${secretKey}`}
|
||||
isChecked={isSelected}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
folderCount?: number;
|
||||
importCount?: number;
|
||||
@ -17,28 +19,68 @@ export const SecretTableResourceCount = ({
|
||||
return (
|
||||
<div className="flex items-center gap-2 divide-x divide-mineshaft-500 text-sm text-mineshaft-400">
|
||||
{importCount > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFileImport} className=" text-green-700" />
|
||||
<span>{importCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total import count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFileImport} className=" text-green-700" />
|
||||
<span>{importCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{folderCount > 0 && (
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
|
||||
<span>{folderCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total folder count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
|
||||
<span>{folderCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{dynamicSecretCount > 0 && (
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" />
|
||||
<span>{dynamicSecretCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total dynamic secret count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" />
|
||||
<span>{dynamicSecretCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{secretCount > 0 && (
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faKey} className="text-bunker-300" />
|
||||
<span>{secretCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total secret count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faKey} className="text-bunker-300" />
|
||||
<span>{secretCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { Badge, Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useAddExternalKms, useUpdateExternalKms } from "@app/hooks/api";
|
||||
import {
|
||||
@ -244,7 +244,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
|
||||
>
|
||||
{AWS_REGIONS.map((awsRegion) => (
|
||||
<SelectItem value={awsRegion.slug} key={`kms-aws-region-${awsRegion.slug}`}>
|
||||
{awsRegion.name} ({awsRegion.slug})
|
||||
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
@ -1,5 +1,3 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
|
||||
@ -8,7 +6,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
|
||||
@ -79,30 +76,16 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
try {
|
||||
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||
|
||||
const key = crypto.randomBytes(16).toString("hex");
|
||||
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: secret,
|
||||
key
|
||||
});
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
name,
|
||||
password,
|
||||
encryptedValue: ciphertext,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
secretValue: secret,
|
||||
expiresAt,
|
||||
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
|
||||
accessType
|
||||
});
|
||||
|
||||
setSecretLink(
|
||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||
hashedHex
|
||||
)}-${encodeURIComponent(key)}`
|
||||
);
|
||||
setSecretLink(`${window.location.origin}/shared/secret/${id}`);
|
||||
reset();
|
||||
|
||||
setCopyTextSecret("secret");
|
||||
|
@ -1,23 +1,42 @@
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { NextRouter, useRouter } from "next/router";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
|
||||
|
||||
import { PasswordContainer,SecretContainer, SecretErrorContainer } from "./components";
|
||||
import { PasswordContainer, SecretContainer, SecretErrorContainer } from "./components";
|
||||
|
||||
const extractDetailsFromUrl = (router: NextRouter) => {
|
||||
const { id, key: urlEncodedKey } = router.query;
|
||||
|
||||
const idString = id as string;
|
||||
|
||||
if (urlEncodedKey) {
|
||||
const [hashedHex, key] = urlEncodedKey ? urlEncodedKey.toString().split("-") : ["", ""];
|
||||
|
||||
return {
|
||||
id: idString,
|
||||
hashedHex,
|
||||
key
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: idString,
|
||||
hashedHex: null,
|
||||
key: null
|
||||
};
|
||||
};
|
||||
|
||||
export const ViewSecretPublicPage = () => {
|
||||
const router = useRouter();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const { id, key: urlEncodedPublicKey } = router.query;
|
||||
|
||||
const [hashedHex, key] = urlEncodedPublicKey
|
||||
? urlEncodedPublicKey.toString().split("-")
|
||||
: ["", ""];
|
||||
const { hashedHex, key, id } = extractDetailsFromUrl(router);
|
||||
|
||||
const {
|
||||
data: fetchSecret,
|
||||
@ -25,7 +44,7 @@ export const ViewSecretPublicPage = () => {
|
||||
isLoading,
|
||||
isFetching
|
||||
} = useGetActiveSharedSecretById({
|
||||
sharedSecretId: id as string,
|
||||
sharedSecretId: id,
|
||||
hashedHex,
|
||||
password
|
||||
});
|
||||
@ -80,7 +99,7 @@ export const ViewSecretPublicPage = () => {
|
||||
)}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{!error && fetchSecret?.secret && key && (
|
||||
{!error && fetchSecret?.secret && (
|
||||
<SecretContainer secret={fetchSecret.secret} secretKey={key} />
|
||||
)}
|
||||
{error && !isInvalidCredential && <SecretErrorContainer />}
|
||||
|
@ -15,7 +15,7 @@ import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
|
||||
|
||||
type Props = {
|
||||
secret: TViewSharedSecretResponse["secret"];
|
||||
secretKey: string;
|
||||
secretKey: string | null;
|
||||
};
|
||||
|
||||
export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||
@ -25,6 +25,10 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||
});
|
||||
|
||||
const decryptedSecret = useMemo(() => {
|
||||
if (secret.secretValue) {
|
||||
return secret.secretValue;
|
||||
}
|
||||
|
||||
if (secret && secret.encryptedValue && key) {
|
||||
const res = decryptSymmetric({
|
||||
ciphertext: secret.encryptedValue,
|
||||
|
Reference in New Issue
Block a user