Merge branch 'main' into feat/addActorToVersionHistory

This commit is contained in:
carlosmonastyrski
2025-03-05 17:53:57 -03:00
19 changed files with 516 additions and 148 deletions
@@ -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;
+3 -1
View File
@@ -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) => {
+5
View File
@@ -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
+2 -1
View File
@@ -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 {
+70 -78
View File
@@ -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)
}
}
} }
+20 -9
View File
@@ -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)
+84
View File
@@ -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",
+2 -1
View File
@@ -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;
}; };
@@ -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>