diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index f0dcfc8cb..a4bdeb29e 100644 --- a/.github/workflows/check-api-for-breaking-changes.yml +++ b/.github/workflows/check-api-for-breaking-changes.yml @@ -35,7 +35,20 @@ jobs: echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env echo "SECRET_SCANNING_PRIVATE_KEY=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: REDIS_URL: redis://172.17.0.1:6379 DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable @@ -49,21 +62,33 @@ jobs: SECONDS=0 HEALTHY=0 while [ $SECONDS -lt 60 ]; do - if docker ps | grep infisical-api | grep -q healthy; then - echo "Container is healthy." - HEALTHY=1 + # Check if container is running + if docker ps | grep infisical-api; then + # 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 fi + echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)" - - docker logs infisical-api - - sleep 2 - SECONDS=$((SECONDS+2)) + sleep 5 + SECONDS=$((SECONDS+5)) done - + if [ $HEALTHY -ne 1 ]; then 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 fi - name: Install openapi-diff @@ -71,7 +96,8 @@ jobs: - 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 - name: cleanup + if: always() run: | docker compose -f "docker-compose.dev.yml" down - docker stop infisical-api - docker remove infisical-api + docker stop infisical-api || true + docker rm infisical-api || true \ No newline at end of file diff --git a/backend/src/db/migrations/20250305080145_add-secret-review-comment.ts b/backend/src/db/migrations/20250305080145_add-secret-review-comment.ts new file mode 100644 index 000000000..7d51bb226 --- /dev/null +++ b/backend/src/db/migrations/20250305080145_add-secret-review-comment.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + 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 { + if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment")) { + await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => { + t.dropColumn("comment"); + }); + } +} diff --git a/backend/src/db/schemas/secret-approval-requests-reviewers.ts b/backend/src/db/schemas/secret-approval-requests-reviewers.ts index a5c445587..147646b8d 100644 --- a/backend/src/db/schemas/secret-approval-requests-reviewers.ts +++ b/backend/src/db/schemas/secret-approval-requests-reviewers.ts @@ -13,7 +13,8 @@ export const SecretApprovalRequestsReviewersSchema = z.object({ requestId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - reviewerUserId: z.string().uuid() + reviewerUserId: z.string().uuid(), + comment: z.string().nullable().optional() }); export type TSecretApprovalRequestsReviewers = z.infer; diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index c6998f105..653d04d4f 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -159,7 +159,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv id: z.string() }), body: z.object({ - status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]), + comment: z.string().optional() }), response: { 200: z.object({ @@ -175,8 +176,25 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, 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 }; } }); @@ -267,7 +285,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), committerUser: approvalRequestUser, - reviewers: approvalRequestUser.extend({ status: z.string() }).array(), + reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(), secretPath: z.string(), commits: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 85e48872e..a8f96bb73 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -22,6 +22,7 @@ import { } from "@app/services/secret-sync/secret-sync-types"; import { KmipPermission } from "../kmip/kmip-enum"; +import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; export type TListProjectAuditLogDTO = { filter: { @@ -165,6 +166,7 @@ export enum EventType { SECRET_APPROVAL_REQUEST = "secret-approval-request", SECRET_APPROVAL_CLOSED = "secret-approval-closed", SECRET_APPROVAL_REOPENED = "secret-approval-reopened", + SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review", SIGN_SSH_KEY = "sign-ssh-key", ISSUE_SSH_CREDS = "issue-ssh-creds", 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 { type: EventType.SIGN_SSH_KEY; metadata: { @@ -2482,4 +2494,5 @@ export type Event = | KmipOperationRevokeEvent | KmipOperationLocateEvent | KmipOperationRegisterEvent - | CreateSecretRequestEvent; + | CreateSecretRequestEvent + | SecretApprovalRequestReview; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index f842359bc..5fc869d12 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -100,6 +100,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"), tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer), 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("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"), tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"), @@ -162,8 +163,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { reviewerEmail: email, reviewerLastName: lastName, reviewerUsername: username, - reviewerFirstName: firstName - }) => (userId ? { userId, status, email, firstName, lastName, username } : undefined) + reviewerFirstName: firstName, + reviewerComment: comment + }) => + userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined }, { key: "approverUserId", diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 7f8d266c9..3624344f1 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -320,6 +320,7 @@ export const secretApprovalRequestServiceFactory = ({ approvalId, actor, status, + comment, actorId, actorAuthMethod, actorOrgId @@ -372,15 +373,18 @@ export const secretApprovalRequestServiceFactory = ({ return secretApprovalRequestReviewerDAL.create( { status, + comment, requestId: secretApprovalRequest.id, reviewerUserId: actorId }, 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 ({ diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts index 89af253dd..5d6358072 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts @@ -80,6 +80,7 @@ export type TStatusChangeDTO = { export type TReviewRequestDTO = { approvalId: string; status: ApprovalStatus; + comment?: string; } & Omit; export type TApprovalRequestCountDTO = TProjectPermission; diff --git a/backend/src/lib/gateway/index.ts b/backend/src/lib/gateway/index.ts index 09e92b0c2..8d25c2af6 100644 --- a/backend/src/lib/gateway/index.ts +++ b/backend/src/lib/gateway/index.ts @@ -2,7 +2,7 @@ import crypto from "node:crypto"; import net from "node:net"; -import * as quic from "@infisical/quic"; +import quicDefault, * as quicModule from "@infisical/quic"; import { BadRequestError } from "../errors"; import { logger } from "../logger"; @@ -10,6 +10,8 @@ import { logger } from "../logger"; const DEFAULT_MAX_RETRIES = 3; const DEFAULT_RETRY_DELAY = 1000; // 1 second +const quic = quicDefault || quicModule; + const parseSubjectDetails = (data: string) => { const values: Record = {}; data.split("\n").forEach((el) => { diff --git a/cli/config/infisical-relay.yaml b/cli/config/infisical-relay.yaml index 1a2f32d21..89c6b5e45 100644 --- a/cli/config/infisical-relay.yaml +++ b/cli/config/infisical-relay.yaml @@ -1,3 +1,8 @@ public_ip: 127.0.0.1 auth_secret: changeThisOnProduction 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 diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go index d8bfebc91..58a0503ff 100644 --- a/cli/packages/gateway/connection.go +++ b/cli/packages/gateway/connection.go @@ -61,7 +61,6 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) { switch string(cmd) { case "FORWARD-TCP": - log.Info().Msg("Starting secure connector proxy...") proxyAddress := string(bytes.Split(args, []byte(" "))[0]) destTarget, err := net.Dial("tcp", proxyAddress) if err != nil { @@ -69,6 +68,7 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) { return } defer destTarget.Close() + log.Info().Msgf("Starting secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String()) // Handle buffered data buffered := reader.Buffered() @@ -87,6 +87,7 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) { } CopyDataFromQuicToTcp(stream, destTarget) + log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String()) return case "PING": if _, err := stream.Write([]byte("PONG\n")); err != nil { diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go index 846231071..43340df38 100644 --- a/cli/packages/gateway/gateway.go +++ b/cli/packages/gateway/gateway.go @@ -6,11 +6,13 @@ import ( "crypto/x509" "fmt" "net" + "os" "strings" "sync" "time" "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/systemd" "github.com/go-resty/resty/v2" "github.com/pion/logging" "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 // 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{ STUNServerAddr: relayDetails.TurnServerAddress, TURNServerAddr: relayDetails.TurnServerAddress, @@ -82,7 +88,7 @@ func (g *Gateway) ConnectWithRelay() error { Username: relayDetails.TurnServerUsername, Password: relayDetails.TurnServerPassword, Realm: relayDetails.TurnServerRealm, - LoggerFactory: logging.NewDefaultLoggerFactory(), + LoggerFactory: logger, } client, err := turn.NewClient(cfg) @@ -96,10 +102,6 @@ func (g *Gateway) ConnectWithRelay() error { TurnServerAddress: relayDetails.TurnServerAddress, 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 return nil @@ -144,7 +146,10 @@ func (g *Gateway) Listen(ctx context.Context) error { errCh := make(chan error, 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) 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, } - g.registerRelayIsActive(ctx, relayUdpConnection.LocalAddr().String(), tlsConfig, quicConfig, errCh) - + g.registerRelayIsActive(ctx, relayUdpConnection.LocalAddr().String(), errCh) quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig) if err != nil { 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 { case <-ctx.Done(): 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) { - ticker := time.NewTicker(5 * time.Second) +func (g *Gateway) createPermissionForStaticIps(staticIps string) error { + 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 failures := 0 @@ -294,78 +332,32 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, serverAddr string, case <-ctx.Done(): return case <-ticker.C: - conn, err := quic.DialAddr(ctx, serverAddr, tlsConf, quicConf) - if conn != nil { - failures = 0 - conn.CloseWithError(0, "connection closed") + // Configure TLS to skip verification + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"infisical-gateway"}, } - - if err != nil && !strings.Contains(err.Error(), "tls: failed to verify certificate") { - 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) + quicConfig := &quic.Config{ + EnableDatagrams: true, + } + func() { + checkCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + 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) { - 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) - } - } + + return nil } diff --git a/cli/packages/gateway/relay.go b/cli/packages/gateway/relay.go index 52636d2d3..0db5fd387 100644 --- a/cli/packages/gateway/relay.go +++ b/cli/packages/gateway/relay.go @@ -3,6 +3,7 @@ package gateway import ( "context" "crypto/tls" + "crypto/x509" "errors" "fmt" "net" @@ -13,6 +14,7 @@ import ( "syscall" udplistener "github.com/Infisical/infisical-merge/packages/gateway/udp_listener" + "github.com/Infisical/infisical-merge/packages/systemd" "github.com/pion/logging" "github.com/pion/turn/v4" "github.com/rs/zerolog/log" @@ -36,8 +38,10 @@ type GatewayRelayConfig struct { RelayMaxPort uint16 `yaml:"relay_max_port"` TlsCertPath string `yaml:"tls_cert_path"` TlsPrivateKeyPath string `yaml:"tls_private_key_path"` + TlsCaPath string `yaml:"tls_ca_path"` tls tls.Certificate + tlsCa string isTlsEnabled bool } @@ -78,19 +82,19 @@ func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) { return nil, errMissingTlsCert } - tlsCertFile, err := os.ReadFile(cfg.TlsCertPath) + cert, err := tls.LoadX509KeyPair(cfg.TlsCertPath, cfg.TlsPrivateKeyPath) if err != nil { - return nil, err - } - tlsPrivateKeyFile, err := os.ReadFile(cfg.TlsPrivateKeyPath) - if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to read load server tls key pair: %w", err) } - cert, err := tls.LoadX509KeyPair(string(tlsCertFile), string(tlsPrivateKeyFile)) - if err != nil { - return nil, err + if cfg.TlsCaPath != "" { + ca, err := os.ReadFile(cfg.TlsCaPath) + if err != nil { + return nil, fmt.Errorf("Failed to read tls ca: %w", err) + } + cfg.tlsCa = string(ca) } + cfg.tls = cert cfg.isTlsEnabled = true } @@ -139,8 +143,12 @@ func (g *GatewayRelay) Run() error { } if g.Config.isTlsEnabled { + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM([]byte(g.Config.tlsCa)) + listenerConfigs[i].Listener = tls.NewListener(conn, &tls.Config{ Certificates: []tls.Certificate{g.Config.tls}, + ClientCAs: caCertPool, }) } else { listenerConfigs[i].Listener = conn @@ -164,6 +172,9 @@ func (g *GatewayRelay) Run() error { } 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 sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) diff --git a/cli/packages/systemd/daemon.go b/cli/packages/systemd/daemon.go new file mode 100644 index 000000000..ce3c97394 --- /dev/null +++ b/cli/packages/systemd/daemon.go @@ -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 +} diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 39c351e38..d348c7bb1 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -122,6 +122,7 @@ export const eventToNameMap: { [K in EventType]: string } = { "OIDC group membership mapping assigned user to groups", [EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]: "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.UPDATE_KMIP_CLIENT]: "Update KMIP client", [EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index ed25c6e4a..76bb283dc 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -150,5 +150,6 @@ export enum EventType { KMIP_OPERATION_ACTIVATE = "kmip-operation-activate", KMIP_OPERATION_REVOKE = "kmip-operation-revoke", 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" } diff --git a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx index ce584a85f..78e1b37a1 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx @@ -13,9 +13,10 @@ export const useUpdateSecretApprovalReviewStatus = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ id, status }) => { + mutationFn: async ({ id, status, comment }) => { const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, { - status + status, + comment }); return data; }, diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index a03526dae..433d82855 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -44,6 +44,7 @@ export type TSecretApprovalRequest = { reviewers: { userId: string; status: ApprovalStatus; + comment: string; email: string; firstName: string; lastName: string; @@ -114,6 +115,7 @@ export type TGetSecretApprovalRequestDetails = { export type TUpdateSecretApprovalReviewStatusDTO = { status: ApprovalStatus; + comment?: string; id: string; }; diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index 04bee3042..aa73d7d07 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -1,19 +1,36 @@ import { ReactNode } from "react"; +import { Controller, useForm } from "react-hook-form"; import { + faAngleDown, faArrowLeft, - faCheck, faCheckCircle, faCircle, faCodeBranch, + faComment, faFolder, faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; 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 z from "zod"; 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 { usePopUp } from "@app/hooks"; import { useGetSecretApprovalRequestDetails, useUpdateSecretApprovalReviewStatus @@ -74,6 +91,13 @@ type Props = { onGoBack: () => void; }; +const reviewFormSchema = z.object({ + comment: z.string().trim().optional().default(""), + status: z.nativeEnum(ApprovalStatus) +}); + +type TReviewFormSchema = z.infer; + export const SecretApprovalRequestChanges = ({ approvalRequestId, onGoBack, @@ -94,6 +118,16 @@ export const SecretApprovalRequestChanges = ({ variables } = useUpdateSecretApprovalReviewStatus(); + const { popUp, handlePopUpToggle } = usePopUp(["reviewChanges"] as const); + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(reviewFormSchema) + }); + const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus; const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus; @@ -101,23 +135,23 @@ export const SecretApprovalRequestChanges = ({ const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some( ({ userId }) => userId === userSession.id ); + const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce< - Record + Record >( (prev, curr) => ({ ...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 { await updateSecretApprovalRequestStatus({ id: approvalRequestId, - status + status, + comment }); createNotification({ type: "success", @@ -130,6 +164,16 @@ export const SecretApprovalRequestChanges = ({ 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) { @@ -150,7 +194,7 @@ export const SecretApprovalRequestChanges = ({ const isMergable = secretApprovalRequestDetails?.policy?.approvals <= secretApprovalRequestDetails?.policy?.approvers?.filter( - ({ userId }) => reviewedUsers?.[userId] === ApprovalStatus.APPROVED + ({ userId }) => reviewedUsers?.[userId]?.status === ApprovalStatus.APPROVED ).length; const hasMerged = secretApprovalRequestDetails?.hasMerged; @@ -202,27 +246,115 @@ export const SecretApprovalRequestChanges = ({ {!hasMerged && secretApprovalRequestDetails.status === "open" && ( - <> - - - + handlePopUpToggle("reviewChanges", isOpen)} + > + + + + +
+
+
Finish your review
+ ( + + + + )} +
+ ); + })} + +
Reviewers
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => { - const status = reviewedUsers?.[requiredApprover.userId]; + const reviewer = reviewedUsers?.[requiredApprover.userId]; return (
*
- - {getReviewedStatusSymbol(status)} + {reviewer?.comment && ( + + + + )} + + {getReviewedStatusSymbol(reviewer?.status)}
@@ -290,7 +464,7 @@ export const SecretApprovalRequestChanges = ({ ) ) .map((reviewer) => { - const status = reviewedUsers?.[reviewer.userId]; + const status = reviewedUsers?.[reviewer.userId].status; return (
*
+ {reviewer.comment && ( + + + + )} {getReviewedStatusSymbol(status)}