mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 17:26:07 +00:00
Merge remote-tracking branch 'origin/main' into misc/privilege-management-v2-transition
This commit is contained in:
@@ -35,7 +35,20 @@ jobs:
|
|||||||
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||||
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
||||||
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
||||||
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api
|
|
||||||
|
echo "Examining built image:"
|
||||||
|
docker image inspect infisical-api | grep -A 5 "Entrypoint"
|
||||||
|
|
||||||
|
docker run --name infisical-api -d -p 4000:4000 \
|
||||||
|
-e DB_CONNECTION_URI=$DB_CONNECTION_URI \
|
||||||
|
-e REDIS_URL=$REDIS_URL \
|
||||||
|
-e JWT_AUTH_SECRET=$JWT_AUTH_SECRET \
|
||||||
|
-e ENCRYPTION_KEY=$ENCRYPTION_KEY \
|
||||||
|
--env-file .env \
|
||||||
|
infisical-api
|
||||||
|
|
||||||
|
echo "Container status right after creation:"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://172.17.0.1:6379
|
REDIS_URL: redis://172.17.0.1:6379
|
||||||
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
||||||
@@ -49,21 +62,33 @@ jobs:
|
|||||||
SECONDS=0
|
SECONDS=0
|
||||||
HEALTHY=0
|
HEALTHY=0
|
||||||
while [ $SECONDS -lt 60 ]; do
|
while [ $SECONDS -lt 60 ]; do
|
||||||
if docker ps | grep infisical-api | grep -q healthy; then
|
# Check if container is running
|
||||||
echo "Container is healthy."
|
if docker ps | grep infisical-api; then
|
||||||
HEALTHY=1
|
# Try to access the API endpoint
|
||||||
|
if curl -s -f http://localhost:4000/api/docs/json > /dev/null 2>&1; then
|
||||||
|
echo "API endpoint is responding. Container seems healthy."
|
||||||
|
HEALTHY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Container is not running!"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
|
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
|
||||||
|
sleep 5
|
||||||
docker logs infisical-api
|
SECONDS=$((SECONDS+5))
|
||||||
|
|
||||||
sleep 2
|
|
||||||
SECONDS=$((SECONDS+2))
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $HEALTHY -ne 1 ]; then
|
if [ $HEALTHY -ne 1 ]; then
|
||||||
echo "Container did not become healthy in time"
|
echo "Container did not become healthy in time"
|
||||||
|
echo "Container status:"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
|
echo "Container logs (if any):"
|
||||||
|
docker logs infisical-api || echo "No logs available"
|
||||||
|
echo "Container inspection:"
|
||||||
|
docker inspect infisical-api | grep -A 5 "State"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Install openapi-diff
|
- name: Install openapi-diff
|
||||||
@@ -71,7 +96,8 @@ jobs:
|
|||||||
- name: Running OpenAPI Spec diff action
|
- name: Running OpenAPI Spec diff action
|
||||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
docker compose -f "docker-compose.dev.yml" down
|
docker compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api || true
|
||||||
docker remove infisical-api
|
docker rm infisical-api || true
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.string("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.dropColumn("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (!hasSecretVersionV2UserActorId) {
|
||||||
|
t.uuid("userActorId");
|
||||||
|
t.foreign("userActorId").references("id").inTable(TableName.Users);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.uuid("identityActorId");
|
||||||
|
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2ActorType) {
|
||||||
|
t.string("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (hasSecretVersionV2UserActorId) {
|
||||||
|
t.dropColumn("userActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.dropColumn("identityActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2ActorType) {
|
||||||
|
t.dropColumn("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ export const SecretApprovalRequestsReviewersSchema = z.object({
|
|||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
reviewerUserId: z.string().uuid()
|
reviewerUserId: z.string().uuid(),
|
||||||
|
comment: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
|
|||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
userActorId: z.string().uuid().nullable().optional(),
|
||||||
|
identityActorId: z.string().uuid().nullable().optional(),
|
||||||
|
actorType: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]),
|
||||||
|
comment: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -175,8 +176,25 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
approvalId: req.params.id,
|
approvalId: req.params.id,
|
||||||
status: req.body.status
|
status: req.body.status,
|
||||||
|
comment: req.body.comment
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: review.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW,
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: review.requestId,
|
||||||
|
reviewedBy: review.reviewerUserId,
|
||||||
|
status: review.status as ApprovalStatus,
|
||||||
|
comment: review.comment || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { review };
|
return { review };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -267,7 +285,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
committerUser: approvalRequestUser,
|
committerUser: approvalRequestUser,
|
||||||
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
|
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: secretRawSchema
|
commits: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
import { KmipPermission } from "../kmip/kmip-enum";
|
import { KmipPermission } from "../kmip/kmip-enum";
|
||||||
|
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
filter: {
|
filter: {
|
||||||
@@ -165,6 +166,7 @@ export enum EventType {
|
|||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
||||||
|
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review",
|
||||||
SIGN_SSH_KEY = "sign-ssh-key",
|
SIGN_SSH_KEY = "sign-ssh-key",
|
||||||
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
||||||
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
||||||
@@ -1314,6 +1316,16 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretApprovalRequestReview {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW;
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: string;
|
||||||
|
reviewedBy: string;
|
||||||
|
status: ApprovalStatus;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SignSshKey {
|
interface SignSshKey {
|
||||||
type: EventType.SIGN_SSH_KEY;
|
type: EventType.SIGN_SSH_KEY;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2482,4 +2494,5 @@ export type Event =
|
|||||||
| KmipOperationRevokeEvent
|
| KmipOperationRevokeEvent
|
||||||
| KmipOperationLocateEvent
|
| KmipOperationLocateEvent
|
||||||
| KmipOperationRegisterEvent
|
| KmipOperationRegisterEvent
|
||||||
| CreateSecretRequestEvent;
|
| CreateSecretRequestEvent
|
||||||
|
| SecretApprovalRequestReview;
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
||||||
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||||
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
|
tx.ref("comment").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerComment"),
|
||||||
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
||||||
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||||
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||||
@@ -162,8 +163,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
reviewerEmail: email,
|
reviewerEmail: email,
|
||||||
reviewerLastName: lastName,
|
reviewerLastName: lastName,
|
||||||
reviewerUsername: username,
|
reviewerUsername: username,
|
||||||
reviewerFirstName: firstName
|
reviewerFirstName: firstName,
|
||||||
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
|
reviewerComment: comment
|
||||||
|
}) =>
|
||||||
|
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
approvalId,
|
approvalId,
|
||||||
actor,
|
actor,
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
@@ -372,15 +373,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
return secretApprovalRequestReviewerDAL.create(
|
return secretApprovalRequestReviewerDAL.create(
|
||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
reviewerUserId: actorId
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return secretApprovalRequestReviewerDAL.updateById(review.id, { status }, tx);
|
|
||||||
|
return secretApprovalRequestReviewerDAL.updateById(review.id, { status, comment }, tx);
|
||||||
});
|
});
|
||||||
return reviewStatus;
|
|
||||||
|
return { ...reviewStatus, projectId: secretApprovalRequest.projectId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateApprovalStatus = async ({
|
const updateApprovalStatus = async ({
|
||||||
@@ -499,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!hasMinApproval && !isSoftEnforcement)
|
if (!hasMinApproval && !isSoftEnforcement)
|
||||||
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||||
|
|
||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
|
||||||
let mergeStatus;
|
let mergeStatus;
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
// this cycle if for bridged secrets
|
// this cycle if for bridged secrets
|
||||||
@@ -857,7 +861,6 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
if (isSoftEnforcement) {
|
if (isSoftEnforcement) {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const project = await projectDAL.findProjectById(projectId);
|
|
||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||||
const approverUsers = await userDAL.find({
|
const approverUsers = await userDAL.find({
|
||||||
@@ -1152,7 +1155,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
environment: env.name,
|
environment: env.name,
|
||||||
secretPath,
|
secretPath,
|
||||||
projectId,
|
projectId,
|
||||||
requestId: secretApprovalRequest.id
|
requestId: secretApprovalRequest.id,
|
||||||
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1452,7 +1456,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
environment: env.name,
|
environment: env.name,
|
||||||
secretPath,
|
secretPath,
|
||||||
projectId,
|
projectId,
|
||||||
requestId: secretApprovalRequest.id
|
requestId: secretApprovalRequest.id,
|
||||||
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export type TStatusChangeDTO = {
|
|||||||
export type TReviewRequestDTO = {
|
export type TReviewRequestDTO = {
|
||||||
approvalId: string;
|
approvalId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TApprovalRequestCountDTO = TProjectPermission;
|
export type TApprovalRequestCountDTO = TProjectPermission;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { NotFoundError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -332,6 +333,7 @@ export const secretRotationQueueFactory = ({
|
|||||||
await secretVersionV2BridgeDAL.insertMany(
|
await secretVersionV2BridgeDAL.insertMany(
|
||||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
|
actorType: ActorType.PLATFORM,
|
||||||
secretId: id
|
secretId: id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
|||||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -370,7 +371,21 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
const secrets = await secretV2BridgeDAL.insertMany(
|
const secrets = await secretV2BridgeDAL.insertMany(
|
||||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||||
secretVersions.map(
|
secretVersions.map(
|
||||||
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
|
({
|
||||||
|
latestSecretVersion,
|
||||||
|
version,
|
||||||
|
updatedAt,
|
||||||
|
createdAt,
|
||||||
|
secretId,
|
||||||
|
envId,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
// exclude the bottom fields from the secret - they are for versioning only.
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType,
|
||||||
|
...el
|
||||||
|
}) => ({
|
||||||
...el,
|
...el,
|
||||||
id: secretId,
|
id: secretId,
|
||||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||||
@@ -401,8 +416,18 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
||||||
|
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
||||||
|
const actorType = actor || ActorType.PLATFORM;
|
||||||
|
|
||||||
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
|
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
secretId: id,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await secretVersionV2TagBridgeDAL.insertMany(
|
await secretVersionV2TagBridgeDAL.insertMany(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
|
|
||||||
import * as quic from "@infisical/quic";
|
import quicDefault, * as quicModule from "@infisical/quic";
|
||||||
|
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
@@ -10,6 +10,8 @@ import { logger } from "../logger";
|
|||||||
const DEFAULT_MAX_RETRIES = 3;
|
const DEFAULT_MAX_RETRIES = 3;
|
||||||
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
||||||
|
|
||||||
|
const quic = quicDefault || quicModule;
|
||||||
|
|
||||||
const parseSubjectDetails = (data: string) => {
|
const parseSubjectDetails = (data: string) => {
|
||||||
const values: Record<string, string> = {};
|
const values: Record<string, string> = {};
|
||||||
data.split("\n").forEach((el) => {
|
data.split("\n").forEach((el) => {
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ const run = async () => {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
logger.error(error, "CRITICAL ERROR: Uncaught Exception");
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (error) => {
|
||||||
|
logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection");
|
||||||
|
});
|
||||||
|
|
||||||
await server.listen({
|
await server.listen({
|
||||||
port: envConfig.PORT,
|
port: envConfig.PORT,
|
||||||
host: envConfig.HOST,
|
host: envConfig.HOST,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
TQueueSecretSyncSyncSecretsByIdDTO,
|
TQueueSecretSyncSyncSecretsByIdDTO,
|
||||||
TQueueSendSecretSyncActionFailedNotificationsDTO
|
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||||
|
|
||||||
export enum QueueName {
|
export enum QueueName {
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
@@ -107,7 +108,7 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
[QueueName.SecretWebhook]: {
|
[QueueName.SecretWebhook]: {
|
||||||
name: QueueJobs.SecWebhook;
|
name: QueueJobs.SecWebhook;
|
||||||
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
payload: TWebhookPayloads;
|
||||||
};
|
};
|
||||||
|
|
||||||
[QueueName.AccessTokenStatusUpdate]:
|
[QueueName.AccessTokenStatusUpdate]:
|
||||||
|
|||||||
@@ -111,7 +111,16 @@ export const secretRawSchema = z.object({
|
|||||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
actor: z
|
||||||
|
.object({
|
||||||
|
actorId: z.string().nullable().optional(),
|
||||||
|
actorType: z.string().nullable().optional(),
|
||||||
|
name: z.string().nullable().optional(),
|
||||||
|
membershipId: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ProjectPermissionSchema = z.object({
|
export const ProjectPermissionSchema = z.object({
|
||||||
|
|||||||
@@ -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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/raw/:secretName",
|
url: "/raw/:secretName",
|
||||||
|
|||||||
@@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,20 +114,27 @@ export const integrationAuthServiceFactory = ({
|
|||||||
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
|
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
|
||||||
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
|
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
|
||||||
|
|
||||||
return Promise.all(
|
const filteredAuthorizations = await Promise.all(
|
||||||
authorizations.filter(async (auth) => {
|
authorizations.map(async (auth) => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
try {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actorId,
|
actor,
|
||||||
projectId: auth.projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId: auth.projectId,
|
||||||
actorOrgId,
|
actorAuthMethod,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actorOrgId,
|
||||||
});
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null;
|
||||||
|
} catch (error) {
|
||||||
|
// user does not belong to the project that the integration auth belongs to
|
||||||
|
return null;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return filteredAuthorizations.filter((auth): auth is NonNullable<typeof auth> => auth !== null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
|
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
|
||||||
|
|||||||
@@ -613,6 +613,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
`${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`)
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
.select(selectAllTableCols(TableName.SecretV2))
|
.select(selectAllTableCols(TableName.SecretV2))
|
||||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
.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("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
);
|
)
|
||||||
|
.select(db.ref("projectId").withSchema(TableName.Environment).as("projectId"));
|
||||||
|
|
||||||
const docs = sqlNestRelationships({
|
const docs = sqlNestRelationships({
|
||||||
data: rawDocs,
|
data: rawDocs,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "tagId",
|
key: "tagId",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -62,6 +63,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor,
|
||||||
tx
|
tx
|
||||||
}: TFnSecretBulkInsert) => {
|
}: TFnSecretBulkInsert) => {
|
||||||
const sanitizedInputSecrets = inputSecrets.map(
|
const sanitizedInputSecrets = inputSecrets.map(
|
||||||
@@ -90,6 +92,10 @@ export const fnSecretBulkInsert = async ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
|
||||||
|
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
|
||||||
|
const actorType = actor?.type || ActorType.PLATFORM;
|
||||||
|
|
||||||
const newSecrets = await secretDAL.insertMany(
|
const newSecrets = await secretDAL.insertMany(
|
||||||
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
||||||
tx
|
tx
|
||||||
@@ -106,6 +112,9 @@ export const fnSecretBulkInsert = async ({
|
|||||||
sanitizedInputSecrets.map((el) => ({
|
sanitizedInputSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
folderId,
|
folderId,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType,
|
||||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
@@ -157,8 +166,13 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
actor
|
||||||
}: TFnSecretBulkUpdate) => {
|
}: TFnSecretBulkUpdate) => {
|
||||||
|
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
|
||||||
|
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
|
||||||
|
const actorType = actor?.type || ActorType.PLATFORM;
|
||||||
|
|
||||||
const sanitizedInputSecrets = inputSecrets.map(
|
const sanitizedInputSecrets = inputSecrets.map(
|
||||||
({
|
({
|
||||||
filter,
|
filter,
|
||||||
@@ -216,7 +230,10 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
encryptedValue,
|
encryptedValue,
|
||||||
reminderRepeatDays,
|
reminderRepeatDays,
|
||||||
folderId,
|
folderId,
|
||||||
secretId
|
secretId,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
tx
|
tx
|
||||||
@@ -616,6 +633,12 @@ export const reshapeBridgeSecret = (
|
|||||||
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
||||||
value: string;
|
value: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
userActorName?: string | null;
|
||||||
|
identityActorName?: string | null;
|
||||||
|
userActorId?: string | null;
|
||||||
|
identityActorId?: string | null;
|
||||||
|
membershipId?: string | null;
|
||||||
|
actorType?: string | null;
|
||||||
tags?: {
|
tags?: {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -636,6 +659,14 @@ export const reshapeBridgeSecret = (
|
|||||||
_id: secret.id,
|
_id: secret.id,
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
user: secret.userId,
|
user: secret.userId,
|
||||||
|
actor: secret.actorType
|
||||||
|
? {
|
||||||
|
actorType: secret.actorType,
|
||||||
|
actorId: secret.userActorId || secret.identityActorId,
|
||||||
|
name: secret.identityActorName || secret.userActorName,
|
||||||
|
membershipId: secret.membershipId
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
tags: secret.tags,
|
tags: secret.tags,
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
secretReminderRepeatDays: secret.reminderRepeatDays,
|
secretReminderRepeatDays: secret.reminderRepeatDays,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { KmsDataKey } from "../kms/kms-types";
|
|||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
|
import { TGetASecretByIdDTO } from "../secret/secret-types";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||||
@@ -73,7 +74,13 @@ type TSecretV2BridgeServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||||
folderDAL: Pick<
|
folderDAL: Pick<
|
||||||
TSecretFolderDALFactory,
|
TSecretFolderDALFactory,
|
||||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
|
| "findBySecretPath"
|
||||||
|
| "updateById"
|
||||||
|
| "findById"
|
||||||
|
| "findByManySecretPath"
|
||||||
|
| "find"
|
||||||
|
| "findBySecretPathMultiEnv"
|
||||||
|
| "findSecretPathByFolderIds"
|
||||||
>;
|
>;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
||||||
@@ -301,6 +308,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -483,6 +494,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -947,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 ({
|
const getSecretByName = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -1230,6 +1312,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1490,6 +1576,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||||
@@ -1522,6 +1612,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||||
@@ -1689,14 +1783,19 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
});
|
});
|
||||||
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
|
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
||||||
return secretVersions.map((el) =>
|
offset,
|
||||||
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
limit,
|
||||||
|
sort: [["createdAt", "desc"]]
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretVersions.map((el) => {
|
||||||
|
return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||||
...el,
|
...el,
|
||||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is a backfilling API for secret references
|
// this is a backfilling API for secret references
|
||||||
@@ -1956,6 +2055,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
type: doc.type,
|
type: doc.type,
|
||||||
@@ -1982,6 +2085,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
filter: {
|
filter: {
|
||||||
@@ -2204,6 +2311,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
getSecretsCountMultiEnv,
|
getSecretsCountMultiEnv,
|
||||||
getSecretsMultiEnv,
|
getSecretsMultiEnv,
|
||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsByFolderMappings
|
getSecretsByFolderMappings,
|
||||||
|
getSecretById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -168,6 +168,10 @@ export type TFnSecretBulkInsert = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
actor?: {
|
||||||
|
type: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type TRequireReferenceIfValue =
|
type TRequireReferenceIfValue =
|
||||||
@@ -192,6 +196,10 @@ export type TFnSecretBulkUpdate = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
actor?: {
|
||||||
|
type: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
|
|
||||||
@@ -119,11 +120,67 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findVersionsBySecretIdWithActors = async (
|
||||||
|
secretId: string,
|
||||||
|
projectId: string,
|
||||||
|
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.SecretVersionV2)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectMembership,
|
||||||
|
`${TableName.ProjectMembership}.userId`,
|
||||||
|
`${TableName.SecretVersionV2}.userActorId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
||||||
|
.where((qb) => {
|
||||||
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
|
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||||
|
})
|
||||||
|
.orWhere((qb) => {
|
||||||
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
|
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretVersionV2),
|
||||||
|
`${TableName.Users}.username as userActorName`,
|
||||||
|
`${TableName.Identity}.name as identityActorName`,
|
||||||
|
`${TableName.ProjectMembership}.id as membershipId`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(
|
||||||
|
sort.map(([column, order, nulls]) => ({
|
||||||
|
column: `${TableName.SecretVersionV2}.${column as string}`,
|
||||||
|
order,
|
||||||
|
nulls
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs: Array<
|
||||||
|
TSecretVersionsV2 & {
|
||||||
|
userActorName: string | undefined | null;
|
||||||
|
identityActorName: string | undefined | null;
|
||||||
|
membershipId: string | undefined | null;
|
||||||
|
}
|
||||||
|
> = await query;
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretVersionV2Orm,
|
...secretVersionV2Orm,
|
||||||
pruneExcessVersions,
|
pruneExcessVersions,
|
||||||
findLatestVersionMany,
|
findLatestVersionMany,
|
||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
findLatestVersionByFolderId
|
findLatestVersionByFolderId,
|
||||||
|
findVersionsBySecretIdWithActors
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -579,6 +579,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
|
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const secretVersions = await secretVersionDAL.insertMany(
|
const secretVersions = await secretVersionDAL.insertMany(
|
||||||
sanitizedInputSecrets.map((el) => ({
|
sanitizedInputSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||||
|
import { WebhookEvents } from "../webhook/webhook-types";
|
||||||
import { TSecretDALFactory } from "./secret-dal";
|
import { TSecretDALFactory } from "./secret-dal";
|
||||||
import { interpolateSecrets } from "./secret-fns";
|
import { interpolateSecrets } from "./secret-fns";
|
||||||
import {
|
import {
|
||||||
@@ -623,7 +624,14 @@ export const secretQueueFactory = ({
|
|||||||
await queueService.queue(
|
await queueService.queue(
|
||||||
QueueName.SecretWebhook,
|
QueueName.SecretWebhook,
|
||||||
QueueJobs.SecWebhook,
|
QueueJobs.SecWebhook,
|
||||||
{ environment, projectId, secretPath },
|
{
|
||||||
|
type: WebhookEvents.SecretModified,
|
||||||
|
payload: {
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
secretPath
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
|
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
|
||||||
removeOnFail: { count: 5 },
|
removeOnFail: { count: 5 },
|
||||||
@@ -1055,6 +1063,8 @@ export const secretQueueFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgByProjectId(projectId);
|
const organization = await orgDAL.findOrgByProjectId(projectId);
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
|
const secret = await secretV2BridgeDAL.findById(data.secretId);
|
||||||
|
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
|
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
|
||||||
@@ -1083,6 +1093,19 @@ export const secretQueueFactory = ({
|
|||||||
organizationName: organization.name
|
organizationName: organization.name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
|
||||||
|
type: WebhookEvents.SecretReminderExpired,
|
||||||
|
payload: {
|
||||||
|
projectName: project.name,
|
||||||
|
projectId: project.id,
|
||||||
|
secretPath: folder?.path,
|
||||||
|
environment: folder?.environmentSlug || "",
|
||||||
|
reminderNote: data.note,
|
||||||
|
secretName: secret?.key,
|
||||||
|
secretId: data.secretId
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const startSecretV2Migration = async (projectId: string) => {
|
const startSecretV2Migration = async (projectId: string) => {
|
||||||
@@ -1490,14 +1513,17 @@ export const secretQueueFactory = ({
|
|||||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId: job.data.projectId
|
projectId: job.data.payload.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
await fnTriggerWebhook({
|
await fnTriggerWebhook({
|
||||||
...job.data,
|
projectId: job.data.payload.projectId,
|
||||||
|
environment: job.data.payload.environment,
|
||||||
|
secretPath: job.data.payload.secretPath || "/",
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
webhookDAL,
|
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
webhookDAL,
|
||||||
|
event: job.data,
|
||||||
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
|
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
TDeleteManySecretRawDTO,
|
TDeleteManySecretRawDTO,
|
||||||
TDeleteSecretDTO,
|
TDeleteSecretDTO,
|
||||||
TDeleteSecretRawDTO,
|
TDeleteSecretRawDTO,
|
||||||
|
TGetASecretByIdRawDTO,
|
||||||
TGetASecretDTO,
|
TGetASecretDTO,
|
||||||
TGetASecretRawDTO,
|
TGetASecretRawDTO,
|
||||||
TGetSecretAccessListDTO,
|
TGetSecretAccessListDTO,
|
||||||
@@ -95,7 +96,7 @@ type TSecretServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
folderDAL: Pick<
|
folderDAL: Pick<
|
||||||
TSecretFolderDALFactory,
|
TSecretFolderDALFactory,
|
||||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
|
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds"
|
||||||
>;
|
>;
|
||||||
secretV2BridgeService: TSecretV2BridgeServiceFactory;
|
secretV2BridgeService: TSecretV2BridgeServiceFactory;
|
||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
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 ({
|
const getSecretByNameRaw = async ({
|
||||||
type,
|
type,
|
||||||
path,
|
path,
|
||||||
@@ -3088,6 +3101,7 @@ export const secretServiceFactory = ({
|
|||||||
getSecretsRawMultiEnv,
|
getSecretsRawMultiEnv,
|
||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsRawByFolderMappings,
|
getSecretsRawByFolderMappings,
|
||||||
getSecretAccessList
|
getSecretAccessList,
|
||||||
|
getSecretByIdRaw
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ export type TGetASecretDTO = {
|
|||||||
version?: number;
|
version?: number;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetASecretByIdDTO = {
|
||||||
|
secretId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateBulkSecretDTO = {
|
export type TCreateBulkSecretDTO = {
|
||||||
path: string;
|
path: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -213,6 +217,10 @@ export type TGetASecretRawDTO = {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetASecretByIdRawDTO = {
|
||||||
|
secretId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateSecretRawDTO = TProjectPermission & {
|
export type TCreateSecretRawDTO = TProjectPermission & {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const buildSlackPayload = (notification: TSlackNotification) => {
|
|||||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||||
*Environment*: ${payload.environment}
|
*Environment*: ${payload.environment}
|
||||||
*Secret path*: ${payload.secretPath || "/"}
|
*Secret path*: ${payload.secretPath || "/"}
|
||||||
|
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}
|
||||||
|
|
||||||
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
|
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
|
||||||
payload.requestId
|
payload.requestId
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export type TSlackNotification =
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
secretKeys: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TWebhookDALFactory } from "./webhook-dal";
|
import { TWebhookDALFactory } from "./webhook-dal";
|
||||||
import { WebhookType } from "./webhook-types";
|
import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
|
||||||
|
|
||||||
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
||||||
|
|
||||||
@@ -54,29 +54,64 @@ export const triggerWebhookRequest = async (
|
|||||||
return req;
|
return req;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWebhookPayload = (
|
export const getWebhookPayload = (event: TWebhookPayloads) => {
|
||||||
eventName: string,
|
if (event.type === WebhookEvents.SecretModified) {
|
||||||
details: {
|
const { projectName, projectId, environment, secretPath, type } = event.payload;
|
||||||
workspaceName: string;
|
|
||||||
workspaceId: string;
|
switch (type) {
|
||||||
environment: string;
|
case WebhookType.SLACK:
|
||||||
secretPath?: string;
|
return {
|
||||||
type?: string | null;
|
text: "A secret value has been added or modified.",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
color: "#E7F256",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
title: "Project",
|
||||||
|
value: projectName,
|
||||||
|
short: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
value: environment,
|
||||||
|
short: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Secret Path",
|
||||||
|
value: secretPath,
|
||||||
|
short: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
case WebhookType.GENERAL:
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
event: event.type,
|
||||||
|
project: {
|
||||||
|
workspaceId: projectId,
|
||||||
|
projectName,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) => {
|
|
||||||
const { workspaceName, workspaceId, environment, secretPath, type } = details;
|
const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case WebhookType.SLACK:
|
case WebhookType.SLACK:
|
||||||
return {
|
return {
|
||||||
text: "A secret value has been added or modified.",
|
text: "You have a secret reminder",
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
color: "#E7F256",
|
color: "#E7F256",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
title: "Project",
|
title: "Project",
|
||||||
value: workspaceName,
|
value: projectName,
|
||||||
short: false
|
short: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -88,6 +123,16 @@ export const getWebhookPayload = (
|
|||||||
title: "Secret Path",
|
title: "Secret Path",
|
||||||
value: secretPath,
|
value: secretPath,
|
||||||
short: false
|
short: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Secret Name",
|
||||||
|
value: secretName,
|
||||||
|
short: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Reminder Note",
|
||||||
|
value: reminderNote,
|
||||||
|
short: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -96,11 +141,14 @@ export const getWebhookPayload = (
|
|||||||
case WebhookType.GENERAL:
|
case WebhookType.GENERAL:
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
event: eventName,
|
event: event.type,
|
||||||
project: {
|
project: {
|
||||||
workspaceId,
|
workspaceId: projectId,
|
||||||
|
projectName,
|
||||||
environment,
|
environment,
|
||||||
secretPath
|
secretPath,
|
||||||
|
secretName,
|
||||||
|
reminderNote
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
event: TWebhookPayloads;
|
||||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
@@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({
|
|||||||
projectId,
|
projectId,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL,
|
event,
|
||||||
secretManagerDecryptor
|
secretManagerDecryptor,
|
||||||
|
projectDAL
|
||||||
}: TFnTriggerWebhookDTO) => {
|
}: TFnTriggerWebhookDTO) => {
|
||||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||||
const toBeTriggeredHooks = webhooks.filter(
|
const toBeTriggeredHooks = webhooks.filter(
|
||||||
@@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({
|
|||||||
);
|
);
|
||||||
if (!toBeTriggeredHooks.length) return;
|
if (!toBeTriggeredHooks.length) return;
|
||||||
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
|
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
|
||||||
const project = await projectDAL.findById(projectId);
|
let { projectName } = event.payload;
|
||||||
|
if (!projectName) {
|
||||||
|
const project = await projectDAL.findById(event.payload.projectId);
|
||||||
|
projectName = project.name;
|
||||||
|
}
|
||||||
|
|
||||||
const webhooksTriggered = await Promise.allSettled(
|
const webhooksTriggered = await Promise.allSettled(
|
||||||
toBeTriggeredHooks.map((hook) =>
|
toBeTriggeredHooks.map((hook) => {
|
||||||
triggerWebhookRequest(
|
const formattedEvent = {
|
||||||
hook,
|
type: event.type,
|
||||||
secretManagerDecryptor,
|
payload: { ...event.payload, type: hook.type, projectName }
|
||||||
getWebhookPayload("secrets.modified", {
|
} as TWebhookPayloads;
|
||||||
workspaceName: project.name,
|
return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
|
||||||
workspaceId: projectId,
|
})
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
type: hook.type
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// filter hooks by status
|
// filter hooks by status
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
TDeleteWebhookDTO,
|
TDeleteWebhookDTO,
|
||||||
TListWebhookDTO,
|
TListWebhookDTO,
|
||||||
TTestWebhookDTO,
|
TTestWebhookDTO,
|
||||||
TUpdateWebhookDTO
|
TUpdateWebhookDTO,
|
||||||
|
WebhookEvents
|
||||||
} from "./webhook-types";
|
} from "./webhook-types";
|
||||||
|
|
||||||
type TWebhookServiceFactoryDep = {
|
type TWebhookServiceFactoryDep = {
|
||||||
@@ -144,12 +145,15 @@ export const webhookServiceFactory = ({
|
|||||||
await triggerWebhookRequest(
|
await triggerWebhookRequest(
|
||||||
webhook,
|
webhook,
|
||||||
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
|
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
|
||||||
getWebhookPayload("test", {
|
getWebhookPayload({
|
||||||
workspaceName: project.name,
|
type: "test" as WebhookEvents.SecretModified,
|
||||||
workspaceId: webhook.projectId,
|
payload: {
|
||||||
environment: webhook.environment.slug,
|
projectName: project.name,
|
||||||
secretPath: webhook.secretPath,
|
projectId: webhook.projectId,
|
||||||
type: webhook.type
|
environment: webhook.environment.slug,
|
||||||
|
secretPath: webhook.secretPath,
|
||||||
|
type: webhook.type
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -30,3 +30,36 @@ export enum WebhookType {
|
|||||||
GENERAL = "general",
|
GENERAL = "general",
|
||||||
SLACK = "slack"
|
SLACK = "slack"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WebhookEvents {
|
||||||
|
SecretModified = "secrets.modified",
|
||||||
|
SecretReminderExpired = "secrets.reminder-expired",
|
||||||
|
TestEvent = "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
type TWebhookSecretModifiedEventPayload = {
|
||||||
|
type: WebhookEvents.SecretModified;
|
||||||
|
payload: {
|
||||||
|
projectName?: string;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath?: string;
|
||||||
|
type?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TWebhookSecretReminderEventPayload = {
|
||||||
|
type: WebhookEvents.SecretReminderExpired;
|
||||||
|
payload: {
|
||||||
|
projectName?: string;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath?: string;
|
||||||
|
type?: string | null;
|
||||||
|
secretName: string;
|
||||||
|
secretId: string;
|
||||||
|
reminderNote?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
public_ip: 127.0.0.1
|
||||||
|
auth_secret: example-auth-secret
|
||||||
|
realm: infisical.org
|
||||||
|
# set port 5349 for tls
|
||||||
|
# port: 5349
|
||||||
|
# tls_private_key_path: /full-path
|
||||||
|
# tls_ca_path: /full-path
|
||||||
|
# tls_cert_path: /full-path
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
public_ip: 127.0.0.1
|
public_ip: 127.0.0.1
|
||||||
auth_secret: changeThisOnProduction
|
auth_secret: changeThisOnProduction
|
||||||
realm: infisical.org
|
realm: infisical.org
|
||||||
|
# set port 5349 for tls
|
||||||
|
# port: 5349
|
||||||
|
# tls_private_key_path: /full-path
|
||||||
|
# tls_ca_path: /full-path
|
||||||
|
# tls_cert_path: /full-path
|
||||||
|
|||||||
+4
-2
@@ -28,8 +28,9 @@ require (
|
|||||||
github.com/rs/zerolog v1.26.1
|
github.com/rs/zerolog v1.26.1
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/spf13/viper v1.8.1
|
github.com/spf13/viper v1.8.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/crypto v0.35.0
|
golang.org/x/crypto v0.35.0
|
||||||
|
golang.org/x/sys v0.30.0
|
||||||
golang.org/x/term v0.29.0
|
golang.org/x/term v0.29.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
@@ -115,7 +116,6 @@ require (
|
|||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/oauth2 v0.21.0 // indirect
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/time v0.6.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
golang.org/x/tools v0.30.0 // indirect
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
@@ -139,3 +139,5 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2
|
replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2
|
||||||
|
|
||||||
|
replace github.com/pion/turn/v4 => github.com/Infisical/turn/v4 v4.0.1
|
||||||
|
|||||||
+4
-4
@@ -49,6 +49,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8=
|
github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8=
|
||||||
github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto=
|
github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto=
|
||||||
|
github.com/Infisical/turn/v4 v4.0.1 h1:omdelNsnFfzS5cu86W5OBR68by68a8sva4ogR0lQQnw=
|
||||||
|
github.com/Infisical/turn/v4 v4.0.1/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
@@ -365,8 +367,6 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
|||||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
|
||||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -425,8 +425,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||||
|
|||||||
@@ -137,15 +137,10 @@ var gatewayRelayCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gatewayCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
|
|
||||||
command.Flags().MarkHidden("domain")
|
|
||||||
command.Parent().HelpFunc()(command, strings)
|
|
||||||
})
|
|
||||||
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||||
|
|
||||||
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
|
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
|
||||||
|
|
||||||
gatewayCmd.AddCommand(gatewayRelayCmd)
|
gatewayCmd.AddCommand(gatewayRelayCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(gatewayCmd)
|
rootCmd.AddCommand(gatewayCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
|||||||
|
|
||||||
switch string(cmd) {
|
switch string(cmd) {
|
||||||
case "FORWARD-TCP":
|
case "FORWARD-TCP":
|
||||||
log.Info().Msg("Starting secure connector proxy...")
|
|
||||||
proxyAddress := string(bytes.Split(args, []byte(" "))[0])
|
proxyAddress := string(bytes.Split(args, []byte(" "))[0])
|
||||||
destTarget, err := net.Dial("tcp", proxyAddress)
|
destTarget, err := net.Dial("tcp", proxyAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -69,6 +68,7 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer destTarget.Close()
|
defer destTarget.Close()
|
||||||
|
log.Info().Msgf("Starting secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String())
|
||||||
|
|
||||||
// Handle buffered data
|
// Handle buffered data
|
||||||
buffered := reader.Buffered()
|
buffered := reader.Buffered()
|
||||||
@@ -87,6 +87,7 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CopyDataFromQuicToTcp(stream, destTarget)
|
CopyDataFromQuicToTcp(stream, destTarget)
|
||||||
|
log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String())
|
||||||
return
|
return
|
||||||
case "PING":
|
case "PING":
|
||||||
if _, err := stream.Write([]byte("PONG\n")); err != nil {
|
if _, err := stream.Write([]byte("PONG\n")); err != nil {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/api"
|
"github.com/Infisical/infisical-merge/packages/api"
|
||||||
|
"github.com/Infisical/infisical-merge/packages/systemd"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/pion/logging"
|
"github.com/pion/logging"
|
||||||
"github.com/pion/turn/v4"
|
"github.com/pion/turn/v4"
|
||||||
@@ -75,6 +77,10 @@ func (g *Gateway) ConnectWithRelay() error {
|
|||||||
|
|
||||||
// Start a new TURN Client and wrap our net.Conn in a STUNConn
|
// Start a new TURN Client and wrap our net.Conn in a STUNConn
|
||||||
// This allows us to simulate datagram based communication over a net.Conn
|
// This allows us to simulate datagram based communication over a net.Conn
|
||||||
|
logger := logging.NewDefaultLoggerFactory()
|
||||||
|
if os.Getenv("LOG_LEVEL") == "debug" {
|
||||||
|
logger.DefaultLogLevel = logging.LogLevelDebug
|
||||||
|
}
|
||||||
cfg := &turn.ClientConfig{
|
cfg := &turn.ClientConfig{
|
||||||
STUNServerAddr: relayDetails.TurnServerAddress,
|
STUNServerAddr: relayDetails.TurnServerAddress,
|
||||||
TURNServerAddr: relayDetails.TurnServerAddress,
|
TURNServerAddr: relayDetails.TurnServerAddress,
|
||||||
@@ -82,7 +88,7 @@ func (g *Gateway) ConnectWithRelay() error {
|
|||||||
Username: relayDetails.TurnServerUsername,
|
Username: relayDetails.TurnServerUsername,
|
||||||
Password: relayDetails.TurnServerPassword,
|
Password: relayDetails.TurnServerPassword,
|
||||||
Realm: relayDetails.TurnServerRealm,
|
Realm: relayDetails.TurnServerRealm,
|
||||||
LoggerFactory: logging.NewDefaultLoggerFactory(),
|
LoggerFactory: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := turn.NewClient(cfg)
|
client, err := turn.NewClient(cfg)
|
||||||
@@ -96,10 +102,6 @@ func (g *Gateway) ConnectWithRelay() error {
|
|||||||
TurnServerAddress: relayDetails.TurnServerAddress,
|
TurnServerAddress: relayDetails.TurnServerAddress,
|
||||||
InfisicalStaticIp: relayDetails.InfisicalStaticIp,
|
InfisicalStaticIp: relayDetails.InfisicalStaticIp,
|
||||||
}
|
}
|
||||||
// if port not specific allow all port
|
|
||||||
if relayDetails.InfisicalStaticIp != "" && !strings.Contains(relayDetails.InfisicalStaticIp, ":") {
|
|
||||||
g.config.InfisicalStaticIp = g.config.InfisicalStaticIp + ":0"
|
|
||||||
}
|
|
||||||
|
|
||||||
g.client = client
|
g.client = client
|
||||||
return nil
|
return nil
|
||||||
@@ -144,7 +146,10 @@ func (g *Gateway) Listen(ctx context.Context) error {
|
|||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
shutdownCh := make(chan bool, 1)
|
shutdownCh := make(chan bool, 1)
|
||||||
|
|
||||||
g.registerPermissionRefresh(ctx, errCh)
|
if err = g.createPermissionForStaticIps(g.config.InfisicalStaticIp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
g.registerHeartBeat(ctx, errCh)
|
g.registerHeartBeat(ctx, errCh)
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey))
|
cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey))
|
||||||
@@ -171,8 +176,7 @@ func (g *Gateway) Listen(ctx context.Context) error {
|
|||||||
KeepAlivePeriod: 2 * time.Second,
|
KeepAlivePeriod: 2 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
g.registerRelayIsActive(ctx, relayUdpConnection.LocalAddr().String(), tlsConfig, quicConfig, errCh)
|
g.registerRelayIsActive(ctx, errCh)
|
||||||
|
|
||||||
quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig)
|
quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to listen for QUIC: %w", err)
|
return fmt.Errorf("Failed to listen for QUIC: %w", err)
|
||||||
@@ -234,6 +238,8 @@ func (g *Gateway) Listen(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// make this compatiable with systemd notify mode
|
||||||
|
systemd.SdNotify(false, systemd.SdNotifyReady)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Info().Msg("Shutting down gateway...")
|
log.Info().Msg("Shutting down gateway...")
|
||||||
@@ -282,90 +288,86 @@ func (g *Gateway) registerHeartBeat(ctx context.Context, errCh chan error) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Gateway) registerRelayIsActive(ctx context.Context, serverAddr string, tlsConf *tls.Config, quicConf *quic.Config, errCh chan error) {
|
func (g *Gateway) createPermissionForStaticIps(staticIps string) error {
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
if staticIps == "" {
|
||||||
|
return fmt.Errorf("Missing Infisical static ips for permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
splittedIps := strings.Split(staticIps, ",")
|
||||||
|
resolvedIps := make([]net.Addr, 0)
|
||||||
|
for _, ip := range splittedIps {
|
||||||
|
ip = strings.TrimSpace(ip)
|
||||||
|
if ip == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if port not specific allow all port
|
||||||
|
if !strings.Contains(ip, ":") {
|
||||||
|
ip = ip + ":0"
|
||||||
|
}
|
||||||
|
|
||||||
|
peerAddr, err := net.ResolveUDPAddr("udp", ip)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to resolve static ip for permission: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedIps = append(resolvedIps, peerAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.client.CreatePermission(resolvedIps...); err != nil {
|
||||||
|
return fmt.Errorf("Failed to set ip permission: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error {
|
||||||
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
maxFailures := 3
|
maxFailures := 3
|
||||||
failures := 0
|
failures := 0
|
||||||
|
|
||||||
|
log.Info().Msg("Starting relay connection health check")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
log.Info().Msg("Stopping relay connection health check")
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
conn, err := quic.DialAddr(ctx, serverAddr, tlsConf, quicConf)
|
func() {
|
||||||
if conn != nil {
|
log.Debug().Msg("Performing relay connection health check")
|
||||||
failures = 0
|
|
||||||
conn.CloseWithError(0, "connection closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && !strings.Contains(err.Error(), "tls: failed to verify certificate") {
|
if g.client == nil {
|
||||||
failures++
|
failures++
|
||||||
log.Warn().Err(err).Int("failures", failures).Msg("Relay connection check failed")
|
log.Warn().Int("failures", failures).Msg("TURN client is nil")
|
||||||
|
if failures >= maxFailures {
|
||||||
if failures >= maxFailures {
|
errCh <- fmt.Errorf("relay connection check failed: TURN client is nil")
|
||||||
errCh <- fmt.Errorf("relay connection check failed: %w", err)
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// we try to refresh permissions - this is a lightweight operation
|
||||||
|
// that will fail immediately if the UDP connection is broken. good for health check
|
||||||
|
log.Debug().Msg("Refreshing TURN permissions to verify connection")
|
||||||
|
if err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp); err != nil {
|
||||||
|
failures++
|
||||||
|
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
|
||||||
|
if failures >= maxFailures {
|
||||||
|
errCh <- fmt.Errorf("relay connection check failed: %w", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Successfully refreshed TURN permissions - connection is healthy")
|
||||||
|
if failures > 0 {
|
||||||
|
log.Info().Int("previous_failures", failures).Msg("Relay connection restored")
|
||||||
|
failures = 0
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Gateway) registerPermissionRefresh(ctx context.Context, errCh chan error) {
|
return nil
|
||||||
if g.config.InfisicalStaticIp == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("Starting TURN permission refresh routine")
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
g.refreshPermission(errCh)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.Info().Msg("Context cancelled, stopping TURN permission refresh")
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
g.refreshPermission(errCh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Gateway) refreshPermission(errCh chan error) {
|
|
||||||
log.Info().Msg("Attempting to refresh TURN permission")
|
|
||||||
maxRetries := 3
|
|
||||||
retryDelay := 5 * time.Second
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for i := 0; i < maxRetries; i++ {
|
|
||||||
peerAddr, err := net.ResolveUDPAddr("udp", g.config.InfisicalStaticIp)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to resolve static IP for permission refresh")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := g.client.CreatePermission(peerAddr); err != nil {
|
|
||||||
lastErr = err
|
|
||||||
log.Warn().Err(err).Int("attempt", i+1).Msg("Failed to refresh TURN permission, retrying...")
|
|
||||||
time.Sleep(retryDelay)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("Successfully refreshed TURN permission")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastErr != nil {
|
|
||||||
log.Error().Err(lastErr).Msg("Failed to refresh TURN permission after retries")
|
|
||||||
if reconnectErr := g.ConnectWithRelay(); reconnectErr != nil {
|
|
||||||
errCh <- fmt.Errorf("failed to refresh permissions and reconnect: %w", reconnectErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package gateway
|
package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -13,6 +17,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
udplistener "github.com/Infisical/infisical-merge/packages/gateway/udp_listener"
|
udplistener "github.com/Infisical/infisical-merge/packages/gateway/udp_listener"
|
||||||
|
"github.com/Infisical/infisical-merge/packages/systemd"
|
||||||
"github.com/pion/logging"
|
"github.com/pion/logging"
|
||||||
"github.com/pion/turn/v4"
|
"github.com/pion/turn/v4"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -36,8 +41,10 @@ type GatewayRelayConfig struct {
|
|||||||
RelayMaxPort uint16 `yaml:"relay_max_port"`
|
RelayMaxPort uint16 `yaml:"relay_max_port"`
|
||||||
TlsCertPath string `yaml:"tls_cert_path"`
|
TlsCertPath string `yaml:"tls_cert_path"`
|
||||||
TlsPrivateKeyPath string `yaml:"tls_private_key_path"`
|
TlsPrivateKeyPath string `yaml:"tls_private_key_path"`
|
||||||
|
TlsCaPath string `yaml:"tls_ca_path"`
|
||||||
|
|
||||||
tls tls.Certificate
|
tls tls.Certificate
|
||||||
|
tlsCa string
|
||||||
isTlsEnabled bool
|
isTlsEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,19 +85,19 @@ func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) {
|
|||||||
return nil, errMissingTlsCert
|
return nil, errMissingTlsCert
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsCertFile, err := os.ReadFile(cfg.TlsCertPath)
|
cert, err := tls.LoadX509KeyPair(cfg.TlsCertPath, cfg.TlsPrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("Failed to read load server tls key pair: %w", err)
|
||||||
}
|
|
||||||
tlsPrivateKeyFile, err := os.ReadFile(cfg.TlsPrivateKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(string(tlsCertFile), string(tlsPrivateKeyFile))
|
if cfg.TlsCaPath != "" {
|
||||||
if err != nil {
|
ca, err := os.ReadFile(cfg.TlsCaPath)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to read tls ca: %w", err)
|
||||||
|
}
|
||||||
|
cfg.tlsCa = string(ca)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.tls = cert
|
cfg.tls = cert
|
||||||
cfg.isTlsEnabled = true
|
cfg.isTlsEnabled = true
|
||||||
}
|
}
|
||||||
@@ -139,8 +146,12 @@ func (g *GatewayRelay) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if g.Config.isTlsEnabled {
|
if g.Config.isTlsEnabled {
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM([]byte(g.Config.tlsCa))
|
||||||
|
|
||||||
listenerConfigs[i].Listener = tls.NewListener(conn, &tls.Config{
|
listenerConfigs[i].Listener = tls.NewListener(conn, &tls.Config{
|
||||||
Certificates: []tls.Certificate{g.Config.tls},
|
Certificates: []tls.Certificate{g.Config.tls},
|
||||||
|
ClientCAs: caCertPool,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
listenerConfigs[i].Listener = conn
|
listenerConfigs[i].Listener = conn
|
||||||
@@ -164,6 +175,9 @@ func (g *GatewayRelay) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msgf("Relay listening on %s\n", connAddress)
|
log.Info().Msgf("Relay listening on %s\n", connAddress)
|
||||||
|
|
||||||
|
// make this compatiable with systemd notify mode
|
||||||
|
systemd.SdNotify(false, systemd.SdNotifyReady)
|
||||||
// Block until user sends SIGINT or SIGTERM
|
// Block until user sends SIGINT or SIGTERM
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingTlsCert = errors.New("Missing TLS files")
|
||||||
|
errWindowsNotSupported = errors.New("Relay is not supported on Windows")
|
||||||
|
)
|
||||||
|
|
||||||
|
type GatewayRelay struct {
|
||||||
|
Config *GatewayRelayConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type GatewayRelayConfig struct {
|
||||||
|
PublicIP string
|
||||||
|
Port int
|
||||||
|
Realm string
|
||||||
|
AuthSecret string
|
||||||
|
RelayMinPort uint16
|
||||||
|
RelayMaxPort uint16
|
||||||
|
TlsCertPath string
|
||||||
|
TlsPrivateKeyPath string
|
||||||
|
TlsCaPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) {
|
||||||
|
return nil, errWindowsNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GatewayRelay) Run() error {
|
||||||
|
return errWindowsNotSupported
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2014 Docker, Inc.
|
||||||
|
// Copyright 2015-2018 CoreOS, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Package daemon provides a Go implementation of the sd_notify protocol.
|
||||||
|
// It can be used to inform systemd of service start-up completion, watchdog
|
||||||
|
// events, and other status changes.
|
||||||
|
//
|
||||||
|
// https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description
|
||||||
|
package systemd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SdNotifyReady tells the service manager that service startup is finished
|
||||||
|
// or the service finished loading its configuration.
|
||||||
|
SdNotifyReady = "READY=1"
|
||||||
|
|
||||||
|
// SdNotifyStopping tells the service manager that the service is beginning
|
||||||
|
// its shutdown.
|
||||||
|
SdNotifyStopping = "STOPPING=1"
|
||||||
|
|
||||||
|
// SdNotifyReloading tells the service manager that this service is
|
||||||
|
// reloading its configuration. Note that you must call SdNotifyReady when
|
||||||
|
// it completed reloading.
|
||||||
|
SdNotifyReloading = "RELOADING=1"
|
||||||
|
|
||||||
|
// SdNotifyWatchdog tells the service manager to update the watchdog
|
||||||
|
// timestamp for the service.
|
||||||
|
SdNotifyWatchdog = "WATCHDOG=1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SdNotify sends a message to the init daemon. It is common to ignore the error.
|
||||||
|
// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET`
|
||||||
|
// will be unconditionally unset.
|
||||||
|
//
|
||||||
|
// It returns one of the following:
|
||||||
|
// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset)
|
||||||
|
// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data)
|
||||||
|
// (true, nil) - notification supported, data has been sent
|
||||||
|
func SdNotify(unsetEnvironment bool, state string) (bool, error) {
|
||||||
|
socketAddr := &net.UnixAddr{
|
||||||
|
Name: os.Getenv("NOTIFY_SOCKET"),
|
||||||
|
Net: "unixgram",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTIFY_SOCKET not set
|
||||||
|
if socketAddr.Name == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsetEnvironment {
|
||||||
|
if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
|
||||||
|
// Error connecting to NOTIFY_SOCKET
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if _, err = conn.Write([]byte(state)); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -36,3 +36,18 @@ If the signature in the header matches the signature that you generated, then yo
|
|||||||
"timestamp": ""
|
"timestamp": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "secrets.reminder-expired",
|
||||||
|
"project": {
|
||||||
|
"workspaceId": "the workspace id",
|
||||||
|
"environment": "project environment",
|
||||||
|
"secretPath": "project folder path",
|
||||||
|
"secretName": "name of the secret",
|
||||||
|
"secretId": "id of the secret",
|
||||||
|
"reminderNote": "reminder note of the secret"
|
||||||
|
},
|
||||||
|
"timestamp": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
"OIDC group membership mapping assigned user to groups",
|
"OIDC group membership mapping assigned user to groups",
|
||||||
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
|
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
|
||||||
"OIDC group membership mapping removed user from groups",
|
"OIDC group membership mapping removed user from groups",
|
||||||
|
[EventType.SECRET_APPROVAL_REQUEST_REVIEW]: "Review Secret Approval Request",
|
||||||
[EventType.CREATE_KMIP_CLIENT]: "Create KMIP client",
|
[EventType.CREATE_KMIP_CLIENT]: "Create KMIP client",
|
||||||
[EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client",
|
[EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client",
|
||||||
[EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client",
|
[EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client",
|
||||||
|
|||||||
@@ -150,5 +150,6 @@ export enum EventType {
|
|||||||
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
||||||
|
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ export const useUpdateSecretApprovalReviewStatus = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, TUpdateSecretApprovalReviewStatusDTO>({
|
return useMutation<object, object, TUpdateSecretApprovalReviewStatusDTO>({
|
||||||
mutationFn: async ({ id, status }) => {
|
mutationFn: async ({ id, status, comment }) => {
|
||||||
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, {
|
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, {
|
||||||
status
|
status,
|
||||||
|
comment
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export type TSecretApprovalRequest = {
|
|||||||
reviewers: {
|
reviewers: {
|
||||||
userId: string;
|
userId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
@@ -114,6 +115,7 @@ export type TGetSecretApprovalRequestDetails = {
|
|||||||
|
|
||||||
export type TUpdateSecretApprovalReviewStatusDTO = {
|
export type TUpdateSecretApprovalReviewStatusDTO = {
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment?: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ export type SecretVersions = {
|
|||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
actor?: {
|
||||||
|
actorId?: string | null;
|
||||||
|
actorType?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
membershipId?: string | null;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// dto
|
// dto
|
||||||
|
|||||||
+218
-35
@@ -1,19 +1,36 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
|
faAngleDown,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faCheck,
|
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
|
faComment,
|
||||||
faFolder,
|
faFolder,
|
||||||
faXmarkCircle
|
faXmarkCircle
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { RadioGroup, RadioGroupIndicator, RadioGroupItem } from "@radix-ui/react-radio-group";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, ContentLoader, EmptyState, IconButton, Tooltip } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
ContentLoader,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
TextArea,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetSecretApprovalRequestDetails,
|
useGetSecretApprovalRequestDetails,
|
||||||
useUpdateSecretApprovalReviewStatus
|
useUpdateSecretApprovalReviewStatus
|
||||||
@@ -74,6 +91,13 @@ type Props = {
|
|||||||
onGoBack: () => void;
|
onGoBack: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reviewFormSchema = z.object({
|
||||||
|
comment: z.string().trim().optional().default(""),
|
||||||
|
status: z.nativeEnum(ApprovalStatus)
|
||||||
|
});
|
||||||
|
|
||||||
|
type TReviewFormSchema = z.infer<typeof reviewFormSchema>;
|
||||||
|
|
||||||
export const SecretApprovalRequestChanges = ({
|
export const SecretApprovalRequestChanges = ({
|
||||||
approvalRequestId,
|
approvalRequestId,
|
||||||
onGoBack,
|
onGoBack,
|
||||||
@@ -94,6 +118,16 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
variables
|
variables
|
||||||
} = useUpdateSecretApprovalReviewStatus();
|
} = useUpdateSecretApprovalReviewStatus();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["reviewChanges"] as const);
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TReviewFormSchema>({
|
||||||
|
resolver: zodResolver(reviewFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus;
|
const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus;
|
||||||
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
||||||
|
|
||||||
@@ -101,23 +135,23 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
|
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||||
({ userId }) => userId === userSession.id
|
({ userId }) => userId === userSession.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||||
Record<string, ApprovalStatus>
|
Record<string, { status: ApprovalStatus; comment: string }>
|
||||||
>(
|
>(
|
||||||
(prev, curr) => ({
|
(prev, curr) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[curr.userId]: curr.status
|
[curr.userId]: { status: curr.status, comment: curr.comment }
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const hasApproved = reviewedUsers?.[userSession.id] === ApprovalStatus.APPROVED;
|
|
||||||
const hasRejected = reviewedUsers?.[userSession.id] === ApprovalStatus.REJECTED;
|
|
||||||
|
|
||||||
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus, comment: string) => {
|
||||||
try {
|
try {
|
||||||
await updateSecretApprovalRequestStatus({
|
await updateSecretApprovalRequestStatus({
|
||||||
id: approvalRequestId,
|
id: approvalRequestId,
|
||||||
status
|
status,
|
||||||
|
comment
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -130,6 +164,16 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
text: "Failed to update the request status"
|
text: "Failed to update the request status"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePopUpToggle("reviewChanges", false);
|
||||||
|
reset({
|
||||||
|
comment: "",
|
||||||
|
status: ApprovalStatus.APPROVED
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitReview = (data: TReviewFormSchema) => {
|
||||||
|
handleSecretApprovalStatusUpdate(data.status, data.comment);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSecretApprovalRequestLoading) {
|
if (isSecretApprovalRequestLoading) {
|
||||||
@@ -150,7 +194,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const isMergable =
|
const isMergable =
|
||||||
secretApprovalRequestDetails?.policy?.approvals <=
|
secretApprovalRequestDetails?.policy?.approvals <=
|
||||||
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
||||||
({ userId }) => reviewedUsers?.[userId] === ApprovalStatus.APPROVED
|
({ userId }) => reviewedUsers?.[userId]?.status === ApprovalStatus.APPROVED
|
||||||
).length;
|
).length;
|
||||||
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
||||||
|
|
||||||
@@ -202,27 +246,115 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!hasMerged && secretApprovalRequestDetails.status === "open" && (
|
{!hasMerged && secretApprovalRequestDetails.status === "open" && (
|
||||||
<>
|
<DropdownMenu
|
||||||
<Button
|
open={popUp.reviewChanges.isOpen}
|
||||||
size="xs"
|
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
|
||||||
leftIcon={hasApproved && <FontAwesomeIcon icon={faCheck} />}
|
>
|
||||||
onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.APPROVED)}
|
<DropdownMenuTrigger asChild>
|
||||||
isLoading={isApproving}
|
<Button
|
||||||
isDisabled={isApproving || hasApproved || !canApprove}
|
variant="outline_bg"
|
||||||
>
|
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
||||||
{hasApproved ? "Approved" : "Approve"}
|
>
|
||||||
</Button>
|
Review
|
||||||
<Button
|
</Button>
|
||||||
size="xs"
|
</DropdownMenuTrigger>
|
||||||
colorSchema="danger"
|
<DropdownMenuContent align="end" asChild className="mt-3">
|
||||||
leftIcon={hasRejected && <FontAwesomeIcon icon={faCheck} />}
|
<form onSubmit={handleSubmit(handleSubmitReview)}>
|
||||||
onClick={() => handleSecretApprovalStatusUpdate(ApprovalStatus.REJECTED)}
|
<div className="flex w-[400px] flex-col space-y-2 p-5">
|
||||||
isLoading={isRejecting}
|
<div className="text-lg font-medium">Finish your review</div>
|
||||||
isDisabled={isRejecting || hasRejected || !canApprove}
|
<Controller
|
||||||
>
|
control={control}
|
||||||
{hasRejected ? "Rejected" : "Reject"}
|
name="comment"
|
||||||
</Button>
|
render={({ field, fieldState: { error } }) => (
|
||||||
</>
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
placeholder="Leave a comment..."
|
||||||
|
reSize="none"
|
||||||
|
className="text-md mt-2 h-48 border border-mineshaft-600 bg-bunker-800"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="status"
|
||||||
|
defaultValue={ApprovalStatus.APPROVED}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<RadioGroup
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="mb-4 space-y-2"
|
||||||
|
aria-label="Status"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
id="approve"
|
||||||
|
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
||||||
|
value={ApprovalStatus.APPROVED}
|
||||||
|
aria-labelledby="approve-label"
|
||||||
|
>
|
||||||
|
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
||||||
|
</RadioGroupItem>
|
||||||
|
<span
|
||||||
|
id="approve-label"
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
field.onChange(ApprovalStatus.APPROVED);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
id="reject"
|
||||||
|
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
|
||||||
|
value={ApprovalStatus.REJECTED}
|
||||||
|
aria-labelledby="reject-label"
|
||||||
|
>
|
||||||
|
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
|
||||||
|
</RadioGroupItem>
|
||||||
|
<span
|
||||||
|
id="reject-label"
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
field.onChange(ApprovalStatus.REJECTED);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isApproving || isRejecting || isSubmitting}
|
||||||
|
variant="outline_bg"
|
||||||
|
>
|
||||||
|
Submit Review
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
@@ -240,7 +372,40 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex items-center space-x-6 rounded-lg bg-mineshaft-800 px-5 py-6">
|
<div className="mt-4 flex flex-col items-center rounded-lg">
|
||||||
|
{secretApprovalRequestDetails?.policy?.approvers
|
||||||
|
.filter((requiredApprover) => reviewedUsers?.[requiredApprover.userId])
|
||||||
|
.map((requiredApprover) => {
|
||||||
|
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mb-4 flex w-full flex-col rounded-md bg-mineshaft-800 p-6"
|
||||||
|
key={`required-approver-${requiredApprover.userId}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="ml-1">
|
||||||
|
{`${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""}`} (
|
||||||
|
{requiredApprover?.email}) has{" "}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`${reviewer?.status === ApprovalStatus.APPROVED ? "text-green-500" : "text-red-500"}`}
|
||||||
|
>
|
||||||
|
{reviewer?.status === ApprovalStatus.APPROVED ? "approved" : "rejected"}
|
||||||
|
</span>{" "}
|
||||||
|
the request.
|
||||||
|
</div>
|
||||||
|
{reviewer?.comment && (
|
||||||
|
<FormControl label="Comment" className="mb-0 mt-2">
|
||||||
|
<TextArea value={reviewer.comment} isDisabled reSize="none">
|
||||||
|
{reviewer?.comment && reviewer.comment}
|
||||||
|
</TextArea>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-6 rounded-lg bg-mineshaft-800 px-5 py-6">
|
||||||
<SecretApprovalRequestAction
|
<SecretApprovalRequestAction
|
||||||
canApprove={canApprove}
|
canApprove={canApprove}
|
||||||
approvalRequestId={secretApprovalRequestDetails.id}
|
approvalRequestId={secretApprovalRequestDetails.id}
|
||||||
@@ -258,7 +423,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<div className="text-sm text-bunker-300">Reviewers</div>
|
<div className="text-sm text-bunker-300">Reviewers</div>
|
||||||
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
||||||
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
||||||
const status = reviewedUsers?.[requiredApprover.userId];
|
const reviewer = reviewedUsers?.[requiredApprover.userId];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
@@ -275,8 +440,17 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content={status || ApprovalStatus.PENDING}>
|
{reviewer?.comment && (
|
||||||
{getReviewedStatusSymbol(status)}
|
<Tooltip content={reviewer.comment}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faComment}
|
||||||
|
size="xs"
|
||||||
|
className="mr-1 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
|
||||||
|
{getReviewedStatusSymbol(reviewer?.status)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,7 +464,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map((reviewer) => {
|
.map((reviewer) => {
|
||||||
const status = reviewedUsers?.[reviewer.userId];
|
const status = reviewedUsers?.[reviewer.userId].status;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
@@ -303,6 +477,15 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{reviewer.comment && (
|
||||||
|
<Tooltip content={reviewer.comment}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faComment}
|
||||||
|
size="xs"
|
||||||
|
className="mr-1 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip content={status || ApprovalStatus.PENDING}>
|
<Tooltip content={status || ApprovalStatus.PENDING}>
|
||||||
{getReviewedStatusSymbol(status)}
|
{getReviewedStatusSymbol(status)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
+208
-95
@@ -5,15 +5,19 @@ import {
|
|||||||
faArrowRotateRight,
|
faArrowRotateRight,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faClock,
|
faClock,
|
||||||
|
faCopy,
|
||||||
|
faDesktop,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faPlus,
|
faPlus,
|
||||||
|
faServer,
|
||||||
faShare,
|
faShare,
|
||||||
faTag,
|
faTag,
|
||||||
faTrash
|
faTrash,
|
||||||
|
faUser
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
@@ -46,6 +50,7 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetSecretVersion } from "@app/hooks/api";
|
import { useGetSecretVersion } from "@app/hooks/api";
|
||||||
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
@@ -120,6 +125,7 @@ export const SecretDetailSidebar = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const cannotEditSecret = permission.cannot(
|
const cannotEditSecret = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
@@ -192,15 +198,73 @@ export const SecretDetailSidebar = ({
|
|||||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReminderSubmit = async (reminderRepeatDays: number | null | undefined, reminderNote: string | null | undefined) => {
|
const handleReminderSubmit = async (
|
||||||
await onSaveSecret(secret, { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, () => { });
|
reminderRepeatDays: number | null | undefined,
|
||||||
}
|
reminderNote: string | null | undefined
|
||||||
|
) => {
|
||||||
|
await onSaveSecret(
|
||||||
|
secret,
|
||||||
|
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true },
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||||
|
|
||||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
const secretReminderNote = watch("reminderNote");
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
|
const getModifiedByIcon = (userType: string | undefined | null) => {
|
||||||
|
switch (userType) {
|
||||||
|
case ActorType.USER:
|
||||||
|
return faUser;
|
||||||
|
case ActorType.IDENTITY:
|
||||||
|
return faDesktop;
|
||||||
|
default:
|
||||||
|
return faServer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModifiedByName = (
|
||||||
|
userType: string | undefined | null,
|
||||||
|
userName: string | null | undefined
|
||||||
|
) => {
|
||||||
|
switch (userType) {
|
||||||
|
case ActorType.PLATFORM:
|
||||||
|
return "System-generated";
|
||||||
|
default:
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLinkToModifyHistoryEntity = (
|
||||||
|
actorId: string,
|
||||||
|
actorType: string,
|
||||||
|
membershipId: string | null = ""
|
||||||
|
) => {
|
||||||
|
switch (actorType) {
|
||||||
|
case ActorType.USER:
|
||||||
|
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${membershipId}`;
|
||||||
|
case ActorType.IDENTITY:
|
||||||
|
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/identities/${actorId}`;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModifyHistoryClick = (
|
||||||
|
actorId: string | undefined | null,
|
||||||
|
actorType: string | undefined | null,
|
||||||
|
membershipId: string | undefined | null
|
||||||
|
) => {
|
||||||
|
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
|
||||||
|
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
|
||||||
|
if (redirectLink) {
|
||||||
|
navigate({ to: redirectLink });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateReminderForm
|
<CreateReminderForm
|
||||||
@@ -213,7 +277,7 @@ export const SecretDetailSidebar = ({
|
|||||||
if (data) {
|
if (data) {
|
||||||
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
|
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
|
||||||
setValue("reminderNote", data.note, { shouldDirty: false });
|
setValue("reminderNote", data.note, { shouldDirty: false });
|
||||||
handleReminderSubmit(data.days, data.note)
|
handleReminderSubmit(data.days, data.note);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -618,7 +682,7 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
||||||
<div className="mb-2 pl-1">Version History</div>
|
<div className="mb-2 pl-1">Version History</div>
|
||||||
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||||
{secretVersion?.map(({ createdAt, secretValue, version, id }) => (
|
{secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div key={id} className="flex w-full flex-col space-y-1">
|
<div key={id} className="flex w-full flex-col space-y-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -633,36 +697,42 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="relative w-10">
|
<div className="relative w-10">
|
||||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex w-full cursor-default flex-col">
|
||||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
{actor && (
|
||||||
Value:
|
<div className="flex flex-row">
|
||||||
</div>
|
<div className="flex w-fit flex-row text-sm">
|
||||||
<div className="group break-all pl-1 font-mono">
|
Modified by:
|
||||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||||
<button
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||||
type="button"
|
<div
|
||||||
className="select-none"
|
onClick={() =>
|
||||||
onClick={(e) => {
|
onModifyHistoryClick(
|
||||||
navigator.clipboard.writeText(secretValue || "");
|
actor.actorId,
|
||||||
const target = e.currentTarget;
|
actor.actorType,
|
||||||
target.style.borderBottom = "1px dashed";
|
actor.membershipId
|
||||||
target.style.paddingBottom = "-1px";
|
)
|
||||||
|
}
|
||||||
// Create and insert popup
|
className="cursor-pointer"
|
||||||
const popup = document.createElement("div");
|
>
|
||||||
popup.className =
|
<FontAwesomeIcon
|
||||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
icon={getModifiedByIcon(actor.actorType)}
|
||||||
popup.textContent = "Copied!";
|
className="ml-2"
|
||||||
target.parentElement?.appendChild(popup);
|
/>
|
||||||
|
</div>
|
||||||
// Remove popup and border after delay
|
</Tooltip>
|
||||||
setTimeout(() => {
|
</div>
|
||||||
popup.remove();
|
</div>
|
||||||
target.style.borderBottom = "none";
|
)}
|
||||||
}, 3000);
|
<div className="flex flex-row">
|
||||||
}}
|
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||||
onKeyDown={(e) => {
|
Value:
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
</div>
|
||||||
|
<div className="group break-all pl-1 font-mono">
|
||||||
|
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="select-none text-left"
|
||||||
|
onClick={(e) => {
|
||||||
navigator.clipboard.writeText(secretValue || "");
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
target.style.borderBottom = "1px dashed";
|
target.style.borderBottom = "1px dashed";
|
||||||
@@ -680,51 +750,74 @@ export const SecretDetailSidebar = ({
|
|||||||
popup.remove();
|
popup.remove();
|
||||||
target.style.borderBottom = "none";
|
target.style.borderBottom = "none";
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
{secretValue}
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
</button>
|
const target = e.currentTarget;
|
||||||
<button
|
target.style.borderBottom = "1px dashed";
|
||||||
type="button"
|
target.style.paddingBottom = "-1px";
|
||||||
className="ml-1 cursor-pointer"
|
|
||||||
onClick={(e) => {
|
// Create and insert popup
|
||||||
e.stopPropagation();
|
const popup = document.createElement("div");
|
||||||
e.currentTarget
|
popup.className =
|
||||||
.closest(".group")
|
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||||
?.classList.remove("show-value");
|
popup.textContent = "Copied!";
|
||||||
}}
|
target.parentElement?.appendChild(popup);
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
// Remove popup and border after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
popup.remove();
|
||||||
|
target.style.borderBottom = "none";
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{secretValue}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.currentTarget
|
e.currentTarget
|
||||||
.closest(".group")
|
.closest(".group")
|
||||||
?.classList.remove("show-value");
|
?.classList.remove("show-value");
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
<FontAwesomeIcon icon={faEyeSlash} />
|
e.stopPropagation();
|
||||||
</button>
|
e.currentTarget
|
||||||
</div>
|
.closest(".group")
|
||||||
<span className="group-[.show-value]:hidden">
|
?.classList.remove("show-value");
|
||||||
{secretValue?.replace(/./g, "*")}
|
}
|
||||||
<button
|
}}
|
||||||
type="button"
|
>
|
||||||
className="ml-1 cursor-pointer"
|
<FontAwesomeIcon icon={faEyeSlash} />
|
||||||
onClick={(e) => {
|
</button>
|
||||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
</div>
|
||||||
}}
|
<span className="group-[.show-value]:hidden">
|
||||||
onKeyDown={(e) => {
|
{secretValue?.replace(/./g, "*")}
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
e.currentTarget
|
e.currentTarget
|
||||||
.closest(".group")
|
.closest(".group")
|
||||||
?.classList.add("show-value");
|
?.classList.add("show-value");
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
<FontAwesomeIcon icon={faEye} />
|
e.currentTarget
|
||||||
</button>
|
.closest(".group")
|
||||||
</span>
|
?.classList.add("show-value");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -898,29 +991,49 @@ export const SecretDetailSidebar = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<div className="flex items-center gap-2">
|
||||||
I={ProjectPermissionActions.Delete}
|
<Tooltip content="Copy Secret ID">
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
<IconButton
|
||||||
colorSchema="danger"
|
variant="outline_bg"
|
||||||
ariaLabel="Delete Secret"
|
ariaLabel="Copy Secret ID"
|
||||||
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
|
onClick={async () => {
|
||||||
isDisabled={!isAllowed}
|
await navigator.clipboard.writeText(secret.id);
|
||||||
onClick={onDeleteSecret}
|
|
||||||
|
createNotification({
|
||||||
|
title: "Secret ID Copied",
|
||||||
|
text: "The secret ID has been copied to your clipboard.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip content="Delete Secret">
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
</Tooltip>
|
||||||
</ProjectPermissionCan>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-1
@@ -241,7 +241,9 @@ export const SecretListView = ({
|
|||||||
|
|
||||||
let successMessage;
|
let successMessage;
|
||||||
if (isReminderEvent) {
|
if (isReminderEvent) {
|
||||||
successMessage = reminderRepeatDays ? "Successfully saved secret reminder" : "Successfully deleted secret reminder";
|
successMessage = reminderRepeatDays
|
||||||
|
? "Successfully saved secret reminder"
|
||||||
|
: "Successfully deleted secret reminder";
|
||||||
} else {
|
} else {
|
||||||
successMessage = "Successfully saved secrets";
|
successMessage = "Successfully saved secrets";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user