mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 12:26:05 +00:00
Merge branch 'main' into feat/addActorToVersionHistory
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 ({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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, relayUdpConnection.LocalAddr().String(), 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,8 +288,40 @@ 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, relayAddress string, errCh chan error) error {
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
maxFailures := 3
|
maxFailures := 3
|
||||||
failures := 0
|
failures := 0
|
||||||
|
|
||||||
@@ -294,78 +332,32 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, serverAddr string,
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
conn, err := quic.DialAddr(ctx, serverAddr, tlsConf, quicConf)
|
// Configure TLS to skip verification
|
||||||
if conn != nil {
|
tlsConfig := &tls.Config{
|
||||||
failures = 0
|
InsecureSkipVerify: true,
|
||||||
conn.CloseWithError(0, "connection closed")
|
NextProtos: []string{"infisical-gateway"},
|
||||||
}
|
}
|
||||||
|
quicConfig := &quic.Config{
|
||||||
if err != nil && !strings.Contains(err.Error(), "tls: failed to verify certificate") {
|
EnableDatagrams: true,
|
||||||
failures++
|
}
|
||||||
log.Warn().Err(err).Int("failures", failures).Msg("Relay connection check failed")
|
func() {
|
||||||
|
checkCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
if failures >= maxFailures {
|
defer cancel()
|
||||||
errCh <- fmt.Errorf("relay connection check failed: %w", err)
|
conn, err := quic.DialAddr(checkCtx, relayAddress, tlsConfig, quicConfig)
|
||||||
|
if err != nil {
|
||||||
|
failures++
|
||||||
|
log.Warn().Err(err).Int("failures", failures).Msg("Relay connection check failed")
|
||||||
|
if failures >= maxFailures {
|
||||||
|
errCh <- fmt.Errorf("relay connection check failed: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if conn != nil {
|
||||||
|
conn.CloseWithError(0, "closed")
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -13,6 +14,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 +38,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 +82,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 +143,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 +172,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,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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+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>
|
||||||
|
|||||||
Reference in New Issue
Block a user