From 76427f43f7f576272eee2481fc5e7bab6e1feebc Mon Sep 17 00:00:00 2001 From: = Date: Wed, 5 Mar 2025 16:16:07 +0530 Subject: [PATCH 01/54] feat: fixed cli issues in gateway --- cli/packages/gateway/connection.go | 3 +- cli/packages/gateway/gateway.go | 150 ++++++++++++++--------------- cli/packages/gateway/relay.go | 4 + cli/packages/systemd/daemon.go | 84 ++++++++++++++++ 4 files changed, 161 insertions(+), 80 deletions(-) create mode 100644 cli/packages/systemd/daemon.go 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..ae18d493e 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() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + conn, err := quic.DialAddr(ctx, 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..6659d1446 100644 --- a/cli/packages/gateway/relay.go +++ b/cli/packages/gateway/relay.go @@ -13,6 +13,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" @@ -164,6 +165,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 +} From babbacdc961e8f48c8dfa48268cfc58ebb0b4fc3 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 5 Mar 2025 19:25:56 +0800 Subject: [PATCH 02/54] feat: add secret approval review comment --- ...0250305080145_add-secret-review-comment.ts | 19 ++ .../secret-approval-requests-reviewers.ts | 3 +- .../v1/secret-approval-request-router.ts | 24 +- .../ee/services/audit-log/audit-log-types.ts | 15 +- .../secret-approval-request-dal.ts | 7 +- .../secret-approval-request-service.ts | 8 +- .../secret-approval-request-types.ts | 1 + .../src/hooks/api/auditLogs/constants.tsx | 1 + frontend/src/hooks/api/auditLogs/enums.tsx | 3 +- .../api/secretApprovalRequest/mutation.tsx | 5 +- .../hooks/api/secretApprovalRequest/types.ts | 2 + .../SecretApprovalRequestChanges.tsx | 218 +++++++++++++++--- 12 files changed, 260 insertions(+), 46 deletions(-) create mode 100644 backend/src/db/migrations/20250305080145_add-secret-review-comment.ts 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..2c503515c 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_REVIEWED, + 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..3e35c16f8 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_REVIEWED = "secret-approval-request-reviewed", 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 SecretApprovalRequestReviewed { + type: EventType.SECRET_APPROVAL_REQUEST_REVIEWED; + 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 + | SecretApprovalRequestReviewed; 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 17eecf508..8569ef2a9 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/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 39c351e38..445bda774 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_REVIEWED]: "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..ad84508e7 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_REVIEWED = "secret-approval-request-reviewed" } 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..899d46424 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
+ ( + + + + )} +
+ ); + })} + +
Date: Wed, 5 Mar 2025 20:00:37 +0530 Subject: [PATCH 07/54] feat: added ca to cli --- cli/config/infisical-relay.yaml | 5 +++++ cli/packages/gateway/relay.go | 25 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) 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/relay.go b/cli/packages/gateway/relay.go index 6659d1446..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" @@ -37,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 } @@ -79,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 } @@ -140,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 From b331a4a7080be136ce2c29ff366a221bb3e0aceb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 22:17:16 +0400 Subject: [PATCH 08/54] fix: breaking changes check --- .github/workflows/check-api-for-breaking-changes.yml | 3 +++ backend/src/server/routes/v1/slack-router.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index f0dcfc8cb..48a58a497 100644 --- a/.github/workflows/check-api-for-breaking-changes.yml +++ b/.github/workflows/check-api-for-breaking-changes.yml @@ -64,6 +64,9 @@ jobs: if [ $HEALTHY -ne 1 ]; then echo "Container did not become healthy in time" + + echo "Container logs:" + docker logs infisical-api exit 1 fi - name: Install openapi-diff diff --git a/backend/src/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index 94276a13c..a351041a2 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -19,6 +19,8 @@ const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({ }) ); +// test change + export const registerSlackRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); From f98f212ecfff4b93fa07a4c6e0cb9115b57f79dd Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 22:23:49 +0400 Subject: [PATCH 09/54] Update check-api-for-breaking-changes.yml --- .../check-api-for-breaking-changes.yml | 53 +++++++++++++------ backend/src/server/routes/v1/slack-router.ts | 2 +- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index 48a58a497..f42c10778 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,24 +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/health > /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 logs:" - docker logs infisical-api + 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 @@ -74,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/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index a351041a2..8ee1846c9 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -19,7 +19,7 @@ const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({ }) ); -// test change +// test change // export const registerSlackRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); From b5575f4c20103bac1d0d06c178de3563e61618f6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 22:31:01 +0400 Subject: [PATCH 10/54] fix api endpoint --- .github/workflows/check-api-for-breaking-changes.yml | 2 +- backend/src/server/routes/v1/slack-router.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index f42c10778..5d16a513c 100644 --- a/.github/workflows/check-api-for-breaking-changes.yml +++ b/.github/workflows/check-api-for-breaking-changes.yml @@ -65,7 +65,7 @@ jobs: # 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/health > /dev/null 2>&1; then + if curl -s -f http://localhost:4000/api/status > /dev/null 2>&1; then echo "API endpoint is responding. Container seems healthy." HEALTHY=1 break diff --git a/backend/src/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index 8ee1846c9..375c9df4f 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -19,7 +19,7 @@ const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({ }) ); -// test change // +// test change // // export const registerSlackRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); From a0a579834c6bf50c53e4ad74b9927ff7ec2f2683 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 22:36:43 +0400 Subject: [PATCH 11/54] fix: check docs endpoint instead of status --- .github/workflows/check-api-for-breaking-changes.yml | 2 +- backend/src/server/routes/v1/slack-router.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index 5d16a513c..a4bdeb29e 100644 --- a/.github/workflows/check-api-for-breaking-changes.yml +++ b/.github/workflows/check-api-for-breaking-changes.yml @@ -65,7 +65,7 @@ jobs: # 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/status > /dev/null 2>&1; then + 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 diff --git a/backend/src/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index 375c9df4f..94276a13c 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -19,8 +19,6 @@ const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({ }) ); -// test change // // - export const registerSlackRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); From 94fe5770461d577e8a8b64d27a580e544b493816 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 22:38:35 +0400 Subject: [PATCH 12/54] chore: test breaking change --- backend/src/server/routes/v3/secret-router.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index a5dc39485..95cbd4772 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -382,7 +382,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/raw/:secretName", + url: "/raw/:secretName/:id", config: { rateLimit: secretsLimit }, @@ -394,9 +394,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) + secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName), + id: z.string().trim() }), querystring: z.object({ + testRequiredField: z.string().describe("test required field"), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), From 85c5d69c36ef3692f2db9f075baf2c7bc07b9ba1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 22:42:29 +0400 Subject: [PATCH 13/54] chore: remove breaking change test --- backend/src/server/routes/v3/secret-router.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 95cbd4772..a5dc39485 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -382,7 +382,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/raw/:secretName/:id", + url: "/raw/:secretName", config: { rateLimit: secretsLimit }, @@ -394,11 +394,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName), - id: z.string().trim() + secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) }), querystring: z.object({ - testRequiredField: z.string().describe("test required field"), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), From 4a66395ce60d4c8c6c771cca89388e43a4e81996 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 18 Feb 2025 06:17:47 +0400 Subject: [PATCH 14/54] feat(api): view secret value, WIP --- ...kfill-secret-permissions-with-readvalue.ts | 36 ++ backend/src/ee/routes/v1/secret-router.ts | 4 +- .../services/permission/project-permission.ts | 50 +- .../secret-approval-request-service.ts | 18 +- .../secret-rotation-service.ts | 8 +- .../secret-snapshot-service.ts | 12 +- backend/src/lib/api-docs/constants.ts | 3 + backend/src/lib/errors/index.ts | 10 + .../src/server/routes/v1/dashboard-router.ts | 21 +- backend/src/server/routes/v3/login-router.ts | 2 + backend/src/server/routes/v3/secret-router.ts | 100 ++-- .../external-migration-fns.ts | 2 +- .../external-migration-queue.ts | 2 +- .../integration/integration-service.ts | 10 +- .../src/services/project/project-service.ts | 11 +- .../secret-import/secret-import-service.ts | 21 +- .../secret-sync/secret-sync-service.ts | 6 +- .../src/services/secret-tag/secret-tag-dal.ts | 1 + .../secret-v2-bridge/secret-v2-bridge-fns.ts | 30 +- .../secret-v2-bridge-service.ts | 537 +++++++++++++----- .../secret-v2-bridge-types.ts | 8 +- backend/src/services/secret/secret-fns.ts | 42 +- backend/src/services/secret/secret-service.ts | 191 +++++-- backend/src/services/secret/secret-types.ts | 2 + .../service-token/service-token-service.ts | 8 +- .../InfisicalSecretInput.tsx | 6 +- .../components/v2/SecretInput/SecretInput.tsx | 29 +- .../context/ProjectPermissionContext/types.ts | 10 +- frontend/src/hooks/api/dashboard/queries.tsx | 3 + frontend/src/hooks/api/dashboard/types.ts | 1 + .../src/hooks/api/secretImports/queries.tsx | 1 + frontend/src/hooks/api/secrets/queries.tsx | 1 + frontend/src/hooks/api/secrets/types.ts | 2 + .../components/GeneralPermissionPolicies.tsx | 40 +- .../ProjectRoleModifySection.utils.tsx | 36 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 4 + .../SecretOverviewTableRow.tsx | 9 +- .../SecretDashboardPage.tsx | 15 +- .../SecretListView/SecretDetailSidebar.tsx | 32 +- .../components/SecretListView/SecretItem.tsx | 21 +- 40 files changed, 1042 insertions(+), 303 deletions(-) create mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts new file mode 100644 index 000000000..d5ec0e55f --- /dev/null +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -0,0 +1,36 @@ +import { selectAllTableCols } from "@app/lib/knex"; +import { Knex } from "knex"; +import { TableName } from "../schemas"; + +// [["read,create","secrets",{"environment":{"$eq":"dev"}}], +// ["read,edit,create","secrets",{"environment":{"$eq":"staging"}}], +// ["read,create","secrets",{"environment":{"$eq":"prod"}},1], +// ["edit,delete,create","secret-folders",{}], +// ["read,edit,delete,create","secret-imports",{}], +// ["read,edit,delete,create","member"], +// ["read,edit,delete,create","role"], +// ["read,edit,delete,create","integrations"], +// ["read,edit","settings"], +// ["edit,delete","workspace"], +// ["read,edit,delete,create","tags"], +// ["read,create,edit,delete,sync-secrets,import-secrets,remove-secrets","secret-syncs"]] + +// enum ProjectPermissionSub { +// Secrets = "secrets" +// } + +export async function up(knex: Knex): Promise { + const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); + + for (const projectRole of projectRoles) { + const { _, permissions } = projectRole; + + const parsedPermissions = JSON.parse(permissions as string) as Record[]; // contains array of permissions. + + for (const parsedPermission of parsedPermissions) { + console.log(parsedPermission); + } + } +} + +export async function down(knex: Knex): Promise {} diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts index 4c249afe0..a964eb1b8 100644 --- a/backend/src/ee/routes/v1/secret-router.ts +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -1,6 +1,6 @@ import z from "zod"; -import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { RAW_SECRETS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; const AccessListEntrySchema = z .object({ - allowedActions: z.nativeEnum(ProjectPermissionActions).array(), + allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(), id: z.string(), membershipId: z.string(), name: z.string() diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4389c3866..f5218c0d6 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -17,6 +17,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionSecretActions { + DescribeSecret = "read", + ReadValue = "readValue", + Create = "create", + Edit = "edit", + Delete = "delete" +} + export enum ProjectPermissionCmekActions { Read = "read", Create = "create", @@ -115,7 +123,7 @@ export type IdentityManagementSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) ] | [ @@ -433,7 +441,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV1Schema.describe( @@ -460,7 +468,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV2Schema.describe( @@ -517,7 +525,6 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ - ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, @@ -550,10 +557,21 @@ const buildAdminPermissionRules = () => { ProjectPermissionActions.Create, ProjectPermissionActions.Delete ], - el as ProjectPermissionSub + el ); }); + can( + [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + can( [ ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -613,10 +631,11 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete ], ProjectPermissionSub.Secrets ); @@ -788,7 +807,8 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + // ? Q(Daniel): Should the viewer role be allowed to read values? Currently not allowed in permission below. + can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); @@ -831,6 +851,8 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); + const canReadValue = permission.includes("readValue"); + const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( @@ -860,6 +882,14 @@ export const buildServiceTokenProjectPermission = ( environment }); } + + if (subject === ProjectPermissionSub.Secrets && canReadValue) { + // @ts-expect-error type + can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { + secretPath: { $glob: secretPath }, + environment + }); + } } ); }); 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 8569ef2a9..5abd47d75 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 @@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; @@ -88,7 +88,12 @@ type TSecretApprovalRequestServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: Pick< TSecretTagDALFactory, - "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2" + | "findManyTagsById" + | "saveTagsToSecret" + | "deleteTagsManySecret" + | "saveTagsToSecretV2" + | "deleteTagsToSecretV2" + | "find" >; secretBlindIndexDAL: Pick; snapshotService: Pick; @@ -914,7 +919,7 @@ export const secretApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -1001,6 +1006,7 @@ export const secretApprovalRequestServiceFactory = ({ : keyName2BlindIndex[secretName]; // add tags if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; + return { ...latestSecretVersions[secretId], ...el, @@ -1363,9 +1369,9 @@ export const secretApprovalRequestServiceFactory = ({ const tagsGroupById = groupBy(tags, (i) => i.id); commits.forEach((commit) => { - let action = ProjectPermissionActions.Create; - if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; - if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; + let action = ProjectPermissionSecretActions.Create; + if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit; + if (commit.op === SecretOperations.Delete) action = ProjectPermissionSecretActions.Delete; ForbiddenError.from(permission).throwUnlessCan( action, diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 02da4b7ea..8d458111a 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; @@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({ }); } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 1c34f6b3d..624b82d77 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -22,7 +22,11 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TGetSnapshotDataDTO, TProjectSnapshotCountDTO, @@ -97,7 +101,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -134,7 +138,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -224,7 +228,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: snapshotDetails.environment.slug, secretPath: fullFolderPath diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 1b458175d..5a412cb45 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -666,6 +666,7 @@ export const SECRETS = { secretPath: "The path of the secret to attach tags to.", type: "The type of the secret to attach tags to. (shared/personal)", environment: "The slug of the environment where the secret is located", + viewSecretValue: "Whether or not to retrieve the secret value.", projectSlug: "The slug of the project where the secret is located.", tagSlugs: "An array of existing tag slugs to attach to the secret." }, @@ -689,6 +690,7 @@ export const RAW_SECRETS = { "The slug of the project to list secrets from. This parameter is only applicable by machine identities.", environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not.", tagSlugs: "The comma separated tag slugs to filter secrets.", metadataFilter: @@ -717,6 +719,7 @@ export const RAW_SECRETS = { secretPath: "The path of the secret to get.", version: "The version of the secret to get.", type: "The type of the secret to get.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not." }, UPDATE: { diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index cc1c1d66b..579674986 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -1,3 +1,6 @@ +// eslint-disable-next-line max-classes-per-file +import { AnyAbility, ForbiddenError } from "@casl/ability"; + /* eslint-disable max-classes-per-file */ export class DatabaseError extends Error { name: string; @@ -59,6 +62,13 @@ export class ForbiddenRequestError extends Error { } } +export class ForbiddenReadSecretError extends ForbiddenRequestError { + constructor({ error, message }: { message?: string; error?: unknown } = {}) { + super({ message, error }); + this.name = "ForbiddenReadSecretError"; + } +} + export class BadRequestError extends Error { name: string; diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index d96c45f04..e89138aee 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -116,6 +116,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ @@ -294,6 +295,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { secrets = await server.services.secret.getSecretsRawMultiEnv({ + viewSecretValue: true, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, @@ -393,6 +395,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), + viewSecretValue: booleanSchema.default(true), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), @@ -410,6 +413,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ @@ -600,10 +604,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { tagSlugs: tags }); + console.log("totalSecretCount", totalSecretCount); + console.log("adjustedOffset", adjustedOffset); + console.log("remainingLimit", remainingLimit); + + console.log("will resolve to true", remainingLimit > 0 && totalSecretCount > adjustedOffset); + if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { + console.log("before running"); const secretsRaw = await server.services.secret.getSecretsRaw({ actorId: req.permission.id, actor: req.permission.type, + viewSecretValue: req.query.viewSecretValue, actorOrgId: req.permission.orgId, environment, actorAuthMethod: req.permission.authMethod, @@ -617,6 +629,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { tagSlugs: tags }); + console.log("secretsRaw", secretsRaw); secrets = secretsRaw.secrets; await server.services.auditLog.createAuditLog({ @@ -649,6 +662,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } } catch (error) { + console.log(error); if (!(error instanceof ForbiddenError)) { throw error; } @@ -862,12 +876,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - keys: z.string().trim().transform(decodeURIComponent) + keys: z.string().trim().transform(decodeURIComponent), + viewSecretValue: booleanSchema.default(true) }), response: { 200: z.object({ secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ @@ -886,7 +902,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { secretPath, projectId, environment } = req.query; + const { secretPath, projectId, environment, viewSecretValue } = req.query; const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? []; if (!keys.length) throw new BadRequestError({ message: "One or more keys required" }); @@ -895,6 +911,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + viewSecretValue, environment, actorAuthMethod: req.permission.authMethod, projectId, diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index cddfc1c2b..7cb9f121a 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -77,6 +77,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { secure: cfg.HTTPS_ENABLED }); + console.log("access token", tokens.access); + return { token: tokens.access, isMfaEnabled: false }; } }); diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index a5dc39485..108507922 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -31,6 +31,14 @@ const SecretReferenceNode = z.object({ environment: z.string(), secretPath: z.string() }); + +const convertStringBoolean = (defaultValue: boolean = false) => { + return z + .enum(["true", "false"]) + .default(defaultValue ? "true" : "false") + .transform((value) => value === "true"); +}; + type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ @@ -45,6 +53,7 @@ const SecretNameSchema = BaseSecretNameSchema.refine( ).refine((el) => !el.includes(":"), "Secret name cannot contain colon."); export const registerSecretRouter = async (server: FastifyZodProvider) => { + // ! Note(Daniel): (Tags) Does not support secrets v2. Request will fail if user doesn't have read value permission. server.route({ method: "POST", url: "/tags/:secretName", @@ -64,6 +73,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { body: z.object({ projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug), environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment), + viewSecretValue: convertStringBoolean(true).describe(SECRETS.ATTACH_TAGS.viewSecretValue), secretPath: z .string() .trim() @@ -108,6 +118,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ! Note(Daniel): (Tags) Does not support secrets v2. Request will fail if user doesn't have read value permission. server.route({ method: "DELETE", url: "/tags/:secretName", @@ -169,6 +180,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Note(Daniel): (Secrets) Done for v2 secrets AND normal secrets GET /raw server.route({ method: "GET", url: "/raw", @@ -247,21 +259,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.expand), - recursive: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.recursive), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.includeImports), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), + recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), + include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), tagSlugs: z .string() .describe(RAW_SECRETS.LIST.tagSlugs) @@ -274,6 +275,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ id: true, @@ -293,6 +295,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .omit({ createdAt: true, updatedAt: true }) .extend({ + // secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional() }) .array() @@ -342,6 +345,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, actorAuthMethod: req.permission.authMethod, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, path: secretPath, metadataFilter: req.query.metadataFilter, includeImports: req.query.include_imports, @@ -376,10 +380,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); } + return { secrets, imports }; } }); + // !!!!!!!!!!!!!!!!!!!!! Note(Daniel): (Secrets) Done for v2 secrets -- GET /raw/:secretName server.route({ method: "GET", url: "/raw/:secretName", @@ -403,20 +409,14 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.expand), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.includeImports) + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), + include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) }), response: { 200: z.object({ secret: secretRawSchema.extend({ + secretValueHidden: z.boolean(), tags: SecretTagsSchema.pick({ id: true, slug: true, @@ -456,6 +456,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, environment, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, projectSlug: workspaceSlug, path: secretPath, secretName: req.params.secretName, @@ -498,6 +499,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ? Note(Daniel): No modify, if user has Create permissions it will return the value they created for this secret --- POST /raw/:secretName server.route({ method: "POST", url: "/raw/:secretName", @@ -611,6 +613,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ? Note(Daniel): Will NOT throw an error. If the user has access to read value, it will return value. + // ? Note(Daniel): If user does NOT have access to read value, it will return for the value, but succeed with update. + // !!!!! Done for both secret types. For legacy secrets, it will return if no read value permission is present. server.route({ method: "PATCH", url: "/raw/:secretName", @@ -728,6 +733,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ? Note(Daniel): Will NOT throw an error. If the user has access to read value, it will return the deleted value + // ? Note(Daniel): If user does NOT have access to read value, it will return for the value, but succeed with delete. + // !!!!! Done for both secret types. For legacy secrets, it will return if no read value permission is present. --- /raw/:secretName server.route({ method: "DELETE", url: "/raw/:secretName", @@ -758,7 +766,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -780,6 +790,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } + const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ @@ -814,6 +825,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. Will throw without the `readValue` permission, just like before. server.route({ method: "GET", url: "/", @@ -928,6 +940,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. Will throw without the `readValue` permission, just like before. server.route({ method: "GET", url: "/:secretName", @@ -942,12 +955,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + viewSecretValue: convertStringBoolean(true), type: z.nativeEnum(SecretType).default(SecretType.Shared), version: z.coerce.number().optional(), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") + include_imports: convertStringBoolean() }), response: { 200: z.object({ @@ -1009,6 +1020,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. Will work exactly like before. It will not attempt to hide the secret value, because the user creating this secret will already know the value upon creation. server.route({ url: "/:secretName", method: "POST", @@ -1180,6 +1192,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. Will work like before, EXCEPT, if the user doesn't have the `readValue` permission, the secret value will be marked as "" server.route({ method: "PATCH", url: "/:secretName", @@ -1218,6 +1231,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( z.object({ + secretValueHidden: z.boolean(), _id: z.string(), workspace: z.string(), environment: z.string() @@ -1367,6 +1381,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. Will work like before, EXCEPT, if the user doesn't have the `readValue` permission, the secret value will be marked as "" server.route({ method: "DELETE", url: "/:secretName", @@ -1491,6 +1506,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ? No need for update, as this endpoint does not expose any values. server.route({ method: "POST", url: "/move", @@ -1546,6 +1562,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. This will works exactly like before. It will not attempt to hide the secret value, because the user creating this secret will already know the value(s) upon creation. server.route({ method: "POST", url: "/batch", @@ -1672,6 +1689,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. Works as before, EXCEPT if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "" server.route({ method: "PATCH", url: "/batch", @@ -1705,7 +1723,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1798,6 +1816,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // !!!! Done. Works as before, EXCEPT if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "" server.route({ method: "DELETE", url: "/batch", @@ -1820,7 +1839,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }) + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1912,6 +1935,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ! (Daniel): Done. Will not attempt to hide secret value because this is a create operation. server.route({ method: "POST", url: "/batch/raw", @@ -2018,6 +2042,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ! Done. Works as before, except if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "" server.route({ method: "PATCH", url: "/batch/raw", @@ -2082,7 +2107,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2170,6 +2195,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ! (Daniel): Done. Works as before, except if the user doesn't have the `readValue` permission, the secret value(s) will be marked as "" server.route({ method: "DELETE", url: "/batch/raw", @@ -2204,7 +2230,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2262,6 +2292,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ! IMPORTANT: CHANGED BEHAVIOR -> Now this endpoint will throw a descriptive error if the user doesn't have access to the value of the secret itself. server.route({ method: "GET", url: "/raw/:secretName/secret-reference-tree", @@ -2314,6 +2345,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + // ? No work needed, does not expose secret value. server.route({ method: "POST", url: "/backfill-secret-references", diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index 744678792..cd0e1c858 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -33,7 +33,7 @@ export type TImportDataIntoInfisicalDTO = { secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; resourceMetadataDAL: Pick; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index e3d77e832..94ca65fac 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -29,7 +29,7 @@ export type TExternalMigrationQueueFactoryDep = { secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; folderDAL: Pick; diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 35994433a..f72357964 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -2,7 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -92,7 +96,7 @@ export const integrationServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath @@ -175,7 +179,7 @@ export const integrationServiceFactory = ({ if (environment || secretPath) { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index e1653d371..d3afaec29 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -11,7 +11,11 @@ import { import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; @@ -747,7 +751,10 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index e8fde04d1..3ce5b0115 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -5,7 +5,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -90,7 +94,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -402,7 +406,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -596,7 +600,7 @@ export const secretImportServiceFactory = ({ const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -647,7 +651,7 @@ export const secretImportServiceFactory = ({ decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -667,7 +671,7 @@ export const secretImportServiceFactory = ({ const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -683,7 +687,10 @@ export const secretImportServiceFactory = ({ return importedSecrets.map((el) => ({ ...el, secrets: el.secrets.map((encryptedSecret) => - decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw( + { ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false }, + botKey + ) ) })); }; diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 8211180e8..7b92a463c 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({ ); ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath @@ -270,7 +270,7 @@ export const secretSyncServiceFactory = ({ throw new BadRequestError({ message: "Must specify both source environment and secret path" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: updatedEnvironment, secretPath: updatedSecretPath diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts index 1df64afa2..3b9151557 100644 --- a/backend/src/services/secret-tag/secret-tag-dal.ts +++ b/backend/src/services/secret-tag/secret-tag-dal.ts @@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find all by ids" }); } }; + return { ...secretTagOrm, saveTagsToSecret: secretJnTagOrm.insertMany, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index cc40b0f26..a9f89a4d6 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -102,6 +102,19 @@ export const fnSecretBulkInsert = async ({ [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id })) ); + + const secretTags = await secretTagDAL.find({ + $in: { + id: newSecretTags.map((el) => el.secret_tagsId) + } + }); + + const secretTagsWithSlugs = await secretTagDAL.find({ + $in: { + id: secretTags.map((el) => el.id) + } + }); + const secretVersions = await secretVersionDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, @@ -137,6 +150,7 @@ export const fnSecretBulkInsert = async ({ if (newSecretTags.length) { const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); + const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({ [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretTag}Id` as const]: secret_tagsId @@ -623,13 +637,13 @@ export const reshapeBridgeSecret = ( name: string; }[]; secretMetadata?: ResourceMetadataDTO; - } + }, + secretValueHidden?: boolean ) => ({ secretKey: secret.key, secretPath, workspace: workspaceId, environment, - secretValue: secret.value || "", secretComment: secret.comment || "", version: secret.version, type: secret.type, @@ -643,5 +657,15 @@ export const reshapeBridgeSecret = ( metadata: secret.metadata, secretMetadata: secret.secretMetadata, createdAt: secret.createdAt, - updatedAt: secret.updatedAt + updatedAt: secret.updatedAt, + + ...(secretValueHidden + ? { + secretValue: "", + secretValueHidden: true + } + : { + secretValue: secret.value || "", + secretValueHidden: false + }) }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 0ffb0ea4c..b2c3f5937 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -11,12 +11,16 @@ import { TSecretsV2 } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenReadSecretError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { diff, groupBy } from "@app/lib/fn"; import { setKnexStringValue } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; @@ -252,7 +256,7 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, type, ...inputSecretData } = inputSecret; ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -271,8 +275,8 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const secret = await secretDAL.transaction((tx) => - fnSecretBulkInsert({ + const secret = await secretDAL.transaction(async (tx) => { + const [createdSecret] = await fnSecretBulkInsert({ folderId, orgId: actorOrgId, inputSecrets: [ @@ -302,8 +306,10 @@ export const secretV2BridgeServiceFactory = ({ secretTagDAL, secretVersionTagDAL, tx - }) - ); + }); + + return createdSecret; + }); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); @@ -318,7 +324,7 @@ export const secretV2BridgeServiceFactory = ({ } return reshapeBridgeSecret(projectId, environment, secretPath, { - ...secret[0], + ...secret, value: inputSecret.secretValue, comment: inputSecret.secretComment || "" }); @@ -390,7 +396,7 @@ export const secretV2BridgeServiceFactory = ({ } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -407,7 +413,7 @@ export const secretV2BridgeServiceFactory = ({ // now check with new ids ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -424,7 +430,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -507,11 +513,27 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...updatedSecret[0], - value: inputSecret.secretValue || "", - comment: inputSecret.secretComment || "" - }); + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, // ! Note(Daniel): We are checking for the EXISTING secret name, not the new secret name. + secretTags: tags?.map((el) => el.slug) // ! Note(Daniel): Same here + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...updatedSecret[0], + value: inputSecret.secretValue || "", + comment: inputSecret.secretComment || "" + }, + secretValueHidden + ); }; const deleteSecret = async ({ @@ -557,7 +579,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -599,15 +621,32 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...deletedSecret[0], - value: deletedSecret[0].encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() - : "", - comment: deletedSecret[0].encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() - : "" - }); + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...deletedSecret[0], + value: deletedSecret[0].encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() + : "", + comment: deletedSecret[0].encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() + : "" + }, + secretValueHidden + ); }; // get unique secrets count for multiple envs @@ -635,7 +674,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -682,7 +724,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -693,7 +738,7 @@ export const secretV2BridgeServiceFactory = ({ }; const getSecretsByFolderMappings = async ( - { projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO, + { projectId, userId, filters, folderMappings, filterByAction }: TGetSecretsRawByFolderMappingsDTO, projectPermission: Awaited>["permission"] ) => { const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); @@ -710,10 +755,13 @@ export const secretV2BridgeServiceFactory = ({ projectId }); + // eslint-disable-next-line no-param-reassign + if (!filterByAction) filterByAction = ProjectPermissionSecretActions.ReadValue; + const decryptedSecrets = secrets .filter((el) => projectPermission.can( - ProjectPermissionActions.Read, + filterByAction as ProjectPermissionSecretActions, // ? Typescript assumes that filterByAction may be undefined, which is not true, so we are casting it to ProjectPermissionSecretActions subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, secretPath: groupedFolderMappings[el.folderId][0].path, @@ -722,8 +770,19 @@ export const secretV2BridgeServiceFactory = ({ }) ) ) - .map((secret) => - reshapeBridgeSecret( + .map((secret) => { + // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. + const secretValueHidden = !projectPermission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( projectId, groupedFolderMappings[secret.folderId][0].environment, groupedFolderMappings[secret.folderId][0].path, @@ -735,9 +794,10 @@ export const secretV2BridgeServiceFactory = ({ comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" - } - ) - ); + }, + secretValueHidden + ); + }); return decryptedSecrets; }; @@ -766,7 +826,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -786,7 +849,8 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderMappings, filters: params, - userId: actorId + userId: actorId, + filterByAction: ProjectPermissionSecretActions.DescribeSecret }, permission ); @@ -801,6 +865,7 @@ export const secretV2BridgeServiceFactory = ({ projectId, actor, actorOrgId, + viewSecretValue, actorAuthMethod, includeImports, recursive, @@ -816,7 +881,14 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretTags: params.tagSlugs + }) + ); let paths: { folderId: string; path: string }[] = []; @@ -854,28 +926,58 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => - permission.can( - ProjectPermissionActions.Read, + .filter((el) => { + if (viewSecretValue) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + } + + return permission.can( + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, secretTags: el.tags.map((i) => i.slug) }) - ) - ) - .map((secret) => - reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }) - ); + ); + }) + .map((secret) => { + const secretValueHidden = + !viewSecretValue || + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + groupedPaths[secret.folderId][0].path, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); + }); const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, @@ -883,8 +985,9 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + // ? Question(Daniel): Will throw an error if the user doesn't have access to any of the expanded secrets. permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -929,16 +1032,28 @@ export const secretV2BridgeServiceFactory = ({ secretImportDAL, expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return ( + permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ) && + permission.can( + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ) + ); + } }); return { @@ -958,6 +1073,7 @@ export const secretV2BridgeServiceFactory = ({ type, secretName, version, + viewSecretValue, includeImports, expandSecretReferences: shouldExpandSecretReferences }: TGetASecretDTO) => { @@ -1019,7 +1135,7 @@ export const secretV2BridgeServiceFactory = ({ )); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, @@ -1028,26 +1144,31 @@ export const secretV2BridgeServiceFactory = ({ }) ); + // this will throw if the user doesn't have read value permission no matter what + // because if its an expansion, it will fully depend on the value. const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return permission.can( + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ) + ); + } }); // now if secret is not found // then search for imported secrets // here we consider the import order also thus starting from bottom + + // currently filters out the secrets that the user doesn't have access to read value on if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ @@ -1057,16 +1178,28 @@ export const secretV2BridgeServiceFactory = ({ secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return ( + permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ) && + permission.can( + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ) + ); + } }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { @@ -1099,13 +1232,41 @@ export const secretV2BridgeServiceFactory = ({ secretValue = expandedSecretValue || ""; } - return reshapeBridgeSecret(projectId, environment, path, { - ...secret, - value: secretValue, - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }); + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ) + ) { + throw new ForbiddenReadSecretError({ + message: `You do not have permission to view secret value on secret with name '${secretName}'` + }); + } + + secretValueHidden = false; + } + + return reshapeBridgeSecret( + projectId, + environment, + path, + { + ...secret, + value: secretValue, + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); }; const createManySecret = async ({ @@ -1173,7 +1334,7 @@ export const secretV2BridgeServiceFactory = ({ inputSecrets.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1201,8 +1362,8 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const newSecrets = await secretDAL.transaction(async (tx) => - fnSecretBulkInsert({ + const newSecrets = await secretDAL.transaction(async (tx) => { + const createdSecrets = await fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1231,8 +1392,19 @@ export const secretV2BridgeServiceFactory = ({ secretTagDAL, secretVersionTagDAL, tx - }) - ); + }); + + const secs = await secretDAL.find( + { + $in: { + id: createdSecrets.map((el) => el.id) + } + }, + { tx } + ); + + return secs; + }); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -1244,13 +1416,29 @@ export const secretV2BridgeServiceFactory = ({ environmentSlug: folder.environment.slug }); - return newSecrets.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return newSecrets.map((el) => { + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; const updateManySecret = async ({ @@ -1293,7 +1481,17 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const updatedSecrets: Array = []; + const updatedSecrets: Array< + TSecretsV2 & { + secretPath: string; + tags: { + id: string; + slug: string; + color?: string | null; + name: string; + }[]; + } + > = []; await secretDAL.transaction(async (tx) => { for await (const folder of folders) { if (!folder) throw new NotFoundError({ message: "Folder not found" }); @@ -1344,7 +1542,7 @@ export const secretV2BridgeServiceFactory = ({ secretsToUpdateInDB.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1364,7 +1562,7 @@ export const secretV2BridgeServiceFactory = ({ if (updateMode === SecretUpdateMode.Upsert) { secretsToCreate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1378,7 +1576,7 @@ export const secretV2BridgeServiceFactory = ({ // check again to avoid non authorized tags are removed secretsToUpdate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1430,7 +1628,7 @@ export const secretV2BridgeServiceFactory = ({ secretsWithNewName.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1455,7 +1653,7 @@ export const secretV2BridgeServiceFactory = ({ }); await $validateSecretReferences(projectId, permission, secretReferences, tx); - const bulkUpdatedSecrets = await fnSecretBulkUpdate({ + const bulkUpdatedSecretsRes = await fnSecretBulkUpdate({ folderId, orgId: actorOrgId, tx, @@ -1492,9 +1690,21 @@ export const secretV2BridgeServiceFactory = ({ secretVersionTagDAL, resourceMetadataDAL }); + + const bulkUpdatedSecrets = await secretDAL.find( + { + $in: { + id: bulkUpdatedSecretsRes.map((el) => el.id) + } + }, + { + tx + } + ); + updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); if (updateMode === SecretUpdateMode.Upsert) { - const bulkInsertedSecrets = await fnSecretBulkInsert({ + const bulkInsertedSecretsRes = await fnSecretBulkInsert({ inputSecrets: secretsToCreate.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1524,6 +1734,16 @@ export const secretV2BridgeServiceFactory = ({ secretVersionTagDAL, tx }); + + const bulkInsertedSecrets = await secretDAL.find( + { + $in: { + id: bulkInsertedSecretsRes.map((el) => el.id) + } + }, + { tx } + ); + updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); } } @@ -1545,13 +1765,33 @@ export const secretV2BridgeServiceFactory = ({ ) ); - return updatedSecrets.map((el) => - reshapeBridgeSecret(projectId, environment, el.secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return updatedSecrets.map((el) => { + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + return { + ...reshapeBridgeSecret( + projectId, + environment, + el.secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }, + secretValueHidden + ) + }; + }); }; const deleteManySecret = async ({ @@ -1613,7 +1853,7 @@ export const secretV2BridgeServiceFactory = ({ }); secretsToDelete.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1652,13 +1892,35 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - return secretsDeleted.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return secretsDeleted.map((el) => { + const secretToDeleteMatch = secretsToDelete.find( + (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type + ); + + const secretValueHidden = + !secretToDeleteMatch || + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; const getSecretVersions = async ({ @@ -1689,6 +1951,7 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId: folder.projectId }); + const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); return secretVersions.map((el) => reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { @@ -1793,7 +2056,7 @@ export const secretV2BridgeServiceFactory = ({ }); sourceSecrets.forEach((secret) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath, @@ -1876,7 +2139,7 @@ export const secretV2BridgeServiceFactory = ({ // permission check whether can create or edit the ones in the destination folder locallyCreatedSecrets.forEach((secret) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationEnvironment, @@ -1888,7 +2151,7 @@ export const secretV2BridgeServiceFactory = ({ locallyUpdatedSecrets.forEach((secret) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationEnvironment, @@ -2125,7 +2388,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2149,7 +2412,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -2158,7 +2421,7 @@ export const secretV2BridgeServiceFactory = ({ }) ); - const secretValue = secret.encryptedValue + const decryptedSecretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; @@ -2169,7 +2432,7 @@ export const secretV2BridgeServiceFactory = ({ decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -2179,10 +2442,26 @@ export const secretV2BridgeServiceFactory = ({ ) }); + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ) + ) { + throw new ForbiddenRequestError({ + message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.` + }); + } + const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ environment, secretPath, - value: secretValue + value: decryptedSecretValue }); return { tree: stackTrace, value: expandedValue }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index ad8264e81..f3f3ebf0f 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -1,6 +1,7 @@ import { Knex } from "knex"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -36,6 +37,7 @@ export type TGetSecretsDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + viewSecretValue: boolean; metadataFilter?: { key?: string; value?: string; @@ -57,6 +59,7 @@ export type TGetASecretDTO = { includeImports?: boolean; version?: number; projectId: string; + viewSecretValue: boolean; } & Omit; export type TCreateSecretDTO = TProjectPermission & { @@ -166,7 +169,7 @@ export type TFnSecretBulkInsert = { resourceMetadataDAL: Pick; secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; }; @@ -190,7 +193,7 @@ export type TFnSecretBulkUpdate = { resourceMetadataDAL: Pick; secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; tx?: Knex; }; @@ -332,4 +335,5 @@ export type TGetSecretsRawByFolderMappingsDTO = { folderMappings: { folderId: string; path: string; environment: string }[]; userId: string; filters: TFindSecretsByFolderIdsFilter; + filterByAction?: ProjectPermissionSecretActions; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 1775d1f44..22d94dc42 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -13,7 +13,7 @@ import { TSecrets } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -190,7 +190,7 @@ export const recursivelyGetSecretPaths = ({ const allowedPaths = paths.filter( (folder) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: folder.path @@ -344,6 +344,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD export const decryptSecretRaw = ( secret: TSecrets & { + secretValueHidden: boolean; workspace: string; environment: string; secretPath: string; @@ -362,12 +363,14 @@ export const decryptSecretRaw = ( key }); - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); + const secretValue = !secret.secretValueHidden + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }) + : ""; let secretComment = ""; @@ -385,6 +388,7 @@ export const decryptSecretRaw = ( secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, + secretValueHidden: secret.secretValueHidden, secretValue, secretComment, version: secret.version, @@ -1197,3 +1201,25 @@ export const fnDeleteProjectSecretReminders = async ( } } }; + +export const conditionallyHideSecretValue = ( + shouldHideValue: boolean, + { + secretValueCiphertext, + secretValueIV, + secretValueTag + }: { + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + } +) => { + const hiddenPlaceholder = "hidden-by-infisical>"; + + return { + secretValueCiphertext: shouldHideValue ? hiddenPlaceholder : secretValueCiphertext, + secretValueIV: shouldHideValue ? hiddenPlaceholder : secretValueIV, + secretValueTag: shouldHideValue ? hiddenPlaceholder : secretValueTag, + secretValueHidden: shouldHideValue + }; +}; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 93f68e813..9927a14c0 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -13,7 +13,11 @@ import { } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -48,6 +52,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretDALFactory } from "./secret-dal"; import { + conditionallyHideSecretValue, decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkDelete, @@ -204,7 +209,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -322,7 +327,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -444,7 +449,22 @@ export const secretServiceFactory = ({ environmentSlug: folder.environment.slug }); } - return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path }; + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path + }) + ); + + return { + ...updatedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]), + workspace: projectId, + environment, + secretPath: path + }; }; const deleteSecret = async ({ @@ -467,7 +487,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -540,7 +560,19 @@ export const secretServiceFactory = ({ }); } - return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return { + ...deletedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]), + _id: deletedSecret[0].id, + workspace: projectId, + environment, + secretPath: path + }; }; const getSecrets = async ({ @@ -589,7 +621,7 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -614,7 +646,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -671,7 +703,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -721,7 +753,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -739,6 +771,7 @@ export const secretServiceFactory = ({ if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { return { ...importedSecrets[i].secrets[j], + secretValueHidden: false, workspace: projectId, environment: importedSecrets[i].environment, secretPath: importedSecrets[i].secretPath @@ -749,7 +782,13 @@ export const secretServiceFactory = ({ } if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); - return { ...secret, workspace: projectId, environment, secretPath: path }; + return { + ...secret, + secretValueHidden: false, // Always false because we check permission at the beginning of the function + workspace: projectId, + environment, + secretPath: path + }; }; const createManySecret = async ({ @@ -771,7 +810,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -859,7 +898,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -901,8 +940,8 @@ export const secretServiceFactory = ({ if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); const references = await getSecretReference(projectId); - const secrets = await secretDAL.transaction(async (tx) => - fnSecretBulkUpdate({ + const secrets = await secretDAL.transaction(async (tx) => { + const updatedSecrets = await fnSecretBulkUpdate({ folderId, projectId, tx, @@ -932,8 +971,18 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL - }) - ); + }); + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return updatedSecrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); + }); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -967,7 +1016,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -1019,7 +1068,15 @@ export const secretServiceFactory = ({ } } - return secrets; + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return secrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); }); await snapshotService.performSnapshot(folderId); @@ -1180,6 +1237,7 @@ export const secretServiceFactory = ({ secretName, path: secretPath, environment, + viewSecretValue: false, type: "shared" }); @@ -1194,10 +1252,11 @@ export const secretServiceFactory = ({ | (typeof groupPermissions)[number] ) => { const allowedActions = [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Delete, - ProjectPermissionActions.Create, - ProjectPermissionActions.Edit + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit ].filter((action) => entityPermission.permission.can( action, @@ -1234,6 +1293,7 @@ export const secretServiceFactory = ({ actorId, actorOrgId, actorAuthMethod, + viewSecretValue, environment, includeImports, expandSecretReferences, @@ -1249,6 +1309,7 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, + viewSecretValue, environment, path, recursive, @@ -1285,14 +1346,20 @@ export const secretServiceFactory = ({ recursive }); - const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); + const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey)); const filteredSecrets = tagSlugs.length ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) : decryptedSecrets; const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const decryptedImportSecrets = importedSecrets.map((sec) => decryptSecretRaw( - { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, + { + ...sec, + environment: el.environment, + workspace: projectId, + secretPath: el.secretPath, + secretValueHidden: false + }, botKey ) ); @@ -1387,6 +1454,7 @@ export const secretServiceFactory = ({ path, actor, environment, + viewSecretValue, projectId: workspaceId, expandSecretReferences, projectSlug, @@ -1406,6 +1474,7 @@ export const secretServiceFactory = ({ includeImports, actorAuthMethod, path, + viewSecretValue, actorOrgId, actor, actorId, @@ -1436,6 +1505,7 @@ export const secretServiceFactory = ({ message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, name: "bot_not_found_error" }); + const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); if (expandSecretReferences) { @@ -1454,7 +1524,10 @@ export const secretServiceFactory = ({ decryptedSecret.secretValue = expandedSecretValue || ""; } - return { secretMetadata: undefined, ...decryptedSecret }; + return { + secretMetadata: undefined, + ...decryptedSecret + }; }; const createSecretRaw = async ({ @@ -1605,7 +1678,16 @@ export const secretServiceFactory = ({ tags: tagIds }); - return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; + return { + type: SecretProtectionType.Direct as const, + secret: decryptSecretRaw( + { + ...secret, + secretValueHidden: false + }, + botKey + ) + }; }; const updateSecretRaw = async ({ @@ -2001,7 +2083,7 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secrets: secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey) ) }; }; @@ -2307,6 +2389,7 @@ export const secretServiceFactory = ({ return secretVersions.map((el) => decryptSecretRaw( { + secretValueHidden: false, ...el, workspace: folder.projectId, environment: folder.environment.envSlug, @@ -2340,7 +2423,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2446,7 +2529,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2637,29 +2720,33 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath - }) - ); + const permissionChecks = [ + { + action: ProjectPermissionSecretActions.Delete, + subject: { + environment: sourceEnvironment, + secretPath: sourceSecretPath + } + }, + { + action: ProjectPermissionSecretActions.Create, + subject: { + environment: destinationEnvironment, + secretPath: destinationSecretPath + } + }, + { + action: ProjectPermissionSecretActions.Edit, + subject: { + environment: destinationEnvironment, + secretPath: destinationSecretPath + } + } + ] as const; - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); + for (const { action, subject: permissionSubject } of permissionChecks) { + ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, permissionSubject)); + } const { botKey } = await projectBotService.getBotKey(project.id); if (!botKey) { diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 158605276..06de34663 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -180,6 +180,7 @@ export type TGetSecretsRawDTO = { expandSecretReferences?: boolean; path: string; environment: string; + viewSecretValue: boolean; includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; @@ -205,6 +206,7 @@ export type TGetASecretRawDTO = { secretName: string; path: string; environment: string; + viewSecretValue: boolean; expandSecretReferences?: boolean; type: "shared" | "personal"; includeImports?: boolean; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 654917feb..9b87c29f8 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -5,7 +5,11 @@ import bcrypt from "bcrypt"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -67,7 +71,7 @@ export const serviceTokenServiceFactory = ({ scopes.forEach(({ environment, secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); }); diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 6fbe96238..a36d38a75 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -2,6 +2,7 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useSt import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; +import { twMerge } from "tailwind-merge"; import { useWorkspace } from "@app/context"; import { useDebounce, useToggle } from "@app/hooks"; @@ -54,6 +55,7 @@ type Props = Omit, "onChange" | "val secretPath?: string; environment?: string; containerClassName?: string; + secretValueHidden?: boolean; }; type ReferenceItem = { @@ -70,6 +72,7 @@ export const InfisicalSecretInput = forwardRef( containerClassName, secretPath: propSecretPath, environment: propEnvironment, + secretValueHidden, ...props }, ref @@ -275,6 +278,7 @@ export const InfisicalSecretInput = forwardRef( ref={handleRef} onKeyDown={handleKeyDown} value={value} + valueHidden={secretValueHidden} onFocus={() => setIsFocused.on()} onBlur={(evt) => { // should not on blur when its mouse down selecting a item from suggestion @@ -282,7 +286,7 @@ export const InfisicalSecretInput = forwardRef( setIsFocused.off(); }} onChange={(e) => onChange?.(e.target.value)} - containerClassName={containerClassName} + containerClassName={twMerge(containerClassName)} /> { let finalStr = ""; @@ -14,7 +18,26 @@ const replaceContentWithDot = (str: string) => { return finalStr; }; -const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => { +const syntaxHighlight = ( + content?: string | null, + isVisible?: boolean, + isImport?: boolean, + valueHidden?: boolean +) => { + if (valueHidden && !content) + return ( +
+ VALUE HIDDEN +
+ + + +
+
+ ); if (isImport && !content) return "IMPORTED"; if (content === "") return "EMPTY"; if (!content) return "EMPTY"; @@ -50,6 +73,7 @@ type Props = TextareaHTMLAttributes & { isImport?: boolean; isReadOnly?: boolean; isDisabled?: boolean; + valueHidden?: boolean; containerClassName?: string; }; @@ -61,6 +85,7 @@ export const SecretInput = forwardRef( value, isVisible, isImport, + valueHidden, containerClassName, onBlur, isDisabled, @@ -81,7 +106,7 @@ export const SecretInput = forwardRef(
             
               
-                {syntaxHighlight(value, isVisible || isSecretFocused, isImport)}
+                {syntaxHighlight(value, isVisible || isSecretFocused, isImport, valueHidden)}
               
             
           
diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d368a949f..260ffce09 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -7,6 +7,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionSecretActions { + DescribeSecret = "read", + ReadValue = "readValue", + Create = "create", + Edit = "edit", + Delete = "delete" +} + export enum ProjectPermissionDynamicSecretActions { ReadRootCredential = "read-root-credential", CreateRootCredential = "create-root-credential", @@ -138,7 +146,7 @@ export type SecretImportSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ( | ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 4b63be988..f704081a9 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -207,6 +207,7 @@ export const useGetProjectSecretsDetails = ( search = "", includeSecrets, includeFolders, + viewSecretValue, includeImports, includeDynamicSecrets, tags @@ -231,6 +232,7 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, + viewSecretValue, offset, projectId, environment, @@ -247,6 +249,7 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, + viewSecretValue, offset, projectId, environment, diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 786b2b43a..4808b463c 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -69,6 +69,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit< TGetDashboardProjectSecretsOverviewDTO, "environments" > & { + viewSecretValue: boolean; environment: string; includeImports?: boolean; tags: Record; diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index c2ddd2fbe..67780bb34 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -176,6 +176,7 @@ export const useGetImportedSecretsAllEnvs = ({ env: encSecret.environment, key: encSecret.secretKey, value: encSecret.secretValue, + secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index a6dd959b0..5549bdcec 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -68,6 +68,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { env: el.environment, key: el.secretKey, value: el.secretValue, + secretValueHidden: el.secretValueHidden, tags: el.tags || [], comment: el.secretComment || "", reminderRepeatDays: el.secretReminderRepeatDays, diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 92dc220b8..b2287f6fe 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -37,6 +37,7 @@ export type SecretV3RawSanitized = { version: number; key: string; value?: string; + secretValueHidden: boolean; comment?: string; reminderRepeatDays?: number | null; reminderNote?: string | null; @@ -61,6 +62,7 @@ export type SecretV3Raw = { environment: string; version: number; type: string; + secretValueHidden: boolean; secretKey: string; secretPath: string; secretValue?: string; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index de2231fc9..5719d4455 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -35,6 +35,11 @@ export const GeneralPermissionPolicies = ) => { + if (subject === "secrets") { + console.log("secret subject"); + console.log(actions); + } + const { control } = useFormContext(); const items = useFieldArray({ control, @@ -116,25 +121,36 @@ export const GeneralPermissionPolicies = Actions
{actions.map(({ label, value }) => { + if (subject === "secrets") { + console.log("value", value); + } + if (typeof value !== "string") return undefined; + return ( ( -
- - {label} - -
- )} + render={({ field }) => { + if (subject === "secrets") { + console.log("field", field); + } + + return ( +
+ + {label} + +
+ ); + }} /> ); })} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 7dcb118fd..e26ec8bc4 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -9,6 +9,7 @@ import { PermissionConditionOperators, ProjectPermissionDynamicSecretActions, ProjectPermissionKmipActions, + ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, TPermissionCondition, TPermissionConditionOperators @@ -22,6 +23,14 @@ const GeneralPolicyActionSchema = z.object({ create: z.boolean().optional() }); +const SecretPolicyActionSchema = z.object({ + read: z.boolean().optional(), // describe secret + edit: z.boolean().optional(), + delete: z.boolean().optional(), + create: z.boolean().optional(), + readValue: z.boolean().optional() +}); + const CmekPolicyActionSchema = z.object({ read: z.boolean().optional(), edit: z.boolean().optional(), @@ -114,7 +123,7 @@ export const projectRoleFormSchema = z.object({ .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ - [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ + [ProjectPermissionSub.Secrets]: SecretPolicyActionSchema.extend({ inverted: z.boolean().optional(), conditions: ConditionSchema }) @@ -283,6 +292,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { }); return; } + + if (subject === ProjectPermissionSub.Secrets) { + const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret); + const canEdit = action.includes(ProjectPermissionSecretActions.Edit); + const canDelete = action.includes(ProjectPermissionSecretActions.Delete); + const canCreate = action.includes(ProjectPermissionSecretActions.Create); + const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + read: canRead, + create: canCreate, + edit: canEdit, + delete: canDelete, + readValue: canReadValue, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + + return; + } + // for other subjects const canRead = action.includes(ProjectPermissionActions.Read); const canEdit = action.includes(ProjectPermissionActions.Edit); @@ -483,8 +514,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Secrets]: { title: "Secrets", actions: [ - { label: "Read", value: "read" }, + { label: "Describe Secret", value: "read" }, { label: "Create", value: "create" }, + { label: "Read Value", value: "readValue" }, { label: "Modify", value: "edit" }, { label: "Remove", value: "delete" } ] diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index d3dfd49e7..f22a87722 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -39,6 +39,7 @@ type Props = { isVisible?: boolean; isImportedSecret: boolean; environment: string; + secretValueHidden: boolean; secretPath: string; onSecretCreate: (env: string, key: string, value: string) => Promise; onSecretUpdate: ( @@ -58,6 +59,7 @@ export const SecretEditRow = ({ isImportedSecret, onSecretUpdate, secretName, + secretValueHidden, onSecretCreate, onSecretDelete, environment, @@ -151,6 +153,7 @@ export const SecretEditRow = ({ value={field.value as string} key="secret-input" isVisible={isVisible} + secretValueHidden={secretValueHidden} secretPath={secretPath} environment={environment} isImport={isImportedSecret} @@ -158,6 +161,7 @@ export const SecretEditRow = ({ )} />
+
{ const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; const canReadSecret = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -111,6 +112,17 @@ const Page = () => { secretTags: ["*"] }) ); + const canReadSecretValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: "*", + secretTags: ["*"] + }) + ); + + console.log("Can read secret value", canReadSecret); const canReadSecretImports = permission.can( ProjectPermissionActions.Read, @@ -176,6 +188,7 @@ const Page = () => { orderDirection, includeImports: canReadSecretImports && filter.include.import, includeFolders: filter.include.folder, + viewSecretValue: canReadSecretValue, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 168cd6f43..889a3b285 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -52,6 +52,8 @@ import { ProjectType } from "@app/hooks/api/workspace/types"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { useEffect } from "react"; type Props = { isOpen?: boolean; @@ -122,7 +124,7 @@ export const SecretDetailSidebar = ({ const selectTagSlugs = selectedTags.map((i) => i.slug); const cannotEditSecret = permission.cannot( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -130,16 +132,31 @@ export const SecretDetailSidebar = ({ secretTags: selectTagSlugs }) ); + + const cannotReadSecretValue = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretKey, + secretTags: selectTagSlugs + }) + ); + const isReadOnly = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: secretKey, secretTags: selectTagSlugs }) - ) && cannotEditSecret; + ) && + cannotEditSecret && + cannotReadSecretValue; + + console.log("cannotReadSecretValue", cannotReadSecretValue); const overrideAction = watch("overrideAction"); const isOverridden = @@ -261,7 +278,14 @@ export const SecretDetailSidebar = ({ key="secret-value" control={control} render={({ field }) => ( - +
{actions.map(({ label, value }) => { - if (subject === "secrets") { - console.log("value", value); - } - if (typeof value !== "string") return undefined; return ( @@ -134,10 +125,6 @@ export const GeneralPermissionPolicies = { - if (subject === "secrets") { - console.log("field", field); - } - return (
Date: Wed, 19 Feb 2025 08:36:38 +0400 Subject: [PATCH 22/54] fix: updated migration to support additional privileges --- ...kfill-secret-permissions-with-readvalue.ts | 120 +++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts index b29c2901b..c672a1a7d 100644 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -31,6 +31,12 @@ export const unpackPermissions = (permissions: unknown) => export async function up(knex: Knex): Promise { const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); + const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( + selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) + ); + const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( + selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) + ); for await (const projectRole of projectRoles) { const { permissions } = projectRole; @@ -59,10 +65,72 @@ export async function up(knex: Knex): Promise { .update({ permissions: JSON.stringify(repackedPermissions) }); } } + + for await (const identityAdditionalPrivilege of projectIdentityAdditionalPrivileges) { + const { permissions } = identityAdditionalPrivilege; + + const parsedPermissions = unpackPermissions(permissions); + let shouldUpdate = false; + + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { + action.push(SecretActions.ReadValue); + parsedPermissions[i] = { ...parsedPermission, action }; + shouldUpdate = true; + } + } + } + + if (shouldUpdate) { + const repackedPermissions = packRules(parsedPermissions); + + await knex(TableName.IdentityProjectAdditionalPrivilege) + .where("id", identityAdditionalPrivilege.id) + .update({ permissions: JSON.stringify(repackedPermissions) }); + } + } + + for await (const userAdditionalPrivilege of projectUserAdditionalPrivileges) { + const { permissions } = userAdditionalPrivilege; + + const parsedPermissions = unpackPermissions(permissions); + let shouldUpdate = false; + + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { + action.push(SecretActions.ReadValue); + parsedPermissions[i] = { ...parsedPermission, action }; + shouldUpdate = true; + } + } + } + + if (shouldUpdate) { + const repackedPermissions = packRules(parsedPermissions); + + await knex(TableName.ProjectUserAdditionalPrivilege) + .where("id", userAdditionalPrivilege.id) + .update({ permissions: JSON.stringify(repackedPermissions) }); + } + } } export async function down(knex: Knex): Promise { const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); + const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( + selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) + ); + const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( + selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) + ); for await (const projectRole of projectRoles) { const { permissions } = projectRole; @@ -76,7 +144,7 @@ export async function down(knex: Knex): Promise { if (subject === ProjectPermissionSub.Secrets) { if (action.includes(SecretActions.ReadValue)) { - action.splice(action.indexOf(SecretActions.ReadValue), 1); + action.splice(action.indexOf(SecretActions.ReadValue)); parsedPermissions[i] = { ...parsedPermission, action }; } } @@ -88,4 +156,54 @@ export async function down(knex: Knex): Promise { .where("id", projectRole.id) .update({ permissions: JSON.stringify(repackedPermissions) }); } + + for await (const identityAdditionalPrivilege of identityAdditionalPrivileges) { + const { permissions } = identityAdditionalPrivilege; + + const parsedPermissions = unpackPermissions(permissions); + + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.ReadValue)) { + action.splice(action.indexOf(SecretActions.ReadValue)); + parsedPermissions[i] = { ...parsedPermission, action }; + } + } + } + + const repackedPermissions = packRules(parsedPermissions); + + await knex(TableName.IdentityProjectAdditionalPrivilege) + .where("id", identityAdditionalPrivilege.id) + .update({ permissions: JSON.stringify(repackedPermissions) }); + } + + for await (const userAdditionalPrivilege of userAdditionalPrivileges) { + const { permissions } = userAdditionalPrivilege; + + const parsedPermissions = unpackPermissions(permissions); + + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.ReadValue)) { + action.splice(action.indexOf(SecretActions.ReadValue)); + parsedPermissions[i] = { ...parsedPermission, action }; + } + } + } + + const repackedPermissions = packRules(parsedPermissions); + + await knex(TableName.ProjectUserAdditionalPrivilege) + .where("id", userAdditionalPrivilege.id) + .update({ permissions: JSON.stringify(repackedPermissions) }); + } } From 1ff615913c1dc939a9544b2db480ba5119116d01 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 19 Feb 2025 09:41:52 +0400 Subject: [PATCH 23/54] fix: bulk secret create --- .../src/services/secret-v2-bridge/secret-v2-bridge-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 466c92034..d8c152451 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1400,7 +1400,7 @@ export const secretV2BridgeServiceFactory = ({ const secs = await secretDAL.find( { $in: { - id: createdSecrets.map((el) => el.id) + [`${TableName.SecretV2}.id` as "id"]: createdSecrets.map((el) => el.id) } }, { tx } From bf0946141672505273163591da482de82446eb66 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 19 Feb 2025 09:47:53 +0400 Subject: [PATCH 24/54] Update secret-v2-bridge-service.ts --- .../src/services/secret-v2-bridge/secret-v2-bridge-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index d8c152451..d32fef605 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1697,7 +1697,7 @@ export const secretV2BridgeServiceFactory = ({ const bulkUpdatedSecrets = await secretDAL.find( { $in: { - id: bulkUpdatedSecretsRes.map((el) => el.id) + [`${TableName.SecretV2}.id` as "id"]: bulkUpdatedSecretsRes.map((el) => el.id) } }, { From 21bdecdf2a3c231797e1e26dcb61dfd970aec0d2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 19 Feb 2025 09:55:01 +0400 Subject: [PATCH 25/54] Update secret-v2-bridge-service.ts --- .../src/services/secret-v2-bridge/secret-v2-bridge-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index d32fef605..1dc4c540a 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1741,7 +1741,7 @@ export const secretV2BridgeServiceFactory = ({ const bulkInsertedSecrets = await secretDAL.find( { $in: { - id: bulkInsertedSecretsRes.map((el) => el.id) + [`${TableName.SecretV2}.id` as "id"]: bulkInsertedSecretsRes.map((el) => el.id) } }, { tx } From 50ee60a3ea97408249affb4e12831dbba6f963bf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 19 Feb 2025 10:01:07 +0400 Subject: [PATCH 26/54] Update service-token.spec.ts --- backend/e2e-test/routes/v2/service-token.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index 6cc8f6e34..c6bc84673 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12 const createServiceToken = async ( scopes: { environment: string; secretPath: string }[], - permissions: ("read" | "write")[] + permissions: ("read" | "write" | "readValue")[] ) => { const projectKeyRes = await testServer.inject({ method: "GET", @@ -139,7 +139,7 @@ describe("Service token secret ops", async () => { beforeAll(async () => { serviceToken = await createServiceToken( [{ secretPath: "/**", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "write", "readValue"] ); // this is ensure cli service token decryptiong working fine From 3e8f02a4f9e99aab552f437b64adb81025d25056 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 19 Feb 2025 10:13:11 +0400 Subject: [PATCH 27/54] Update service-token.spec.ts --- backend/e2e-test/routes/v2/service-token.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index c6bc84673..bfe043523 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -496,7 +496,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret path access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "readValue", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -518,7 +518,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret environment access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "readValue", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -540,7 +540,7 @@ describe("Service token fail cases", async () => { test("Unauthorized write operation", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read"] + ["read", "readValue"] ); const writeSecrets = await testServer.inject({ method: "POST", From f0b05c683bf831e08baf5c95d30117da9d7f06bc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 19 Feb 2025 20:22:40 +0400 Subject: [PATCH 28/54] fix: service token creation --- backend/src/server/routes/v2/service-token-router.ts | 2 +- backend/src/services/service-token/service-token-types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index fb10f17db..1a84c9123 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => iv: z.string().trim(), tag: z.string().trim(), expiresIn: z.number().nullable(), - permissions: z.enum(["read", "write"]).array() + permissions: z.enum(["read", "write", "readValue"]).array() }), response: { 200: z.object({ diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts index f1a700908..d06d90a9d 100644 --- a/backend/src/services/service-token/service-token-types.ts +++ b/backend/src/services/service-token/service-token-types.ts @@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = { iv: string; tag: string; expiresIn?: number | null; - permissions: ("read" | "write")[]; + permissions: ("read" | "write" | "readValue")[]; } & TProjectPermission; export type TGetServiceTokenInfoDTO = Omit; From f28f4f75612d0704098e37d8e4245a0d64b97ec3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 20 Feb 2025 04:45:52 +0400 Subject: [PATCH 29/54] fix: requested changes --- ...kfill-secret-permissions-with-readvalue.ts | 289 +++++++++--------- .../services/permission/project-permission.ts | 3 +- 2 files changed, 151 insertions(+), 141 deletions(-) diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts index c672a1a7d..950f9ad37 100644 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -16,7 +16,7 @@ enum SecretActions { ReadValue = "readValue" } -export const UnpackedPermissionSchema = z.object({ +const UnpackedPermissionSchema = z.object({ subject: z .union([z.string().min(1), z.string().array()]) .transform((el) => (typeof el !== "string" ? el[0] : el)) @@ -26,9 +26,59 @@ export const UnpackedPermissionSchema = z.object({ inverted: z.boolean().optional() }); -export const unpackPermissions = (permissions: unknown) => +const $unpackPermissions = (permissions: unknown) => UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[])); +const $updatePermissionsUp = (permissions: unknown) => { + const parsedPermissions = $unpackPermissions(permissions); + let shouldUpdate = false; + + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { + action.push(SecretActions.ReadValue); + parsedPermissions[i] = { ...parsedPermission, action }; + shouldUpdate = true; + } + } + } + + return { + parsedPermissions, + shouldUpdate + }; +}; + +const $updatePermissionsDown = (permissions: unknown) => { + const parsedPermissions = $unpackPermissions(permissions); + + let shouldUpdate = false; + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.ReadValue)) { + action.splice(action.indexOf(SecretActions.ReadValue)); + parsedPermissions[i] = { ...parsedPermission, action }; + + shouldUpdate = true; + } + } + } + + const repackedPermissions = packRules(parsedPermissions); + + return { + repackedPermissions, + shouldUpdate + }; +}; + export async function up(knex: Knex): Promise { const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( @@ -38,88 +88,62 @@ export async function up(knex: Knex): Promise { selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) ); - for await (const projectRole of projectRoles) { - const { permissions } = projectRole; - - const parsedPermissions = unpackPermissions(permissions); - let shouldUpdate = false; - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { - action.push(SecretActions.ReadValue); - parsedPermissions[i] = { ...parsedPermission, action }; - shouldUpdate = true; - } - } - } + const updatedRoles = projectRoles.reduce((acc, projectRole) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions); if (shouldUpdate) { - const repackedPermissions = packRules(parsedPermissions); - - await knex(TableName.ProjectRoles) - .where("id", projectRole.id) - .update({ permissions: JSON.stringify(repackedPermissions) }); + acc.push({ + ...projectRole, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); } + return acc; + }, []); + + const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce< + typeof projectIdentityAdditionalPrivileges + >((acc, identityAdditionalPrivilege) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...identityAdditionalPrivilege, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce< + typeof projectUserAdditionalPrivileges + >((acc, userAdditionalPrivilege) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...userAdditionalPrivilege, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + if (updatedRoles.length > 0) { + await knex(TableName.ProjectRoles).insert(updatedRoles).onConflict("id").merge(["permissions"]); } - for await (const identityAdditionalPrivilege of projectIdentityAdditionalPrivileges) { - const { permissions } = identityAdditionalPrivilege; - - const parsedPermissions = unpackPermissions(permissions); - let shouldUpdate = false; - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { - action.push(SecretActions.ReadValue); - parsedPermissions[i] = { ...parsedPermission, action }; - shouldUpdate = true; - } - } - } - - if (shouldUpdate) { - const repackedPermissions = packRules(parsedPermissions); - - await knex(TableName.IdentityProjectAdditionalPrivilege) - .where("id", identityAdditionalPrivilege.id) - .update({ permissions: JSON.stringify(repackedPermissions) }); - } + if (updatedIdentityAdditionalPrivileges.length > 0) { + await knex(TableName.IdentityProjectAdditionalPrivilege) + .insert(updatedIdentityAdditionalPrivileges) + .onConflict("id") + .merge(["permissions"]); } - for await (const userAdditionalPrivilege of projectUserAdditionalPrivileges) { - const { permissions } = userAdditionalPrivilege; - - const parsedPermissions = unpackPermissions(permissions); - let shouldUpdate = false; - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { - action.push(SecretActions.ReadValue); - parsedPermissions[i] = { ...parsedPermission, action }; - shouldUpdate = true; - } - } - } - - if (shouldUpdate) { - const repackedPermissions = packRules(parsedPermissions); - - await knex(TableName.ProjectUserAdditionalPrivilege) - .where("id", userAdditionalPrivilege.id) - .update({ permissions: JSON.stringify(repackedPermissions) }); - } + if (updatedUserAdditionalPrivileges.length > 0) { + await knex(TableName.ProjectUserAdditionalPrivilege) + .insert(updatedUserAdditionalPrivileges) + .onConflict("id") + .merge(["permissions"]); } } @@ -132,78 +156,63 @@ export async function down(knex: Knex): Promise { selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) ); - for await (const projectRole of projectRoles) { - const { permissions } = projectRole; + const updatedRoles = projectRoles.reduce((acc, projectRole) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions); - const parsedPermissions = unpackPermissions(permissions); - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.ReadValue)) { - action.splice(action.indexOf(SecretActions.ReadValue)); - parsedPermissions[i] = { ...parsedPermission, action }; - } - } + if (shouldUpdate) { + acc.push({ + ...projectRole, + permissions: JSON.stringify(repackedPermissions) + }); } + return acc; + }, []); - const repackedPermissions = packRules(parsedPermissions); + const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce( + (acc, identityAdditionalPrivilege) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions); - await knex(TableName.ProjectRoles) - .where("id", projectRole.id) - .update({ permissions: JSON.stringify(repackedPermissions) }); + if (shouldUpdate) { + acc.push({ + ...identityAdditionalPrivilege, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, + [] + ); + + const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce( + (acc, userAdditionalPrivilege) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...userAdditionalPrivilege, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, + [] + ); + + if (updatedRoles.length > 0) { + await knex(TableName.ProjectRoles).insert(updatedRoles).onConflict("id").merge(["permissions"]); } - for await (const identityAdditionalPrivilege of identityAdditionalPrivileges) { - const { permissions } = identityAdditionalPrivilege; - - const parsedPermissions = unpackPermissions(permissions); - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.ReadValue)) { - action.splice(action.indexOf(SecretActions.ReadValue)); - parsedPermissions[i] = { ...parsedPermission, action }; - } - } - } - - const repackedPermissions = packRules(parsedPermissions); - + if (updatedIdentityAdditionalPrivileges.length > 0) { await knex(TableName.IdentityProjectAdditionalPrivilege) - .where("id", identityAdditionalPrivilege.id) - .update({ permissions: JSON.stringify(repackedPermissions) }); + .insert(updatedIdentityAdditionalPrivileges) + .onConflict("id") + .merge(["permissions"]); } - for await (const userAdditionalPrivilege of userAdditionalPrivileges) { - const { permissions } = userAdditionalPrivilege; - - const parsedPermissions = unpackPermissions(permissions); - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.ReadValue)) { - action.splice(action.indexOf(SecretActions.ReadValue)); - parsedPermissions[i] = { ...parsedPermission, action }; - } - } - } - - const repackedPermissions = packRules(parsedPermissions); - + if (updatedUserAdditionalPrivileges.length > 0) { await knex(TableName.ProjectUserAdditionalPrivilege) - .where("id", userAdditionalPrivilege.id) - .update({ permissions: JSON.stringify(repackedPermissions) }); + .insert(updatedUserAdditionalPrivileges) + .onConflict("id") + .merge(["permissions"]); } } diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index f5218c0d6..7508d9d51 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -437,11 +437,12 @@ const GeneralPermissionSchema = [ }) ]; +// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only. export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV1Schema.describe( From 2a8bd74e8800337a1b0181847a4d87bb8858a876 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 20 Feb 2025 04:52:40 +0400 Subject: [PATCH 30/54] Update 20250218020306_backfill-secret-permissions-with-readvalue.ts --- ...0218020306_backfill-secret-permissions-with-readvalue.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts index 950f9ad37..216cf7ede 100644 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -62,8 +62,10 @@ const $updatePermissionsDown = (permissions: unknown) => { const { subject, action } = parsedPermission; if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.ReadValue)) { - action.splice(action.indexOf(SecretActions.ReadValue)); + const readValueIndex = action.indexOf(SecretActions.ReadValue); + + if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) { + action.splice(readValueIndex, 1); parsedPermissions[i] = { ...parsedPermission, action }; shouldUpdate = true; From b00413817d5ed3207bcf778806312631d1329673 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 20 Feb 2025 05:17:45 +0400 Subject: [PATCH 31/54] fix: add service token read value permissions --- ...kfill-secret-permissions-with-readvalue.ts | 62 +++++++++++++++++++ .../AddServiceTokenModal.tsx | 8 ++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts index 216cf7ede..c42f4d7bc 100644 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -89,6 +89,37 @@ export async function up(knex: Knex): Promise { const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) ); + const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); + + const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { + const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only. + + if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) { + permissions.push(SecretActions.ReadValue); + acc.push({ + ...serviceToken, + permissions + }); + } + return acc; + }, []); + + if (updatedServiceTokens.length > 0) { + await knex(TableName.ServiceToken) + .whereIn( + "id", + updatedServiceTokens.map((t) => t.id) + ) + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${updatedServiceTokens.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + updatedServiceTokens.map((t) => t.permissions) + ) + }); + } const updatedRoles = projectRoles.reduce((acc, projectRole) => { const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions); @@ -157,6 +188,37 @@ export async function down(knex: Knex): Promise { const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) ); + const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); + + const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { + const { permissions } = serviceToken; + + if (permissions.includes(SecretActions.ReadValue)) { + permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1); + acc.push({ + ...serviceToken, + permissions + }); + } + return acc; + }, []); + + if (updatedServiceTokens.length > 0) { + await knex(TableName.ServiceToken) + .whereIn( + "id", + updatedServiceTokens.map((t) => t.id) + ) + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${updatedServiceTokens.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + updatedServiceTokens.map((t) => t.permissions) + ) + }); + } const updatedRoles = projectRoles.reduce((acc, projectRole) => { const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions); diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index 46a9c5680..a669a8360 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -58,6 +58,7 @@ const schema = z.object({ permissions: z .object({ read: z.boolean(), + readValue: z.boolean(), write: z.boolean() }) .required() @@ -296,14 +297,19 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { name="permissions" defaultValue={{ read: true, + readValue: false, write: false }} render={({ field: { onChange, value }, fieldState: { error } }) => { const options = [ { - label: "Read (default)", + label: "Describe Secret (default)", value: "read" }, + { + label: "Read Value (optional)", + value: "readValue" + }, { label: "Write (optional)", value: "write" From 9cb1a312871f314e1404c2c7a4f9c0562173ee51 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 20 Feb 2025 05:17:59 +0400 Subject: [PATCH 32/54] fix: allow Viewer role to read value --- backend/src/ee/services/permission/project-permission.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7508d9d51..c270b4a93 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -808,8 +808,8 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - // ? Q(Daniel): Should the viewer role be allowed to read values? Currently not allowed in permission below. can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); From 12b447425b4dc545790547fb314f4ce912856ddb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 20 Feb 2025 05:47:54 +0400 Subject: [PATCH 33/54] chore: further cleanup --- backend/src/lib/errors/index.ts | 7 ------- backend/src/server/routes/v3/secret-router.ts | 4 ++-- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 3 ++- .../secret-v2-bridge/secret-v2-bridge-service.ts | 8 ++++---- backend/src/services/secret/secret-fns.ts | 12 ++++++------ 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index 6b917263f..7dbffed20 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -60,13 +60,6 @@ export class ForbiddenRequestError extends Error { } } -export class ForbiddenReadSecretError extends ForbiddenRequestError { - constructor({ error, message }: { message?: string; error?: unknown } = {}) { - super({ message, error }); - this.name = "ForbiddenReadSecretError"; - } -} - export class BadRequestError extends Error { name: string; diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index ff448088e..8262a5eab 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -292,7 +292,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .omit({ createdAt: true, updatedAt: true }) .extend({ - // secretValueHidden: z.boolean(), + // No `secretValueHidden` on imports, because imported secrets that the user doesn't have read value permission on, will be filtered out. + // Therefore all returned secret imports will have the value present. secretMetadata: ResourceMetadataSchema.optional() }) .array() @@ -2322,7 +2323,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - // ? No work needed, does not expose secret value. server.route({ method: "POST", url: "/backfill-secret-references", diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 5b7811222..3bf00c878 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -7,6 +7,7 @@ import { logger } from "@app/lib/logger"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; @@ -649,7 +650,7 @@ export const reshapeBridgeSecret = ( ...(secretValueHidden ? { - secretValue: "", + secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, secretValueHidden: true } : { diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 1dc4c540a..321015d19 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -20,7 +20,7 @@ import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-app import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; -import { BadRequestError, ForbiddenReadSecretError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { diff, groupBy } from "@app/lib/fn"; import { setKnexStringValue } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; @@ -987,7 +987,6 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - // ? Question(Daniel): Will throw an error if the user doesn't have access to any of the expanded secrets. permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { @@ -1249,8 +1248,9 @@ export const secretV2BridgeServiceFactory = ({ ) && secretType !== SecretType.Personal ) { - throw new ForbiddenReadSecretError({ - message: `You do not have permission to view secret value on secret with name '${secretName}'` + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" }); } diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 22d94dc42..084aa2ca1 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -51,6 +51,8 @@ import { TUpdateManySecretsRawFnFactory } from "./secret-types"; +export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = ""; + export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { const appCfg = getConfig(); const secretBlindIndex = await buildSecretBlindIndexFromName({ @@ -370,7 +372,7 @@ export const decryptSecretRaw = ( tag: secret.secretValueTag, key }) - : ""; + : INFISICAL_SECRET_VALUE_HIDDEN_MASK; let secretComment = ""; @@ -1214,12 +1216,10 @@ export const conditionallyHideSecretValue = ( secretValueTag: string; } ) => { - const hiddenPlaceholder = "hidden-by-infisical>"; - return { - secretValueCiphertext: shouldHideValue ? hiddenPlaceholder : secretValueCiphertext, - secretValueIV: shouldHideValue ? hiddenPlaceholder : secretValueIV, - secretValueTag: shouldHideValue ? hiddenPlaceholder : secretValueTag, + secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext, + secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV, + secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag, secretValueHidden: shouldHideValue }; }; From 8447190bf8c1326ce230589bfadc396ce549aaaf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 20 Feb 2025 05:50:27 +0400 Subject: [PATCH 34/54] fix: coderabbit requested changes --- frontend/src/components/v2/SecretInput/SecretInput.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 5bf6e83b7..96f79e65f 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -50,7 +50,6 @@ type Props = TextareaHTMLAttributes & { isImport?: boolean; isReadOnly?: boolean; isDisabled?: boolean; - valueHidden?: boolean; containerClassName?: string; }; @@ -62,7 +61,6 @@ export const SecretInput = forwardRef( value, isVisible, isImport, - valueHidden, containerClassName, onBlur, isDisabled, From 711cc438f6d23d1a93f8d8a5cc20eb1d1c26e433 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 20 Feb 2025 06:13:11 +0400 Subject: [PATCH 35/54] chore: better error --- backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 3bf00c878..4aea143d0 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -519,7 +519,7 @@ export const expandSecretReferencesFactory = ({ const referredValue = await fetchSecret(environment, secretPath, secretKey); if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(environment, secretPath); @@ -538,7 +538,7 @@ export const expandSecretReferencesFactory = ({ const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); From 77e1ccc8d783ba8c80b508b5f1de44ad98fcf26f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 3 Mar 2025 17:07:35 +0400 Subject: [PATCH 36/54] fix: view secret value permission (requested changes) --- .../v1/secret-approval-request-router.ts | 14 +- backend/src/ee/routes/v1/snapshot-router.ts | 2 + .../services/permission/project-permission.ts | 13 +- .../secret-approval-request-service.ts | 2 +- .../secret-replication-service.ts | 1 + .../secret-snapshot-service.ts | 38 +++- .../ssh-certificate/ssh-certificate-schema.ts | 2 + backend/src/server/routes/sanitizedSchemas.ts | 9 + .../src/server/routes/v1/dashboard-router.ts | 77 +++----- backend/src/server/routes/v3/secret-router.ts | 55 +----- .../external-migration-fns.ts | 2 +- .../external-migration-queue.ts | 2 +- .../integration-delete-secret.ts | 3 +- .../secret-import/secret-import-fns.ts | 19 +- .../secret-import/secret-import-service.ts | 5 +- .../services/secret-sync/secret-sync-queue.ts | 3 +- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 23 ++- .../secret-v2-bridge-service.ts | 173 ++++++++++-------- .../secret-v2-bridge-types.ts | 10 +- backend/src/services/secret/secret-queue.ts | 3 +- backend/src/services/secret/secret-service.ts | 18 +- backend/src/services/secret/secret-types.ts | 5 +- .../InfisicalSecretInput.tsx | 3 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 6 +- 24 files changed, 262 insertions(+), 226 deletions(-) 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 653d04d4f..bc25bf165 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -10,7 +10,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; @@ -250,14 +250,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv } }); - const tagSchema = SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .array() - .optional(); - server.route({ method: "GET", url: "/:id", @@ -291,7 +283,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .extend({ op: z.string(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.nullish(), secret: z .object({ @@ -310,7 +302,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv secretKey: z.string(), secretValue: z.string().optional(), secretComment: z.string().optional(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.nullish() }) .optional() diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index fc2d25712..64471393b 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -31,6 +31,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { secretVersions: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true }) .extend({ + secretValueHidden: z.boolean(), secretId: z.string(), tags: SecretTagsSchema.pick({ id: true, @@ -55,6 +56,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); + return { secretSnapshot }; } }); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index c270b4a93..9347d69e7 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -442,7 +442,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV1Schema.describe( @@ -947,7 +947,17 @@ export const backfillPermissionV1SchemaToV2Schema = ( subject: ProjectPermissionSub.SecretImports as const })); + const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({ + subject: ProjectPermissionSub.Secrets as const, + ...el, + action: + el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue) + ? el.action.concat(ProjectPermissionSecretActions.ReadValue) + : el.action + })); + const secretFolderPolicies = secretSubjects + .map(({ subject, ...el }) => ({ ...el, // read permission is not needed anymore @@ -989,6 +999,7 @@ export const backfillPermissionV1SchemaToV2Schema = ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts secretImportPolicies, + secretPolicies, dynamicSecretPolicies, hasReadOnlyFolder.length ? [] : secretFolderPolicies ); 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 5abd47d75..5430558a9 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 @@ -111,7 +111,7 @@ type TSecretApprovalRequestServiceFactoryDep = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 3c25db98c..5fae2675d 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({ folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), + viewSecretValue: true, hasSecretAccess: () => true }); // secrets that gets replicated across imports diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index b7a972d02..35023028f 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -38,6 +38,7 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal"; import { getFullFolderPath } from "./snapshot-service-fns"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; type TSecretSnapshotServiceFactoryDep = { snapshotDAL: TSnapshotDALFactory; @@ -184,7 +185,7 @@ export const secretSnapshotServiceFactory = ({ snapshotDetails = { ...encryptedSnapshotDetails, secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - ForbiddenError.from(permission).throwUnlessCan( + const canReadValue = permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: encryptedSnapshotDetails.environment.slug, @@ -194,12 +195,20 @@ export const secretSnapshotServiceFactory = ({ }) ); + let secretValue = ""; + if (canReadValue) { + secretValue = el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : ""; + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + return { ...el, secretKey: el.key, - secretValue: el.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : "", + secretValueHidden: !canReadValue, + secretValue, secretComment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" @@ -228,7 +237,7 @@ export const secretSnapshotServiceFactory = ({ key: botKey }); - ForbiddenError.from(permission).throwUnlessCan( + const canReadValue = permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: encryptedSnapshotDetails.environment.slug, @@ -238,15 +247,24 @@ export const secretSnapshotServiceFactory = ({ }) ); - return { - ...el, - secretKey, - secretValue: decryptSymmetric128BitHexKeyUTF8({ + let secretValue = ""; + + if (canReadValue) { + secretValue = decryptSymmetric128BitHexKeyUTF8({ ciphertext: el.secretValueCiphertext, iv: el.secretValueIV, tag: el.secretValueTag, key: botKey - }), + }); + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + + return { + ...el, + secretKey, + secretValueHidden: !canReadValue, + secretValue, secretComment: el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext ? decryptSymmetric128BitHexKeyUTF8({ diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts index cf8d59d8c..834a8990a 100644 --- a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + import { SshCertificatesSchema } from "@app/db/schemas"; export const sanitizedSshCertificate = SshCertificatesSchema.pick({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 4d645ac4b..024e44224 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -7,6 +7,7 @@ import { ProjectRolesSchema, ProjectsSchema, SecretApprovalPoliciesSchema, + SecretTagsSchema, UsersSchema } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -232,3 +233,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ kmsCertificateKeyId: true, auditLogsRetentionDays: true }); + +export const SanitizedTagSchema = SecretTagsSchema.pick({ + id: true, + slug: true, + color: true +}).extend({ + name: z.string() +}); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 24dee9c87..7fe602aea 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -15,7 +15,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -119,14 +119,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional(), @@ -416,14 +409,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional(), @@ -605,24 +591,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { - const secretsRaw = await server.services.secret.getSecretsRaw({ - actorId: req.permission.id, - actor: req.permission.type, - viewSecretValue: req.query.viewSecretValue, - actorOrgId: req.permission.orgId, - environment, - actorAuthMethod: req.permission.authMethod, - projectId, - path: secretPath, - orderBy, - orderDirection, - search, - limit: remainingLimit, - offset: adjustedOffset, - tagSlugs: tags - }); - - secrets = secretsRaw.secrets; + secrets = ( + await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + viewSecretValue: req.query.viewSecretValue, + throwOnMissingReadValuePermission: false, + actorOrgId: req.permission.orgId, + environment, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + orderBy, + orderDirection, + search, + limit: remainingLimit, + offset: adjustedOffset, + tagSlugs: tags + }) + ).secrets; await server.services.auditLog.createAuditLog({ projectId, @@ -703,14 +690,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .extend({ secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional() @@ -868,7 +848,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), keys: z.string().trim().transform(decodeURIComponent), - viewSecretValue: booleanSchema.default(true) + viewSecretValue: booleanSchema.default(false) }), response: { 200: z.object({ @@ -877,14 +857,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional() diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 8262a5eab..9e72a4094 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -23,7 +23,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; -import { secretRawSchema } from "../sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; const SecretReferenceNode = z.object({ key: z.string(), @@ -72,7 +72,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { body: z.object({ projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug), environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment), - viewSecretValue: convertStringBoolean(true).describe(SECRETS.ATTACH_TAGS.viewSecretValue), secretPath: z .string() .trim() @@ -84,17 +83,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - }) - ) + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + tags: SanitizedTagSchema.array() + }) }) } }, @@ -148,13 +139,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) }) } @@ -274,14 +259,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().optional(), secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array(), imports: z @@ -292,8 +270,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .omit({ createdAt: true, updatedAt: true }) .extend({ - // No `secretValueHidden` on imports, because imported secrets that the user doesn't have read value permission on, will be filtered out. - // Therefore all returned secret imports will have the value present. + secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional() }) .array() @@ -414,14 +391,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { 200: z.object({ secret: secretRawSchema.extend({ secretValueHidden: z.boolean(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional(), + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.optional() }) }) @@ -845,13 +815,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspace: z.string(), environment: z.string(), secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) .array(), imports: z @@ -945,7 +909,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - viewSecretValue: convertStringBoolean(true), type: z.nativeEnum(SecretType).default(SecretType.Shared), version: z.coerce.number().optional(), include_imports: convertStringBoolean() diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index cd0e1c858..fe0d49ca8 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -31,7 +31,7 @@ export type TImportDataIntoInfisicalDTO = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index 94ca65fac..8aa46b94c 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -27,7 +27,7 @@ export type TExternalMigrationQueueFactoryDep = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index fdefd0e62..4b07245ac 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async ( secretDAL: secretV2BridgeDAL, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d21ad3b9c..edc2efbe8 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -6,6 +6,7 @@ import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; type TSecretImportSecrets = { secretPath: string; @@ -39,6 +40,7 @@ type TSecretImportSecretsV2 = { // akhilmhdh: yes i know you can put ?. // But for somereason ts consider ? and undefined explicit as different just ts things secretValue: string; + secretValueHidden: boolean; secretComment: string; secretMetadata?: ResourceMetadataDTO; })[]; @@ -150,12 +152,14 @@ export const fnSecretsV2FromImports = async ({ secretImportDAL, decryptor, expandSecretReferences, - hasSecretAccess + hasSecretAccess, + viewSecretValue }: { secretImports: (Omit & { importEnv: { id: string; slug: string; name: string }; })[]; folderDAL: Pick; + viewSecretValue: boolean; secretDAL: Pick; secretImportDAL: Pick; decryptor: (value?: Buffer | null) => string; @@ -168,9 +172,11 @@ export const fnSecretsV2FromImports = async ({ hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { const cyclicDetector = new Set(); - const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ - { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } - ]; + const stack: { + secretImports: typeof rootSecretImports; + depth: number; + parentImportedSecrets: (TSecretsV2 & { secretValueHidden: boolean })[]; + }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; const processedImports: TSecretImportSecretsV2[] = []; @@ -229,7 +235,8 @@ export const fnSecretsV2FromImports = async ({ .map((item) => ({ ...item, secretKey: item.key, - secretValue: decryptor(item.encryptedValue), + secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK, + secretValueHidden: !viewSecretValue, secretComment: decryptor(item.encryptedComment), environment: importEnv.slug, workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. @@ -267,6 +274,8 @@ export const fnSecretsV2FromImports = async ({ processedImport.secrets = unique(processedImport.secrets, (i) => i.key); return Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; + const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, secretPath: processedImport.secretPath, diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 3ce5b0115..6c412edc0 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -94,7 +94,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -406,7 +406,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -646,6 +646,7 @@ export const secretImportServiceFactory = ({ const importedSecrets = await fnSecretsV2FromImports({ secretImports, folderDAL, + viewSecretValue: true, secretDAL: secretV2BridgeDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index cdc9540da..8afcf6416 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -249,7 +249,8 @@ export const secretSyncQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 4aea143d0..6eb95e9b9 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -148,7 +148,16 @@ export const fnSecretBulkInsert = async ({ await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); } - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkUpdate = async ({ @@ -286,7 +295,15 @@ export const fnSecretBulkUpdate = async ({ tx ); - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkDelete = async ({ @@ -627,7 +644,7 @@ export const reshapeBridgeSecret = ( }[]; secretMetadata?: ResourceMetadataDTO; }, - secretValueHidden?: boolean + secretValueHidden: boolean ) => ({ secretKey: secret.key, secretPath, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 321015d19..c24b97ff4 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -323,11 +323,17 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...secret, - value: inputSecret.secretValue, - comment: inputSecret.secretComment || "" - }); + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: inputSecret.secretValue, + comment: inputSecret.secretComment || "" + }, + false + ); }; const updateSecret = async ({ @@ -407,9 +413,9 @@ export const secretV2BridgeServiceFactory = ({ // validate tags // fetch all tags and if not same count throw error meaning one was invalid tags - const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; - if ((inputSecret.tagIds || []).length !== tags.length) - throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); + const newTags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; + if ((inputSecret.tagIds || []).length !== newTags.length) + throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` }); // now check with new ids ForbiddenError.from(permission).throwUnlessCan( @@ -418,7 +424,7 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath, secretName: inputSecret.secretName, - secretTags: tags?.map((el) => el.slug) + secretTags: newTags?.map((el) => el.slug) }) ); @@ -435,7 +441,7 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath, secretName: inputSecret.newSecretName, - secretTags: tags?.map((el) => el.slug) + secretTags: newTags?.map((el) => el.slug) }) ); } @@ -513,13 +519,14 @@ export const secretV2BridgeServiceFactory = ({ }); } + const tagsToCheck = newTags?.length ? newTags : secret.tags; const secretValueHidden = !permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath, - secretName: inputSecret.secretName, // ! Note(Daniel): We are checking for the EXISTING secret name, not the new secret name. - secretTags: tags?.map((el) => el.slug) // ! Note(Daniel): Same here + secretName: inputSecret.secretName, + secretTags: tagsToCheck.length ? tagsToCheck.map((el) => el.slug) : undefined }) ); @@ -738,7 +745,13 @@ export const secretV2BridgeServiceFactory = ({ }; const getSecretsByFolderMappings = async ( - { projectId, userId, filters, folderMappings, filterByAction }: TGetSecretsRawByFolderMappingsDTO, + { + projectId, + userId, + filters, + folderMappings, + filterByAction = ProjectPermissionSecretActions.ReadValue + }: TGetSecretsRawByFolderMappingsDTO, projectPermission: Awaited>["permission"] ) => { const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); @@ -755,13 +768,10 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - // eslint-disable-next-line no-param-reassign - if (!filterByAction) filterByAction = ProjectPermissionSecretActions.ReadValue; - const decryptedSecrets = secrets .filter((el) => projectPermission.can( - filterByAction as ProjectPermissionSecretActions, // ? Typescript assumes that filterByAction may be undefined, which is not true, so we are casting it to ProjectPermissionSecretActions + filterByAction, subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, secretPath: groupedFolderMappings[el.folderId][0].path, @@ -870,6 +880,7 @@ export const secretV2BridgeServiceFactory = ({ includeImports, recursive, expandSecretReferences: shouldExpandSecretReferences, + throwOnMissingReadValuePermission = true, ...params }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission({ @@ -927,19 +938,7 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => { - if (viewSecretValue) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); - } - - return permission.can( + const canDescribeSecret = permission.can( ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, @@ -948,6 +947,43 @@ export const secretV2BridgeServiceFactory = ({ secretTags: el.tags.map((i) => i.slug) }) ); + + if (!canDescribeSecret) { + return false; + } + + if (viewSecretValue) { + // Recursive secret, should be filtered out + if (groupedPaths[el.folderId][0].path !== path) { + const canReadRecursiveSecretValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + if (!canReadRecursiveSecretValue) { + return false; + } + } + + if (throwOnMissingReadValuePermission) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + } + } + + return canDescribeSecret; }) .map((secret) => { const isPersonalSecret = secret.userId === actorId && secret.type === SecretType.Personal; @@ -1027,6 +1063,7 @@ export const secretV2BridgeServiceFactory = ({ const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ + viewSecretValue, secretImports: allowedImports, secretDAL, folderDAL, @@ -1174,6 +1211,7 @@ export const secretV2BridgeServiceFactory = ({ const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ secretImports, + viewSecretValue, secretDAL, folderDAL, secretImportDAL, @@ -1207,11 +1245,17 @@ export const secretV2BridgeServiceFactory = ({ for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { const importedSecret = importedSecrets[i].secrets[j]; if (secretName === importedSecret.key) { - return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, { - ...importedSecret, - value: importedSecret.secretValue || "", - comment: importedSecret.secretComment || "" - }); + return reshapeBridgeSecret( + projectId, + importedSecrets[i].environment, + importedSecrets[i].secretPath, + { + ...importedSecret, + value: importedSecret.secretValue || "", + comment: importedSecret.secretComment || "" + }, + importedSecret.secretValueHidden + ); } } } @@ -1365,8 +1409,8 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const newSecrets = await secretDAL.transaction(async (tx) => { - const createdSecrets = await fnSecretBulkInsert({ + const newSecrets = await secretDAL.transaction(async (tx) => + fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1395,19 +1439,8 @@ export const secretV2BridgeServiceFactory = ({ secretTagDAL, secretVersionTagDAL, tx - }); - - const secs = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: createdSecrets.map((el) => el.id) - } - }, - { tx } - ); - - return secs; - }); + }) + ); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -1656,7 +1689,7 @@ export const secretV2BridgeServiceFactory = ({ }); await $validateSecretReferences(projectId, permission, secretReferences, tx); - const bulkUpdatedSecretsRes = await fnSecretBulkUpdate({ + const bulkUpdatedSecrets = await fnSecretBulkUpdate({ folderId, orgId: actorOrgId, tx, @@ -1694,20 +1727,9 @@ export const secretV2BridgeServiceFactory = ({ resourceMetadataDAL }); - const bulkUpdatedSecrets = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: bulkUpdatedSecretsRes.map((el) => el.id) - } - }, - { - tx - } - ); - updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); if (updateMode === SecretUpdateMode.Upsert) { - const bulkInsertedSecretsRes = await fnSecretBulkInsert({ + const bulkInsertedSecrets = await fnSecretBulkInsert({ inputSecrets: secretsToCreate.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1738,15 +1760,6 @@ export const secretV2BridgeServiceFactory = ({ tx }); - const bulkInsertedSecrets = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: bulkInsertedSecretsRes.map((el) => el.id) - } - }, - { tx } - ); - updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); } } @@ -1957,11 +1970,17 @@ export const secretV2BridgeServiceFactory = ({ const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); return secretVersions.map((el) => - reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) + reshapeBridgeSecret( + folder.projectId, + folder.environment.envSlug, + "/", + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + false + ) ); }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index f3f3ebf0f..9cfe49b83 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -38,6 +38,7 @@ export type TGetSecretsDTO = { recursive?: boolean; tagSlugs?: string[]; viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; metadataFilter?: { key?: string; value?: string; @@ -50,6 +51,11 @@ export type TGetSecretsDTO = { keys?: string[]; } & TProjectPermission; +export type TGetSecretsMissingReadValuePermissionDTO = Omit< + TGetSecretsDTO, + "viewSecretValue" | "recursive" | "expandSecretReferences" +>; + export type TGetASecretDTO = { secretName: string; path: string; @@ -167,7 +173,7 @@ export type TFnSecretBulkInsert = { } >; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; @@ -191,7 +197,7 @@ export type TFnSecretBulkUpdate = { data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; }[]; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 00b0e7da8..65e3eaf4f 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -402,7 +402,8 @@ export const secretQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 9927a14c0..5c0806041 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -667,7 +667,15 @@ export const secretServiceFactory = ({ environment, secretPath: groupedPaths[secret.folderId][0].path })), - imports: importedSecrets + imports: importedSecrets.map((el) => { + return { + ...el, + secrets: el.secrets.map((secret) => ({ + ...secret, + secretValueHidden: false + })) + }; + }) }; } @@ -974,7 +982,7 @@ export const secretServiceFactory = ({ }); const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -1299,6 +1307,7 @@ export const secretServiceFactory = ({ expandSecretReferences, recursive, tagSlugs = [], + throwOnMissingReadValuePermission = true, ...paramsV2 }: TGetSecretsRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1310,6 +1319,7 @@ export const secretServiceFactory = ({ actor, actorOrgId, viewSecretValue, + throwOnMissingReadValuePermission, environment, path, recursive, @@ -1318,6 +1328,7 @@ export const secretServiceFactory = ({ tagSlugs, ...paramsV2 }); + return { secrets, imports }; } @@ -1370,6 +1381,7 @@ export const secretServiceFactory = ({ const importedEntries = decryptedImportSecrets.reduce( ( accum: { + secretValueHidden: boolean; secretKey: string; secretPath: string; workspace: string; @@ -1413,6 +1425,7 @@ export const secretServiceFactory = ({ Object.keys(secretsGroupByPath).map((groupedPath) => Promise.allSettled( secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: groupedPath, @@ -1429,6 +1442,7 @@ export const secretServiceFactory = ({ processedImports.map((processedImport) => Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: path, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 06de34663..8ac9f58c0 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -181,6 +181,7 @@ export type TGetSecretsRawDTO = { path: string; environment: string; viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; @@ -411,7 +412,7 @@ export type TCreateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -448,7 +449,7 @@ export type TUpdateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index b244aa5f2..6fbe96238 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -2,7 +2,6 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useSt import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; -import { twMerge } from "tailwind-merge"; import { useWorkspace } from "@app/context"; import { useDebounce, useToggle } from "@app/hooks"; @@ -283,7 +282,7 @@ export const InfisicalSecretInput = forwardRef( setIsFocused.off(); }} onChange={(e) => onChange?.(e.target.value)} - containerClassName={twMerge(containerClassName)} + containerClassName={containerClassName} /> {secretValueHidden ? ( -
+
********
From 8fd5fdbc6ac27dde411bd9ec1e63f4278c2a4f0a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 3 Mar 2025 17:52:39 +0400 Subject: [PATCH 37/54] chore: minor changes --- backend/src/server/routes/v1/dashboard-router.ts | 2 +- .../src/services/secret-v2-bridge/secret-v2-bridge-service.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 7fe602aea..abe998c2e 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { z } from "zod"; -import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionDynamicSecretActions, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index c24b97ff4..533520e61 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -981,6 +981,8 @@ export const secretV2BridgeServiceFactory = ({ }) ); } + // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error. + // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value. } return canDescribeSecret; From abbd427ee21b899ba6ae5b946d330aa88d69219e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 3 Mar 2025 17:53:55 +0400 Subject: [PATCH 38/54] minor lint fixes --- .../src/ee/routes/v1/secret-approval-request-router.ts | 7 +------ .../ee/services/ssh-certificate/ssh-certificate-schema.ts | 2 -- backend/src/server/routes/v3/secret-router.ts | 8 +------- 3 files changed, 2 insertions(+), 15 deletions(-) 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 bc25bf165..1ceb6019b 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -1,11 +1,6 @@ import { z } from "zod"; -import { - SecretApprovalRequestsReviewersSchema, - SecretApprovalRequestsSchema, - SecretTagsSchema, - UsersSchema -} from "@app/db/schemas"; +import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts index 834a8990a..cf8d59d8c 100644 --- a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts @@ -1,5 +1,3 @@ -import { z } from "zod"; - import { SshCertificatesSchema } from "@app/db/schemas"; export const sanitizedSshCertificate = SshCertificatesSchema.pick({ diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 9e72a4094..3bc312c98 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1,13 +1,7 @@ import picomatch from "picomatch"; import { z } from "zod"; -import { - SecretApprovalRequestsSchema, - SecretsSchema, - SecretTagsSchema, - SecretType, - ServiceTokenScopes -} from "@app/db/schemas"; +import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; From 5e7086016043d4448e4821b0b484ce477d62f7a8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 3 Mar 2025 18:45:06 +0400 Subject: [PATCH 39/54] fix: ui bug --- .../SecretListView/SecretDetailSidebar.tsx | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 390963212..b2a784612 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -276,54 +276,56 @@ export const SecretDetailSidebar = ({ key="secret-value" control={control} render={({ field }) => ( - - - - The value of this secret is hidden because you do not have the - read secret value permission. - -
- ) : undefined - } - label="Value" - > - - +
+ + + + The value of this secret is hidden because you do not have the + read secret value permission. + +
+ ) : undefined + } + label="Value" + > +
+ + +
+ +
)} /> )}
-
- -
From a86a951acca1616dad1689cd9be749de7c5f65cc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 3 Mar 2025 19:15:57 +0400 Subject: [PATCH 40/54] Update secret-snapshot-service.ts --- .../ee/services/secret-snapshot/secret-snapshot-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 35023028f..e8eab3017 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -102,7 +102,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -139,7 +139,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); From 96046726b28a183a7af10b38ba133421ce09cffd Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 3 Mar 2025 21:05:04 +0400 Subject: [PATCH 41/54] Update 20250218020306_backfill-secret-permissions-with-readvalue.ts --- ...kfill-secret-permissions-with-readvalue.ts | 88 ++++++++++++------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts index c42f4d7bc..c2cac02aa 100644 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -81,6 +81,8 @@ const $updatePermissionsDown = (permissions: unknown) => { }; }; +const CHUNK_SIZE = 1000; + export async function up(knex: Knex): Promise { const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( @@ -89,6 +91,7 @@ export async function up(knex: Knex): Promise { const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) ); + const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { @@ -105,20 +108,25 @@ export async function up(knex: Knex): Promise { }, []); if (updatedServiceTokens.length > 0) { - await knex(TableName.ServiceToken) - .whereIn( - "id", - updatedServiceTokens.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${updatedServiceTokens.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - updatedServiceTokens.map((t) => t.permissions) + for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { + const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ServiceToken) + .whereIn( + "id", + chunk.map((t) => t.id) ) - }); + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + chunk.map((t) => t.permissions) + ) + }); + } } const updatedRoles = projectRoles.reduce((acc, projectRole) => { @@ -162,21 +170,30 @@ export async function up(knex: Knex): Promise { }, []); if (updatedRoles.length > 0) { - await knex(TableName.ProjectRoles).insert(updatedRoles).onConflict("id").merge(["permissions"]); + for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { + const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); + } } if (updatedIdentityAdditionalPrivileges.length > 0) { - await knex(TableName.IdentityProjectAdditionalPrivilege) - .insert(updatedIdentityAdditionalPrivileges) - .onConflict("id") - .merge(["permissions"]); + for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } } if (updatedUserAdditionalPrivileges.length > 0) { - await knex(TableName.ProjectUserAdditionalPrivilege) - .insert(updatedUserAdditionalPrivileges) - .onConflict("id") - .merge(["permissions"]); + for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } } } @@ -263,20 +280,29 @@ export async function down(knex: Knex): Promise { ); if (updatedRoles.length > 0) { - await knex(TableName.ProjectRoles).insert(updatedRoles).onConflict("id").merge(["permissions"]); + for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { + const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); + } } if (updatedIdentityAdditionalPrivileges.length > 0) { - await knex(TableName.IdentityProjectAdditionalPrivilege) - .insert(updatedIdentityAdditionalPrivileges) - .onConflict("id") - .merge(["permissions"]); + for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } } if (updatedUserAdditionalPrivileges.length > 0) { - await knex(TableName.ProjectUserAdditionalPrivilege) - .insert(updatedUserAdditionalPrivileges) - .onConflict("id") - .merge(["permissions"]); + for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } } } From 4b3efb43b035e09a04e9e657299727abcc7f301e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 00:17:51 +0400 Subject: [PATCH 42/54] fix: view secret value permission (requested changes) --- ...kfill-secret-permissions-with-readvalue.ts | 31 ++- .../src/ee/routes/v1/secret-version-router.ts | 7 +- backend/src/ee/routes/v1/snapshot-router.ts | 10 +- .../services/permission/project-permission.ts | 2 +- .../secret-v2-bridge-service.ts | 121 +++++---- .../secret-v2-bridge/secret-version-dal.ts | 59 ++++- backend/src/services/secret/secret-service.ts | 45 ++-- .../src/services/secret/secret-version-dal.ts | 49 +++- frontend/src/components/v2/Blur/Blur.tsx | 22 ++ frontend/src/components/v2/Blur/index.tsx | 1 + .../src/hooks/api/secretSnapshots/queries.tsx | 2 +- frontend/src/hooks/api/secrets/types.ts | 1 + .../SecretOverviewTableRow/SecretEditRow.tsx | 7 +- .../SecretNoAccessOverviewTableRow.tsx | 3 +- .../SecretListView/SecretDetailSidebar.tsx | 229 ++++++++++-------- .../components/SecretListView/SecretItem.tsx | 11 +- .../SecretListView/SecretNoAccessListView.tsx | 9 +- .../components/SnapshotView/SecretItem.tsx | 19 +- 18 files changed, 412 insertions(+), 216 deletions(-) create mode 100644 frontend/src/components/v2/Blur/Blur.tsx create mode 100644 frontend/src/components/v2/Blur/index.tsx diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts index c2cac02aa..9e567cefe 100644 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -221,20 +221,25 @@ export async function down(knex: Knex): Promise { }, []); if (updatedServiceTokens.length > 0) { - await knex(TableName.ServiceToken) - .whereIn( - "id", - updatedServiceTokens.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${updatedServiceTokens.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - updatedServiceTokens.map((t) => t.permissions) + for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { + const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ServiceToken) + .whereIn( + "id", + chunk.map((t) => t.id) ) - }); + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + chunk.map((t) => t.permissions) + ) + }); + } } const updatedRoles = projectRoles.reduce((acc, projectRole) => { diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index 11443ebfe..a09a05c91 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - secretVersions: secretRawSchema.array() + secretVersions: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }) } }, @@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => offset: req.query.offset, secretId: req.params.secretId }); + return { secretVersions }; } }); diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 64471393b..283b9b31e 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,10 +1,10 @@ import { z } from "zod"; -import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { SecretSnapshotsSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSnapshotRouter = async (server: FastifyZodProvider) => { @@ -33,11 +33,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { .extend({ secretValueHidden: z.boolean(), secretId: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }).array() + tags: SanitizedTagSchema.array() }) .array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(), diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 9347d69e7..3f69c481a 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -442,7 +442,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV1Schema.describe( diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 533520e61..9679107ee 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -417,6 +417,8 @@ export const secretV2BridgeServiceFactory = ({ if ((inputSecret.tagIds || []).length !== newTags.length) throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` }); + const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags; + // now check with new ids ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretActions.Edit, @@ -424,7 +426,9 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath, secretName: inputSecret.secretName, - secretTags: newTags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); @@ -441,7 +445,9 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath, secretName: inputSecret.newSecretName, - secretTags: newTags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); } @@ -519,14 +525,15 @@ export const secretV2BridgeServiceFactory = ({ }); } - const tagsToCheck = newTags?.length ? newTags : secret.tags; const secretValueHidden = !permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.secretName, - secretTags: tagsToCheck.length ? tagsToCheck.map((el) => el.slug) : undefined + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); @@ -1073,26 +1080,27 @@ export const secretV2BridgeServiceFactory = ({ expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return ( - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) && - permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + const canDescribe = permission.can( + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) ); + + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ); + + return viewSecretValue ? canDescribe && canReadValue : canDescribe; } }); @@ -1220,26 +1228,26 @@ export const secretV2BridgeServiceFactory = ({ decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return ( - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) && - permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + const canDescribe = permission.can( + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) ); + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ); + + return viewSecretValue ? canDescribe && canReadValue : canDescribe; } }); @@ -1461,7 +1469,7 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath, secretName: el.key, - secretTags: el.tags.map((i) => i.slug) + secretTags: el.tags?.map((i) => i.slug) }) ); @@ -1970,9 +1978,24 @@ export const secretV2BridgeServiceFactory = ({ projectId: folder.projectId }); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => - reshapeBridgeSecret( + const secretVersions = await secretVersionDAL.findBySecretId(secretId, { + offset, + limit, + sort: [["createdAt", "desc"]] + }); + return secretVersions.map((el) => { + const secretValueHidden = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: folder.environment.envSlug, + secretPath: "/", + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + }) + ); + return reshapeBridgeSecret( folder.projectId, folder.environment.envSlug, "/", @@ -1981,9 +2004,9 @@ export const secretV2BridgeServiceFactory = ({ value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" }, - false - ) - ); + secretValueHidden + ); + }); }; // this is a backfilling API for secret references diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 7772b8518..f974ab71e 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; +import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,6 +12,58 @@ export type TSecretVersionV2DALFactory = ReturnType { const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersionV2) + .where(`${TableName.SecretVersionV2}.secretId`, secretId) + .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretVersionV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -124,6 +176,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { pruneExcessVersions, findLatestVersionMany, bulkUpdate, - findLatestVersionByFolderId + findLatestVersionByFolderId, + findBySecretId }; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 5c0806041..cd8971ded 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -667,15 +667,7 @@ export const secretServiceFactory = ({ environment, secretPath: groupedPaths[secret.folderId][0].path })), - imports: importedSecrets.map((el) => { - return { - ...el, - secrets: el.secrets.map((secret) => ({ - ...secret, - secretValueHidden: false - })) - }; - }) + imports: importedSecrets }; } @@ -2399,19 +2391,42 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => - decryptSecretRaw( + const secretVersions = await secretVersionDAL.findBySecretId(secretId, { + offset, + limit, + sort: [["createdAt", "desc"]] + }); + return secretVersions.map((el) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: botKey + }); + + const secretValueHidden = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: folder.environment.envSlug, + secretPath: "/", + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + }) + ); + + return decryptSecretRaw( { - secretValueHidden: false, + secretValueHidden, ...el, workspace: folder.projectId, environment: folder.environment.envSlug, secretPath: "/" }, botKey - ) - ); + ); + }); }; const attachTags = async ({ diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 8e77858a5..8e4544c19 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; +import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType { const secretVersionOrm = ormify(db, TableName.SecretVersion); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersion) + .where(`${TableName.SecretVersion}.secretId`, secretId) + .leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.SecretVersion)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => { findLatestVersionMany, bulkUpdate, findLatestVersionByFolderId, + findBySecretId, bulkUpdateNoVersionIncrement }; }; diff --git a/frontend/src/components/v2/Blur/Blur.tsx b/frontend/src/components/v2/Blur/Blur.tsx new file mode 100644 index 000000000..bd1ded40a --- /dev/null +++ b/frontend/src/components/v2/Blur/Blur.tsx @@ -0,0 +1,22 @@ +import { twMerge } from "tailwind-merge"; + +import { Tooltip } from "../Tooltip/Tooltip"; + +interface IProps { + className?: string; + tooltipText?: string; +} + +export const Blur = ({ className, tooltipText }: IProps) => { + return ( + +
+ ******** +
+
+ ); +}; diff --git a/frontend/src/components/v2/Blur/index.tsx b/frontend/src/components/v2/Blur/index.tsx new file mode 100644 index 000000000..50fb7ccc6 --- /dev/null +++ b/frontend/src/components/v2/Blur/index.tsx @@ -0,0 +1 @@ +export { Blur } from "./Blur"; diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index 263768777..d273bd5ea 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -75,7 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) => id: secretVersion.secretId, env: data.environment.slug, key: secretVersion.secretKey, - secretValueHidden: false, + secretValueHidden: secretVersion.secretValueHidden, value: secretVersion.secretValue || "", tags: secretVersion.tags, comment: secretVersion.secretComment, diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index e3d83a203..9a419a037 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -98,6 +98,7 @@ export type SecretVersions = { envId: string; secretKey: string; secretValue?: string; + secretValueHidden: boolean; secretComment?: string; tags: WsTag[]; __v: number; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 44e6648ac..c0a3ce2ef 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -25,6 +25,7 @@ import { ModalTrigger, Tooltip } from "@app/components/v2"; +import { Blur } from "@app/components/v2/Blur"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; @@ -143,11 +144,7 @@ export const SecretEditRow = ({
{secretValueHidden ? ( - -
- ******** -
-
+ ) : (
-
NO ACCESS
+
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index b2a784612..8b6650247 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -16,6 +16,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { Link } from "@tanstack/react-router"; import { format } from "date-fns"; +import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -207,9 +208,16 @@ export const SecretDetailSidebar = ({ await onSaveSecret(secret, { ...secret, ...data }, () => reset()); }; - const handleReminderSubmit = async (reminderRepeatDays: number | null | undefined, reminderNote: string | null | undefined) => { - await onSaveSecret(secret, { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, () => { }); - } + const handleReminderSubmit = async ( + reminderRepeatDays: number | null | undefined, + reminderNote: string | null | undefined + ) => { + await onSaveSecret( + secret, + { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, + () => {} + ); + }; const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false); @@ -228,7 +236,7 @@ export const SecretDetailSidebar = ({ if (data) { setValue("reminderRepeatDays", data.days, { shouldDirty: false }); setValue("reminderNote", data.note, { shouldDirty: false }); - handleReminderSubmit(data.days, data.note) + handleReminderSubmit(data.days, data.note); } }} /> @@ -278,6 +286,7 @@ export const SecretDetailSidebar = ({ render={({ field }) => (
@@ -651,51 +660,34 @@ export const SecretDetailSidebar = ({
Version History
- {secretVersion?.map(({ createdAt, secretValue, version, id }) => ( -
-
-
-
-
- v{version} + {secretVersion?.map( + ({ createdAt, secretValue, version, id, secretValueHidden }) => ( +
+
+
+
+
+ v{version} +
+
{format(new Date(createdAt), "Pp")}
-
{format(new Date(createdAt), "Pp")}
-
-
-
-
-
-
-
- Value: +
+
+
-
-
- - + -
- - {secretValue?.replace(/./g, "*")} - +
+ + {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} + - + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.currentTarget + .closest(".group") + ?.classList.add("show-value"); + } + }} + > + + + +
+
+ + setValue("value", secretValue)} + > + + + +
-
- - setValue("value", secretValue)} - > - - - -
-
- ))} + ) + )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 7d2c6fd49..0f4711a8e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -46,6 +46,7 @@ import { } from "@app/components/secrets/SecretReferenceDetails"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { Blur } from "@app/components/v2/Blur"; import { FontAwesomeSpriteName, formSchema, @@ -283,15 +284,7 @@ export const SecretItem = memo( )} /> ) : secretValueHidden ? ( - -
- ******** -
-
+ ) : ( { className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent" />
-
- ******** -
+
))} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx index d6680bcef..076cf3b6d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx @@ -20,6 +20,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { Blur } from "@app/components/v2/Blur"; import { useToggle } from "@app/hooks"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; @@ -120,11 +121,25 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => { Value {isModified && ( - + {preSecret?.secretValueHidden ? ( + + ) : ( + + )} )} - + {postSecret?.secretValueHidden ? ( + + ) : ( + + )} {Boolean(preSecret?.idOverride || postSecret?.idOverride) && ( From 5183f76397ad238050f95eede32a63efe315acd7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 02:29:18 +0400 Subject: [PATCH 43/54] fix: pathing --- .../secret-v2-bridge-service.ts | 19 ++++++++++++++++--- backend/src/services/secret/secret-service.ts | 12 +++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 9679107ee..ff896bf22 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -77,7 +77,13 @@ type TSecretV2BridgeServiceFactoryDep = { projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" + | "findBySecretPath" + | "updateById" + | "findById" + | "findByManySecretPath" + | "find" + | "findBySecretPathMultiEnv" + | "findSecretPathByFolderIds" >; secretImportDAL: Pick; secretQueueService: Pick; @@ -1959,11 +1965,18 @@ export const secretV2BridgeServiceFactory = ({ secretId }: TGetSecretVersionsDTO) => { const secret = await secretDAL.findById(secretId); + if (!secret) throw new NotFoundError({ message: `Secret with ID '${secretId}' not found` }); const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -1988,7 +2001,7 @@ export const secretV2BridgeServiceFactory = ({ ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: folder.environment.envSlug, - secretPath: "/", + secretPath: folderWithPath.path, secretName: el.key, ...(el.tags?.length && { secretTags: el.tags.map((tag) => tag.slug) @@ -1998,7 +2011,7 @@ export const secretV2BridgeServiceFactory = ({ return reshapeBridgeSecret( folder.projectId, folder.environment.envSlug, - "/", + folderWithPath.path, { ...el, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index cd8971ded..66f1e4eaa 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -100,7 +100,7 @@ type TSecretServiceFactoryDep = { projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds" >; secretV2BridgeService: TSecretV2BridgeServiceFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; @@ -2378,6 +2378,12 @@ export const secretServiceFactory = ({ const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + const { botKey } = await projectBotService.getBotKey(folder.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); @@ -2408,7 +2414,7 @@ export const secretServiceFactory = ({ ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: folder.environment.envSlug, - secretPath: "/", + secretPath: folderWithPath.path, secretName: secretKey, ...(el.tags?.length && { secretTags: el.tags.map((tag) => tag.slug) @@ -2422,7 +2428,7 @@ export const secretServiceFactory = ({ ...el, workspace: folder.projectId, environment: folder.environment.envSlug, - secretPath: "/" + secretPath: folderWithPath.path }, botKey ); From 4d040706a9fd0dd788839a1c238073dc14b48d85 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 02:31:38 +0400 Subject: [PATCH 44/54] Update SecretDetailSidebar.tsx --- .../components/SecretListView/SecretDetailSidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 8b6650247..2fc84ca90 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -661,8 +661,8 @@ export const SecretDetailSidebar = ({
Version History
{secretVersion?.map( - ({ createdAt, secretValue, version, id, secretValueHidden }) => ( -
+ ({ createdAt, secretValue, version, id, secretValueHidden }, index) => ( +
From 2a292455efee57db73f3233adf82980f24ebd793 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 02:58:46 +0400 Subject: [PATCH 45/54] chore: minor ui improvements --- .../components/ActionBar/ActionBar.tsx | 103 +++++++++++------- .../SecretListView/SecretDetailSidebar.tsx | 30 +++-- .../components/SecretListView/SecretItem.tsx | 2 + 3 files changed, 82 insertions(+), 53 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 03ff3dd57..d545ba748 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -21,6 +21,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AxiosError } from "axios"; import FileSaver from "file-saver"; import { twMerge } from "tailwind-merge"; @@ -54,7 +55,7 @@ import { import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; -import { SecretType, WsTag } from "@app/hooks/api/types"; +import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { @@ -152,51 +153,71 @@ export const ActionBar = ({ }; const handleSecretDownload = async () => { - const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ - workspaceId, - expandSecretReferences: true, - includeImports: true, - environment, - secretPath - }); - const secretsPicked = new Set(); - const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; - localSecrets.forEach((el) => { - secretsPicked.add(el.secretKey); - secretsToDownload.push({ - key: el.secretKey, - value: el.secretValue, - comment: el.secretComment + try { + const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ + workspaceId, + expandSecretReferences: true, + includeImports: true, + environment, + secretPath + }); + const secretsPicked = new Set(); + const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; + localSecrets.forEach((el) => { + secretsPicked.add(el.secretKey); + secretsToDownload.push({ + key: el.secretKey, + value: el.secretValue, + comment: el.secretComment + }); }); - }); - for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { - for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { - const secret = localImportedSecrets[i].secrets[j]; - if (!secretsPicked.has(secret.secretKey)) { - secretsToDownload.push({ - key: secret.secretKey, - value: secret.secretValue, - comment: secret.secretComment - }); + for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { + for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { + const secret = localImportedSecrets[i].secrets[j]; + if (!secretsPicked.has(secret.secretKey)) { + secretsToDownload.push({ + key: secret.secretKey, + value: secret.secretValue, + comment: secret.secretComment + }); + } + secretsPicked.add(secret.secretKey); } - secretsPicked.add(secret.secretKey); } + + const file = secretsToDownload + .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) + .reduce( + (prev, { key, comment, value }, index) => + prev + + (comment + ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` + : `${key}=${value}\n`), + "" + ); + + const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, `${environment}.env`); + } catch (err) { + if (err instanceof AxiosError) { + const error = err?.response?.data as TApiErrors; + + if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) { + createNotification({ + title: "You don't have permission to download secrets", + text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.", + type: "error" + }); + return; + } + } + createNotification({ + title: "Failed to download secrets", + text: "Please try again later.", + type: "error" + }); } - - const file = secretsToDownload - .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) - .reduce( - (prev, { key, comment, value }, index) => - prev + - (comment - ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` - : `${key}=${value}\n`), - "" - ); - - const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); - FileSaver.saveAs(blob, `${environment}.env`); }; const handleSecretBulkDelete = async () => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 2fc84ca90..cc60b29d0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -314,19 +314,25 @@ export const SecretDetailSidebar = ({ {...field} autoFocus={false} /> - + +
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 0f4711a8e..02f7a0aa9 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -307,6 +307,7 @@ export const SecretItem = memo(
Date: Wed, 5 Mar 2025 03:05:40 +0400 Subject: [PATCH 46/54] Update SecretEditRow.tsx --- .../components/SecretOverviewTableRow/SecretEditRow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index c0a3ce2ef..4887d4fd7 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -219,6 +219,7 @@ export const SecretEditRow = ({
Date: Wed, 5 Mar 2025 03:53:56 +0400 Subject: [PATCH 47/54] fix: move permissions --- .../secret-v2-bridge-service.ts | 60 ++++++------ backend/src/services/secret/secret-dal.ts | 45 ++++++++- backend/src/services/secret/secret-service.ts | 98 ++++++++++--------- 3 files changed, 128 insertions(+), 75 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index ff896bf22..ca4313114 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2114,16 +2114,26 @@ export const secretV2BridgeServiceFactory = ({ [`${TableName.SecretV2}.id` as "id"]: secretIds } }); + + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + sourceSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + for (const sourceAction of sourceActions) { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } }); if (sourceSecrets.length !== secretIds.length) { @@ -2198,27 +2208,17 @@ export const secretV2BridgeServiceFactory = ({ // permission check whether can create or edit the ones in the destination folder locallyCreatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); - }); - - locallyUpdatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationFolder.path, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } }); const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index cbaf7ddcd..dc41d129f 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findManySecretsWithTags = async ( + filter: { + secretIds: string[]; + type: SecretType; + }, + tx?: Knex + ) => { + try { + const secrets = await (tx || db.replicaNode())(TableName.Secret) + .whereIn(`${TableName.Secret}.id` as "id", filter.secretIds) + .where("type", filter.type) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + const data = sqlNestRelationships({ + data: secrets, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "get many secrets with tags" }); + } + }; + const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { try { // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) @@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - pruneSecretReminders + pruneSecretReminders, + findManySecretsWithTags }; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 66f1e4eaa..787dfde56 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -6,6 +6,7 @@ import { ActionProjectType, ProjectMembershipRole, ProjectUpgradeStatus, + ProjectVersion, SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, @@ -2730,7 +2731,7 @@ export const secretServiceFactory = ({ message: `Project with slug '${projectSlug}' not found` }); } - if (project.version === 3) { + if (project.version === ProjectVersion.V3) { return secretV2BridgeService.moveSecrets({ sourceEnvironment, sourceSecretPath, @@ -2755,34 +2756,6 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - const permissionChecks = [ - { - action: ProjectPermissionSecretActions.Delete, - subject: { - environment: sourceEnvironment, - secretPath: sourceSecretPath - } - }, - { - action: ProjectPermissionSecretActions.Create, - subject: { - environment: destinationEnvironment, - secretPath: destinationSecretPath - } - }, - { - action: ProjectPermissionSecretActions.Edit, - subject: { - environment: destinationEnvironment, - secretPath: destinationSecretPath - } - } - ] as const; - - for (const { action, subject: permissionSubject } of permissionChecks) { - ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, permissionSubject)); - } - const { botKey } = await projectBotService.getBotKey(project.id); if (!botKey) { throw new NotFoundError({ @@ -2810,11 +2783,9 @@ export const secretServiceFactory = ({ }); } - const sourceSecrets = await secretDAL.find({ + const sourceSecrets = await secretDAL.findManySecretsWithTags({ type: SecretType.Shared, - $in: { - id: secretIds - } + secretIds }); if (sourceSecrets.length !== secretIds.length) { @@ -2823,21 +2794,60 @@ export const secretServiceFactory = ({ }); } - const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ - ...secret, - secretKey: decryptSymmetric128BitHexKeyUTF8({ + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + + const decryptedSourceSecrets = sourceSecrets.map((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, key: botKey - }), - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key: botKey - }) - })); + }); + + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath, + secretName: secretKey, + ...(secret.tags.length && { + secretTags: secret.tags.map((t) => t.id) + }) + }) + ); + } + + for (const sourceAction of sourceActions) { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secretKey, + ...(secret.tags.length && { + secretTags: secret.tags.map((t) => t.id) + }) + }) + ); + } + + return { + ...secret, + secretKey, + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: botKey + }) + }; + }); let isSourceUpdated = false; let isDestinationUpdated = false; From b607429b9925f46ab7c80522f169b0cd8aab95ed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 04:07:52 +0400 Subject: [PATCH 48/54] chore: minor ui improvements --- .../SecretReferenceDetails.tsx | 45 ++++++++++++++++--- frontend/src/hooks/api/types.ts | 9 +++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx index 8164353d8..b8724f5dc 100644 --- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx +++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx @@ -1,13 +1,21 @@ -import { useState } from "react"; -import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +/* eslint-disable no-nested-ternary */ +import { useEffect, useState } from "react"; +import { + faChevronRight, + faExclamationTriangle, + faEye, + faEyeSlash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Collapsible from "@radix-ui/react-collapsible"; +import { AxiosError } from "axios"; import { twMerge } from "tailwind-merge"; +import { createNotification } from "@app/components/notifications"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useGetSecretReferenceTree } from "@app/hooks/api"; -import { TSecretReferenceTraceNode } from "@app/hooks/api/types"; +import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types"; import style from "./SecretReferenceDetails.module.css"; @@ -84,7 +92,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - const { data, isPending } = useGetSecretReferenceTree({ + const { data, isPending, isError, error } = useGetSecretReferenceTree({ secretPath, environmentSlug: environment, projectId, @@ -94,6 +102,26 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const tree = data?.tree; const secretValue = data?.value; + useEffect(() => { + if (error instanceof AxiosError) { + const err = error?.response?.data as TApiErrors; + + if (err?.error === ApiErrorTypes.CustomForbiddenError) { + createNotification({ + title: "You don't have permission to view reference tree", + text: "You don't have permission to view one or more of the referenced secrets.", + type: "error" + }); + return; + } + createNotification({ + title: "Error fetching secret reference tree", + text: "Please try again later.", + type: "error" + }); + } + }, [error]); + if (isPending) { return (
@@ -114,11 +142,16 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
- {tree && ( + {isError ? ( +
+ +

Error fetching secret reference tree

+
+ ) : tree ? (
- )} + ) : null}
Click a secret key to view its sub-references. diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index c03358b42..029d8a605 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -46,7 +46,8 @@ export enum ApiErrorTypes { ValidationError = "ValidationFailure", BadRequestError = "BadRequest", UnauthorizedError = "UnauthorizedError", - ForbiddenError = "PermissionDenied" + ForbiddenError = "PermissionDenied", + CustomForbiddenError = "ForbiddenError" } export type TApiErrors = @@ -69,6 +70,12 @@ export type TApiErrors = details: PureAbility["rules"]; statusCode: 403; } + | { + reqId: string; + error: ApiErrorTypes.CustomForbiddenError; + message: string; + statusCode: 403; + } | { reqId: string; statusCode: 400; From 3c2c70912fe82fe2b5ec7f20dfff97e3df41f0b8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 04:22:32 +0400 Subject: [PATCH 49/54] Update secret-service.ts --- backend/src/services/secret/secret-service.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 787dfde56..27981a555 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -2814,11 +2814,7 @@ export const secretServiceFactory = ({ destinationAction, subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, - secretPath: destinationSecretPath, - secretName: secretKey, - ...(secret.tags.length && { - secretTags: secret.tags.map((t) => t.id) - }) + secretPath: destinationSecretPath }) ); } @@ -2828,11 +2824,7 @@ export const secretServiceFactory = ({ sourceAction, subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, - secretPath: sourceSecretPath, - secretName: secretKey, - ...(secret.tags.length && { - secretTags: secret.tags.map((t) => t.id) - }) + secretPath: sourceSecretPath }) ); } From ea9da6d2a84fa62a8f0810cbe8940c8e38770ff8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 19:19:12 +0400 Subject: [PATCH 50/54] fix: view secret value (requested changes) --- .../src/server/routes/v1/dashboard-router.ts | 3 + .../InfisicalSecretInput.tsx | 1 + frontend/src/hooks/api/secrets/queries.tsx | 23 +++++-- frontend/src/hooks/api/secrets/types.ts | 1 + .../components/QuickSearchSecretItem.tsx | 62 ++++++++++++------- 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index abe998c2e..27ba3f8a3 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -5,6 +5,7 @@ import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@ap import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionDynamicSecretActions, + ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { DASHBOARD } from "@app/lib/api-docs"; @@ -688,6 +689,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), tags: SanitizedTagSchema.array().optional() @@ -734,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.getSecretsRawByFolderMappings( { + filterByAction: ProjectPermissionSecretActions.DescribeSecret, projectId, folderMappings, filters: { diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 6fbe96238..dd6d3575e 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -120,6 +120,7 @@ export const InfisicalSecretInput = forwardRef( const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused; const { data: secrets } = useGetProjectSecrets({ + viewSecretValue: false, environment: suggestionSource.environment || "", secretPath: suggestionSource.secretPath || "", workspaceId, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 5549bdcec..3e82c3b90 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -26,8 +26,13 @@ import { export const secretKeys = { // this is also used in secretSnapshot part - getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) => - [{ workspaceId, environment, secretPath }, "secrets"] as const, + getProjectSecret: ({ + workspaceId, + environment, + secretPath, + viewSecretValue + }: TGetProjectSecretsKey) => + [{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretAccessList: ({ workspaceId, @@ -44,13 +49,15 @@ export const fetchProjectSecrets = async ({ environment, secretPath, includeImports, - expandSecretReferences + expandSecretReferences, + viewSecretValue }: TGetProjectSecretsKey) => { const { data } = await apiRequest.get("/api/v3/secrets/raw", { params: { environment, workspaceId, secretPath, + viewSecretValue, expandSecretReferences, include_imports: includeImports } @@ -108,6 +115,7 @@ export const useGetProjectSecrets = ({ workspaceId, environment, secretPath, + viewSecretValue, options }: TGetProjectSecretsDTO & { options?: Omit< @@ -124,8 +132,13 @@ export const useGetProjectSecrets = ({ ...options, // wait for all values to be available enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }), - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), + queryKey: secretKeys.getProjectSecret({ + workspaceId, + environment, + secretPath, + viewSecretValue + }), + queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }), select: useCallback( (data: Awaited>) => mergePersonalSecrets(data.secrets), [] diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 9a419a037..d7b6cbcad 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -113,6 +113,7 @@ export type TGetProjectSecretsKey = { environment: string; secretPath?: string; includeImports?: boolean; + viewSecretValue?: boolean; expandSecretReferences?: boolean; }; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx index e901a4ff7..9943de5b6 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx @@ -110,21 +110,31 @@ export const QuickSearchSecretItem = ({ )} {isSingleEnv ? ( - { - e.stopPropagation(); - const el = envSlugMap.get(groupSecret.env)?.name; - if (el) { - handleCopy(groupSecret.value!, el); - } - }} + - - + { + e.stopPropagation(); + const el = envSlugMap.get(groupSecret.env)?.name; + if (el) { + handleCopy(groupSecret.value!, el); + } + }} + > + + + ) : ( @@ -158,14 +168,24 @@ export const QuickSearchSecretItem = ({ )} - - - + + + + Hover to Reveal... From bdbb6346cbbeaa384578e1cee4157555a1e6d6f5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 21:12:48 +0400 Subject: [PATCH 51/54] fix: permission error instead of not found error on single secret import --- .../secret-import/secret-import-fns.ts | 14 ++++++- .../secret-v2-bridge-service.ts | 39 ++++++++++++------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index edc2efbe8..a849be997 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -3,10 +3,10 @@ import { groupBy, unique } from "@app/lib/fn"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "../secret/secret-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; -import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; type TSecretImportSecrets = { secretPath: string; @@ -33,6 +33,12 @@ type TSecretImportSecretsV2 = { folderId: string | undefined; importFolderId: string; secrets: (TSecretsV2 & { + secretTags: { + slug: string; + name: string; + color?: string | null; + id: string; + }[]; workspace: string; environment: string; _id: string; @@ -175,7 +181,10 @@ export const fnSecretsV2FromImports = async ({ const stack: { secretImports: typeof rootSecretImports; depth: number; - parentImportedSecrets: (TSecretsV2 & { secretValueHidden: boolean })[]; + parentImportedSecrets: (TSecretsV2 & { + secretValueHidden: boolean; + secretTags: { slug: string; name: string; id: string; color: string }[]; + })[]; }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; const processedImports: TSecretImportSecretsV2[] = []; @@ -237,6 +246,7 @@ export const fnSecretsV2FromImports = async ({ secretKey: item.key, secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK, secretValueHidden: !viewSecretValue, + secretTags: item.tags, secretComment: decryptor(item.encryptedComment), environment: importEnv.slug, workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index ca4313114..7f69ae7f1 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1234,7 +1234,7 @@ export const secretV2BridgeServiceFactory = ({ decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - const canDescribe = permission.can( + return permission.can( ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, @@ -1243,17 +1243,6 @@ export const secretV2BridgeServiceFactory = ({ secretTags: expandSecretTags }) ); - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); - - return viewSecretValue ? canDescribe && canReadValue : canDescribe; } }); @@ -1261,6 +1250,30 @@ export const secretV2BridgeServiceFactory = ({ for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { const importedSecret = importedSecrets[i].secrets[j]; if (secretName === importedSecret.key) { + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: importedSecret.environment, + secretPath: importedSecrets[i].secretPath, + secretName: importedSecret.key, + secretTags: (importedSecret.secretTags || []).map((el) => el.slug) + }) + ) && + secretType !== SecretType.Personal + ) { + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret import value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" + }); + } + + secretValueHidden = false; + } + return reshapeBridgeSecret( projectId, importedSecrets[i].environment, @@ -1270,7 +1283,7 @@ export const secretV2BridgeServiceFactory = ({ value: importedSecret.secretValue || "", comment: importedSecret.secretComment || "" }, - importedSecret.secretValueHidden + secretValueHidden ); } } From df21a1fb81de6f207282f81b0d5837f080c3af80 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 21:22:06 +0400 Subject: [PATCH 52/54] fix: types --- backend/src/services/secret-import/secret-import-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index a849be997..e5a450441 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -183,7 +183,7 @@ export const fnSecretsV2FromImports = async ({ depth: number; parentImportedSecrets: (TSecretsV2 & { secretValueHidden: boolean; - secretTags: { slug: string; name: string; id: string; color: string }[]; + secretTags: { slug: string; name: string; id: string; color?: string | null }[]; })[]; }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; From bf6cfbac7aaa63b657db70031dca783a71bf64bb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 5 Mar 2025 14:15:02 -0500 Subject: [PATCH 53/54] Revert "feat(api/secrets): view secret value permission" --- .../e2e-test/routes/v2/service-token.spec.ts | 10 +- backend/e2e-test/vitest-environment-knex.ts | 1 + ...kfill-secret-permissions-with-readvalue.ts | 313 -------- .../v1/secret-approval-request-router.ts | 21 +- backend/src/ee/routes/v1/secret-router.ts | 4 +- .../src/ee/routes/v1/secret-version-router.ts | 7 +- backend/src/ee/routes/v1/snapshot-router.ts | 12 +- .../services/permission/project-permission.ts | 60 +- .../secret-approval-request-service.ts | 20 +- .../secret-replication-service.ts | 1 - .../secret-rotation-service.ts | 8 +- .../secret-snapshot-service.ts | 144 ++-- backend/src/lib/api-docs/constants.ts | 3 - backend/src/lib/errors/index.ts | 1 - backend/src/server/routes/sanitizedSchemas.ts | 9 - .../src/server/routes/v1/dashboard-router.ts | 90 ++- .../server/routes/v2/service-token-router.ts | 2 +- backend/src/server/routes/v3/secret-router.ts | 148 ++-- .../external-migration-fns.ts | 4 +- .../external-migration-queue.ts | 4 +- .../integration-delete-secret.ts | 3 +- .../integration/integration-service.ts | 10 +- .../src/services/project/project-service.ts | 11 +- .../secret-import/secret-import-fns.ts | 29 +- .../secret-import/secret-import-service.ts | 22 +- .../services/secret-sync/secret-sync-queue.ts | 3 +- .../secret-sync/secret-sync-service.ts | 6 +- .../src/services/secret-tag/secret-tag-dal.ts | 1 - .../secret-v2-bridge/secret-v2-bridge-fns.ts | 44 +- .../secret-v2-bridge-service.ts | 672 +++++------------- .../secret-v2-bridge-types.ts | 18 +- .../secret-v2-bridge/secret-version-dal.ts | 59 +- backend/src/services/secret/secret-dal.ts | 45 +- backend/src/services/secret/secret-fns.ts | 42 +- backend/src/services/secret/secret-queue.ts | 3 +- backend/src/services/secret/secret-service.ts | 278 ++------ backend/src/services/secret/secret-types.ts | 7 +- .../src/services/secret/secret-version-dal.ts | 49 +- .../service-token/service-token-service.ts | 8 +- .../service-token/service-token-types.ts | 2 +- .../SecretReferenceDetails.tsx | 45 +- frontend/src/components/v2/Blur/Blur.tsx | 22 - frontend/src/components/v2/Blur/index.tsx | 1 - .../InfisicalSecretInput.tsx | 1 - .../context/ProjectPermissionContext/types.ts | 10 +- frontend/src/hooks/api/dashboard/queries.tsx | 3 - frontend/src/hooks/api/dashboard/types.ts | 1 - .../api/secretApprovalRequest/queries.tsx | 1 - .../src/hooks/api/secretImports/queries.tsx | 2 - .../src/hooks/api/secretSnapshots/queries.tsx | 1 - frontend/src/hooks/api/secrets/queries.tsx | 24 +- frontend/src/hooks/api/secrets/types.ts | 5 - frontend/src/hooks/api/types.ts | 9 +- .../AddServiceTokenModal.tsx | 8 +- .../components/GeneralPermissionPolicies.tsx | 27 +- .../ProjectRoleModifySection.utils.tsx | 36 +- .../CreateSecretForm/CreateSecretForm.tsx | 3 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 43 +- .../SecretNoAccessOverviewTableRow.tsx | 3 +- .../SecretOverviewTableRow.tsx | 9 +- .../SecretRenameRow.tsx | 12 +- .../components/QuickSearchSecretItem.tsx | 62 +- .../SelectionPanel/SelectionPanel.tsx | 5 +- .../MoveSecretsDialog/MoveSecretsDialog.tsx | 5 +- .../SecretDashboardPage.tsx | 13 +- .../components/ActionBar/ActionBar.tsx | 101 ++- .../SecretListView/SecretDetailSidebar.tsx | 330 ++++----- .../components/SecretListView/SecretItem.tsx | 24 +- .../SecretListView/SecretNoAccessListView.tsx | 9 +- .../components/SnapshotView/SecretItem.tsx | 19 +- 70 files changed, 806 insertions(+), 2202 deletions(-) delete mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts delete mode 100644 frontend/src/components/v2/Blur/Blur.tsx delete mode 100644 frontend/src/components/v2/Blur/index.tsx diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index bfe043523..6cc8f6e34 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12 const createServiceToken = async ( scopes: { environment: string; secretPath: string }[], - permissions: ("read" | "write" | "readValue")[] + permissions: ("read" | "write")[] ) => { const projectKeyRes = await testServer.inject({ method: "GET", @@ -139,7 +139,7 @@ describe("Service token secret ops", async () => { beforeAll(async () => { serviceToken = await createServiceToken( [{ secretPath: "/**", environment: seedData1.environment.slug }], - ["read", "write", "readValue"] + ["read", "write"] ); // this is ensure cli service token decryptiong working fine @@ -496,7 +496,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret path access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue", "write"] + ["read", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -518,7 +518,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret environment access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue", "write"] + ["read", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -540,7 +540,7 @@ describe("Service token fail cases", async () => { test("Unauthorized write operation", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue"] + ["read"] ); const writeSecrets = await testServer.inject({ method: "POST", diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 46b322349..9dfb38aeb 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -120,3 +120,4 @@ export default { }; } }; + diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts deleted file mode 100644 index 9e567cefe..000000000 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import { Knex } from "knex"; -import { z } from "zod"; - -import { selectAllTableCols } from "@app/lib/knex"; - -import { TableName } from "../schemas"; - -enum ProjectPermissionSub { - Secrets = "secrets" -} - -enum SecretActions { - Read = "read", - ReadValue = "readValue" -} - -const UnpackedPermissionSchema = z.object({ - subject: z - .union([z.string().min(1), z.string().array()]) - .transform((el) => (typeof el !== "string" ? el[0] : el)) - .optional(), - action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), - conditions: z.unknown().optional(), - inverted: z.boolean().optional() -}); - -const $unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[])); - -const $updatePermissionsUp = (permissions: unknown) => { - const parsedPermissions = $unpackPermissions(permissions); - let shouldUpdate = false; - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { - action.push(SecretActions.ReadValue); - parsedPermissions[i] = { ...parsedPermission, action }; - shouldUpdate = true; - } - } - } - - return { - parsedPermissions, - shouldUpdate - }; -}; - -const $updatePermissionsDown = (permissions: unknown) => { - const parsedPermissions = $unpackPermissions(permissions); - - let shouldUpdate = false; - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - const readValueIndex = action.indexOf(SecretActions.ReadValue); - - if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) { - action.splice(readValueIndex, 1); - parsedPermissions[i] = { ...parsedPermission, action }; - - shouldUpdate = true; - } - } - } - - const repackedPermissions = packRules(parsedPermissions); - - return { - repackedPermissions, - shouldUpdate - }; -}; - -const CHUNK_SIZE = 1000; - -export async function up(knex: Knex): Promise { - const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); - const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( - selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) - ); - const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( - selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) - ); - - const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); - - const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { - const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only. - - if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) { - permissions.push(SecretActions.ReadValue); - acc.push({ - ...serviceToken, - permissions - }); - } - return acc; - }, []); - - if (updatedServiceTokens.length > 0) { - for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { - const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ServiceToken) - .whereIn( - "id", - chunk.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - chunk.map((t) => t.permissions) - ) - }); - } - } - - const updatedRoles = projectRoles.reduce((acc, projectRole) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions); - - if (shouldUpdate) { - acc.push({ - ...projectRole, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce< - typeof projectIdentityAdditionalPrivileges - >((acc, identityAdditionalPrivilege) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...identityAdditionalPrivilege, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce< - typeof projectUserAdditionalPrivileges - >((acc, userAdditionalPrivilege) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...userAdditionalPrivilege, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - if (updatedRoles.length > 0) { - for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { - const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedIdentityAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedUserAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } -} - -export async function down(knex: Knex): Promise { - const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); - const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( - selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) - ); - const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( - selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) - ); - const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); - - const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { - const { permissions } = serviceToken; - - if (permissions.includes(SecretActions.ReadValue)) { - permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1); - acc.push({ - ...serviceToken, - permissions - }); - } - return acc; - }, []); - - if (updatedServiceTokens.length > 0) { - for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { - const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ServiceToken) - .whereIn( - "id", - chunk.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - chunk.map((t) => t.permissions) - ) - }); - } - } - - const updatedRoles = projectRoles.reduce((acc, projectRole) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions); - - if (shouldUpdate) { - acc.push({ - ...projectRole, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, []); - - const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce( - (acc, identityAdditionalPrivilege) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...identityAdditionalPrivilege, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, - [] - ); - - const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce( - (acc, userAdditionalPrivilege) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...userAdditionalPrivilege, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, - [] - ); - - if (updatedRoles.length > 0) { - for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { - const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedIdentityAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedUserAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } -} 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 1ceb6019b..653d04d4f 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -1,11 +1,16 @@ import { z } from "zod"; -import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas"; +import { + SecretApprovalRequestsReviewersSchema, + SecretApprovalRequestsSchema, + SecretTagsSchema, + UsersSchema +} from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; @@ -245,6 +250,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv } }); + const tagSchema = SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .array() + .optional(); + server.route({ method: "GET", url: "/:id", @@ -278,7 +291,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .extend({ op: z.string(), - tags: SanitizedTagSchema.array().optional(), + tags: tagSchema, secretMetadata: ResourceMetadataSchema.nullish(), secret: z .object({ @@ -297,7 +310,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv secretKey: z.string(), secretValue: z.string().optional(), secretComment: z.string().optional(), - tags: SanitizedTagSchema.array().optional(), + tags: tagSchema, secretMetadata: ResourceMetadataSchema.nullish() }) .optional() diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts index a964eb1b8..4c249afe0 100644 --- a/backend/src/ee/routes/v1/secret-router.ts +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -1,6 +1,6 @@ import z from "zod"; -import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; import { RAW_SECRETS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; const AccessListEntrySchema = z .object({ - allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(), + allowedActions: z.nativeEnum(ProjectPermissionActions).array(), id: z.string(), membershipId: z.string(), name: z.string() diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index a09a05c91..11443ebfe 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -22,11 +22,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - secretVersions: secretRawSchema - .extend({ - secretValueHidden: z.boolean() - }) - .array() + secretVersions: secretRawSchema.array() }) } }, @@ -41,7 +37,6 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => offset: req.query.offset, secretId: req.params.secretId }); - return { secretVersions }; } }); diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 283b9b31e..fc2d25712 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,10 +1,10 @@ import { z } from "zod"; -import { SecretSnapshotsSchema } from "@app/db/schemas"; +import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSnapshotRouter = async (server: FastifyZodProvider) => { @@ -31,9 +31,12 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { secretVersions: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true }) .extend({ - secretValueHidden: z.boolean(), secretId: z.string(), - tags: SanitizedTagSchema.array() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }).array() }) .array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(), @@ -52,7 +55,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); - return { secretSnapshot }; } }); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 3f69c481a..4389c3866 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -17,14 +17,6 @@ export enum ProjectPermissionActions { Delete = "delete" } -export enum ProjectPermissionSecretActions { - DescribeSecret = "read", - ReadValue = "readValue", - Create = "create", - Edit = "edit", - Delete = "delete" -} - export enum ProjectPermissionCmekActions { Read = "read", Create = "create", @@ -123,7 +115,7 @@ export type IdentityManagementSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionSecretActions, + ProjectPermissionActions, ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) ] | [ @@ -437,7 +429,6 @@ const GeneralPermissionSchema = [ }) ]; -// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only. export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), @@ -469,7 +460,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV2Schema.describe( @@ -526,6 +517,7 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ + ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, @@ -558,21 +550,10 @@ const buildAdminPermissionRules = () => { ProjectPermissionActions.Create, ProjectPermissionActions.Delete ], - el + el as ProjectPermissionSub ); }); - can( - [ - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Edit, - ProjectPermissionSecretActions.Delete - ], - ProjectPermissionSub.Secrets - ); - can( [ ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -632,11 +613,10 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Edit, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Delete + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete ], ProjectPermissionSub.Secrets ); @@ -808,8 +788,7 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); @@ -852,8 +831,6 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); - const canReadValue = permission.includes("readValue"); - const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( @@ -883,14 +860,6 @@ export const buildServiceTokenProjectPermission = ( environment }); } - - if (subject === ProjectPermissionSub.Secrets && canReadValue) { - // @ts-expect-error type - can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { - secretPath: { $glob: secretPath }, - environment - }); - } } ); }); @@ -947,17 +916,7 @@ export const backfillPermissionV1SchemaToV2Schema = ( subject: ProjectPermissionSub.SecretImports as const })); - const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({ - subject: ProjectPermissionSub.Secrets as const, - ...el, - action: - el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue) - ? el.action.concat(ProjectPermissionSecretActions.ReadValue) - : el.action - })); - const secretFolderPolicies = secretSubjects - .map(({ subject, ...el }) => ({ ...el, // read permission is not needed anymore @@ -999,7 +958,6 @@ export const backfillPermissionV1SchemaToV2Schema = ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts secretImportPolicies, - secretPolicies, dynamicSecretPolicies, hasReadOnlyFolder.length ? [] : secretFolderPolicies ); 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 5430558a9..8569ef2a9 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 @@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; @@ -88,12 +88,7 @@ type TSecretApprovalRequestServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: Pick< TSecretTagDALFactory, - | "findManyTagsById" - | "saveTagsToSecret" - | "deleteTagsManySecret" - | "saveTagsToSecretV2" - | "deleteTagsToSecretV2" - | "find" + "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2" >; secretBlindIndexDAL: Pick; snapshotService: Pick; @@ -111,7 +106,7 @@ type TSecretApprovalRequestServiceFactoryDep = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -919,7 +914,7 @@ export const secretApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -1006,7 +1001,6 @@ export const secretApprovalRequestServiceFactory = ({ : keyName2BlindIndex[secretName]; // add tags if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; - return { ...latestSecretVersions[secretId], ...el, @@ -1369,9 +1363,9 @@ export const secretApprovalRequestServiceFactory = ({ const tagsGroupById = groupBy(tags, (i) => i.id); commits.forEach((commit) => { - let action = ProjectPermissionSecretActions.Create; - if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit; - if (commit.op === SecretOperations.Delete) action = ProjectPermissionSecretActions.Delete; + let action = ProjectPermissionActions.Create; + if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; + if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; ForbiddenError.from(permission).throwUnlessCan( action, diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 5fae2675d..3c25db98c 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -265,7 +265,6 @@ export const secretReplicationServiceFactory = ({ folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - viewSecretValue: true, hasSecretAccess: () => true }); // secrets that gets replicated across imports diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 8d458111a..02da4b7ea 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -15,11 +15,7 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "../permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; @@ -110,7 +106,7 @@ export const secretRotationServiceFactory = ({ }); } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index e8eab3017..1c34f6b3d 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -22,11 +22,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "../permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TGetSnapshotDataDTO, TProjectSnapshotCountDTO, @@ -38,7 +34,6 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal"; import { getFullFolderPath } from "./snapshot-service-fns"; -import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; type TSecretSnapshotServiceFactoryDep = { snapshotDAL: TSnapshotDALFactory; @@ -102,7 +97,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -139,7 +134,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -166,7 +161,6 @@ export const secretSnapshotServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - const shouldUseBridge = snapshot.projectVersion === 3; let snapshotDetails; if (shouldUseBridge) { @@ -175,110 +169,68 @@ export const secretSnapshotServiceFactory = ({ projectId: snapshot.projectId }); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); - - const fullFolderPath = await getFullFolderPath({ - folderDAL, - folderId: encryptedSnapshotDetails.folderId, - envId: encryptedSnapshotDetails.environment.id - }); - snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: el.key, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }) - ); - - let secretValue = ""; - if (canReadValue) { - secretValue = el.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : ""; - } else { - secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; - } - - return { - ...el, - secretKey: el.key, - secretValueHidden: !canReadValue, - secretValue, - secretComment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : "" - }; - }) + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ + ...el, + secretKey: el.key, + secretValue: el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : "", + secretComment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + })) }; } else { const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); - - const fullFolderPath = await getFullFolderPath({ - folderDAL, - folderId: encryptedSnapshotDetails.folderId, - envId: encryptedSnapshotDetails.environment.id - }); - const { botKey } = await projectBotService.getBotKey(snapshot.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` }); snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - const secretKey = decryptSymmetric128BitHexKeyUTF8({ + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ + ...el, + secretKey: decryptSymmetric128BitHexKeyUTF8({ ciphertext: el.secretKeyCiphertext, iv: el.secretKeyIV, tag: el.secretKeyTag, key: botKey - }); - - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: secretKey, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }) - ); - - let secretValue = ""; - - if (canReadValue) { - secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretValueCiphertext, - iv: el.secretValueIV, - tag: el.secretValueTag, - key: botKey - }); - } else { - secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; - } - - return { - ...el, - secretKey, - secretValueHidden: !canReadValue, - secretValue, - secretComment: - el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext - ? decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretCommentCiphertext, - iv: el.secretCommentIV, - tag: el.secretCommentTag, - key: botKey - }) - : "" - }; - }) + }), + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretValueCiphertext, + iv: el.secretValueIV, + tag: el.secretValueTag, + key: botKey + }), + secretComment: + el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretCommentCiphertext, + iv: el.secretCommentIV, + tag: el.secretCommentTag, + key: botKey + }) + : "" + })) }; } + const fullFolderPath = await getFullFolderPath({ + folderDAL, + folderId: snapshotDetails.folderId, + envId: snapshotDetails.environment.id + }); + + // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: snapshotDetails.environment.slug, + secretPath: fullFolderPath + }) + ); + return snapshotDetails; }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5a412cb45..1b458175d 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -666,7 +666,6 @@ export const SECRETS = { secretPath: "The path of the secret to attach tags to.", type: "The type of the secret to attach tags to. (shared/personal)", environment: "The slug of the environment where the secret is located", - viewSecretValue: "Whether or not to retrieve the secret value.", projectSlug: "The slug of the project where the secret is located.", tagSlugs: "An array of existing tag slugs to attach to the secret." }, @@ -690,7 +689,6 @@ export const RAW_SECRETS = { "The slug of the project to list secrets from. This parameter is only applicable by machine identities.", environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", - viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not.", tagSlugs: "The comma separated tag slugs to filter secrets.", metadataFilter: @@ -719,7 +717,6 @@ export const RAW_SECRETS = { secretPath: "The path of the secret to get.", version: "The version of the secret to get.", type: "The type of the secret to get.", - viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not." }, UPDATE: { diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index 7dbffed20..cc1c1d66b 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -1,5 +1,4 @@ /* eslint-disable max-classes-per-file */ - export class DatabaseError extends Error { name: string; diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 024e44224..4d645ac4b 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -7,7 +7,6 @@ import { ProjectRolesSchema, ProjectsSchema, SecretApprovalPoliciesSchema, - SecretTagsSchema, UsersSchema } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -233,11 +232,3 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ kmsCertificateKeyId: true, auditLogsRetentionDays: true }); - -export const SanitizedTagSchema = SecretTagsSchema.pick({ - id: true, - slug: true, - color: true -}).extend({ - name: z.string() -}); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 27ba3f8a3..d96c45f04 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,11 +1,10 @@ import { ForbiddenError, subject } from "@casl/ability"; import { z } from "zod"; -import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; +import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionDynamicSecretActions, - ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { DASHBOARD } from "@app/lib/api-docs"; @@ -16,7 +15,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -117,10 +116,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), secrets: secretRawSchema .extend({ - secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SanitizedTagSchema.array().optional() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() + .optional() }) .array() .optional(), @@ -289,7 +294,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { secrets = await server.services.secret.getSecretsRawMultiEnv({ - viewSecretValue: true, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, @@ -389,7 +393,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), - viewSecretValue: booleanSchema.default(true), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), @@ -407,10 +410,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(), secrets: secretRawSchema .extend({ - secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SanitizedTagSchema.array().optional() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() + .optional() }) .array() .optional(), @@ -592,25 +601,23 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { - secrets = ( - await server.services.secret.getSecretsRaw({ - actorId: req.permission.id, - actor: req.permission.type, - viewSecretValue: req.query.viewSecretValue, - throwOnMissingReadValuePermission: false, - actorOrgId: req.permission.orgId, - environment, - actorAuthMethod: req.permission.authMethod, - projectId, - path: secretPath, - orderBy, - orderDirection, - search, - limit: remainingLimit, - offset: adjustedOffset, - tagSlugs: tags - }) - ).secrets; + const secretsRaw = await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + environment, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + orderBy, + orderDirection, + search, + limit: remainingLimit, + offset: adjustedOffset, + tagSlugs: tags + }); + + secrets = secretsRaw.secrets; await server.services.auditLog.createAuditLog({ projectId, @@ -689,10 +696,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), secrets: secretRawSchema .extend({ - secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SanitizedTagSchema.array().optional() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() + .optional() }) .array() .optional() @@ -736,7 +749,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.getSecretsRawByFolderMappings( { - filterByAction: ProjectPermissionSecretActions.DescribeSecret, projectId, folderMappings, filters: { @@ -850,17 +862,22 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - keys: z.string().trim().transform(decodeURIComponent), - viewSecretValue: booleanSchema.default(false) + keys: z.string().trim().transform(decodeURIComponent) }), response: { 200: z.object({ secrets: secretRawSchema .extend({ - secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SanitizedTagSchema.array().optional() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() + .optional() }) .array() .optional() @@ -869,7 +886,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { secretPath, projectId, environment, viewSecretValue } = req.query; + const { secretPath, projectId, environment } = req.query; const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? []; if (!keys.length) throw new BadRequestError({ message: "One or more keys required" }); @@ -878,7 +895,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, - viewSecretValue, environment, actorAuthMethod: req.permission.authMethod, projectId, diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index 1a84c9123..fb10f17db 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => iv: z.string().trim(), tag: z.string().trim(), expiresIn: z.number().nullable(), - permissions: z.enum(["read", "write", "readValue"]).array() + permissions: z.enum(["read", "write"]).array() }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 3bc312c98..a5dc39485 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1,7 +1,13 @@ import picomatch from "picomatch"; import { z } from "zod"; -import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; +import { + SecretApprovalRequestsSchema, + SecretsSchema, + SecretTagsSchema, + SecretType, + ServiceTokenScopes +} from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -17,7 +23,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; -import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; +import { secretRawSchema } from "../sanitizedSchemas"; const SecretReferenceNode = z.object({ key: z.string(), @@ -25,14 +31,6 @@ const SecretReferenceNode = z.object({ environment: z.string(), secretPath: z.string() }); - -const convertStringBoolean = (defaultValue: boolean = false) => { - return z - .enum(["true", "false"]) - .default(defaultValue ? "true" : "false") - .transform((value) => value === "true"); -}; - type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ @@ -77,9 +75,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ - tags: SanitizedTagSchema.array() - }) + secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( + z.object({ + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() + }) + ) }) } }, @@ -133,7 +139,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ - tags: SanitizedTagSchema.array() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() }) }) } @@ -235,10 +247,21 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), - viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), - expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), - recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), - include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), + expandSecretReferences: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.LIST.expand), + recursive: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.LIST.recursive), + include_imports: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.LIST.includeImports), tagSlugs: z .string() .describe(RAW_SECRETS.LIST.tagSlugs) @@ -251,9 +274,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), - secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SanitizedTagSchema.array().optional() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() + .optional() }) .array(), imports: z @@ -264,7 +293,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .omit({ createdAt: true, updatedAt: true }) .extend({ - secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional() }) .array() @@ -314,7 +342,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, actorAuthMethod: req.permission.authMethod, projectId: workspaceId, - viewSecretValue: req.query.viewSecretValue, path: secretPath, metadataFilter: req.query.metadataFilter, includeImports: req.query.include_imports, @@ -349,7 +376,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); } - return { secrets, imports }; } }); @@ -377,15 +403,28 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), - viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), - expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), - include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) + expandSecretReferences: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.GET.expand), + include_imports: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.GET.includeImports) }), response: { 200: z.object({ secret: secretRawSchema.extend({ - secretValueHidden: z.boolean(), - tags: SanitizedTagSchema.array().optional(), + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() + .optional(), secretMetadata: ResourceMetadataSchema.optional() }) }) @@ -417,7 +456,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, environment, projectId: workspaceId, - viewSecretValue: req.query.viewSecretValue, projectSlug: workspaceSlug, path: secretPath, secretName: req.params.secretName, @@ -624,9 +662,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema.extend({ - secretValueHidden: z.boolean() - }) + secret: secretRawSchema }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -722,9 +758,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema.extend({ - secretValueHidden: z.boolean() - }) + secret: secretRawSchema }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -746,7 +780,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } - const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ @@ -809,7 +842,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspace: z.string(), environment: z.string(), secretPath: z.string().optional(), - tags: SanitizedTagSchema.array() + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .array() }) .array(), imports: z @@ -905,7 +944,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash), type: z.nativeEnum(SecretType).default(SecretType.Shared), version: z.coerce.number().optional(), - include_imports: convertStringBoolean() + include_imports: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") }), response: { 200: z.object({ @@ -1176,7 +1218,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( z.object({ - secretValueHidden: z.boolean(), _id: z.string(), workspace: z.string(), environment: z.string() @@ -1346,12 +1387,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ - _id: z.string(), - secretValueHidden: z.boolean(), - workspace: z.string(), - environment: z.string() - }) + secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( + z.object({ + _id: z.string(), + workspace: z.string(), + environment: z.string() + }) + ) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1663,7 +1705,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1778,11 +1820,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }) - .extend({ - secretValueHidden: z.boolean() - }) - .array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2044,7 +2082,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() + secrets: secretRawSchema.array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2166,11 +2204,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema - .extend({ - secretValueHidden: z.boolean() - }) - .array() + secrets: secretRawSchema.array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index fe0d49ca8..744678792 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; resourceMetadataDAL: Pick; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index 8aa46b94c..e3d77e832 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; folderDAL: Pick; diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index 4b07245ac..fdefd0e62 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -68,8 +68,7 @@ const getIntegrationSecretsV2 = async ( secretDAL: secretV2BridgeDAL, secretImportDAL, secretImports, - hasSecretAccess: () => true, - viewSecretValue: true + hasSecretAccess: () => true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index f72357964..35994433a 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -2,11 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -96,7 +92,7 @@ export const integrationServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath @@ -179,7 +175,7 @@ export const integrationServiceFactory = ({ if (environment || secretPath) { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index d3afaec29..e1653d371 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -11,11 +11,7 @@ import { import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; @@ -751,10 +747,7 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index e5a450441..d21ad3b9c 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -3,7 +3,6 @@ import { groupBy, unique } from "@app/lib/fn"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "../secret/secret-dal"; -import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; @@ -33,12 +32,6 @@ type TSecretImportSecretsV2 = { folderId: string | undefined; importFolderId: string; secrets: (TSecretsV2 & { - secretTags: { - slug: string; - name: string; - color?: string | null; - id: string; - }[]; workspace: string; environment: string; _id: string; @@ -46,7 +39,6 @@ type TSecretImportSecretsV2 = { // akhilmhdh: yes i know you can put ?. // But for somereason ts consider ? and undefined explicit as different just ts things secretValue: string; - secretValueHidden: boolean; secretComment: string; secretMetadata?: ResourceMetadataDTO; })[]; @@ -158,14 +150,12 @@ export const fnSecretsV2FromImports = async ({ secretImportDAL, decryptor, expandSecretReferences, - hasSecretAccess, - viewSecretValue + hasSecretAccess }: { secretImports: (Omit & { importEnv: { id: string; slug: string; name: string }; })[]; folderDAL: Pick; - viewSecretValue: boolean; secretDAL: Pick; secretImportDAL: Pick; decryptor: (value?: Buffer | null) => string; @@ -178,14 +168,9 @@ export const fnSecretsV2FromImports = async ({ hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { const cyclicDetector = new Set(); - const stack: { - secretImports: typeof rootSecretImports; - depth: number; - parentImportedSecrets: (TSecretsV2 & { - secretValueHidden: boolean; - secretTags: { slug: string; name: string; id: string; color?: string | null }[]; - })[]; - }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; + const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ + { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } + ]; const processedImports: TSecretImportSecretsV2[] = []; @@ -244,9 +229,7 @@ export const fnSecretsV2FromImports = async ({ .map((item) => ({ ...item, secretKey: item.key, - secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK, - secretValueHidden: !viewSecretValue, - secretTags: item.tags, + secretValue: decryptor(item.encryptedValue), secretComment: decryptor(item.encryptedComment), environment: importEnv.slug, workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. @@ -284,8 +267,6 @@ export const fnSecretsV2FromImports = async ({ processedImport.secrets = unique(processedImport.secrets, (i) => i.key); return Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { - if (decryptedSecret.secretValueHidden) return; - const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, secretPath: processedImport.secretPath, diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 6c412edc0..e8fde04d1 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -5,11 +5,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -94,7 +90,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -406,7 +402,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -600,7 +596,7 @@ export const secretImportServiceFactory = ({ const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -646,13 +642,12 @@ export const secretImportServiceFactory = ({ const importedSecrets = await fnSecretsV2FromImports({ secretImports, folderDAL, - viewSecretValue: true, secretDAL: secretV2BridgeDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -672,7 +667,7 @@ export const secretImportServiceFactory = ({ const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -688,10 +683,7 @@ export const secretImportServiceFactory = ({ return importedSecrets.map((el) => ({ ...el, secrets: el.secrets.map((encryptedSecret) => - decryptSecretRaw( - { ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false }, - botKey - ) + decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey) ) })); }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 8afcf6416..cdc9540da 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -249,8 +249,7 @@ export const secretSyncQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true, - viewSecretValue: true + hasSecretAccess: () => true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 7b92a463c..8211180e8 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { - ProjectPermissionSecretActions, + ProjectPermissionActions, ProjectPermissionSecretSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({ ); ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath @@ -270,7 +270,7 @@ export const secretSyncServiceFactory = ({ throw new BadRequestError({ message: "Must specify both source environment and secret path" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: updatedEnvironment, secretPath: updatedSecretPath diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts index 3b9151557..1df64afa2 100644 --- a/backend/src/services/secret-tag/secret-tag-dal.ts +++ b/backend/src/services/secret-tag/secret-tag-dal.ts @@ -47,7 +47,6 @@ export const secretTagDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find all by ids" }); } }; - return { ...secretTagOrm, saveTagsToSecret: secretJnTagOrm.insertMany, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 6eb95e9b9..cc40b0f26 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -7,7 +7,6 @@ import { logger } from "@app/lib/logger"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; -import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; @@ -103,7 +102,6 @@ export const fnSecretBulkInsert = async ({ [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id })) ); - const secretVersions = await secretVersionDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, @@ -139,7 +137,6 @@ export const fnSecretBulkInsert = async ({ if (newSecretTags.length) { const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); - const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({ [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretTag}Id` as const]: secret_tagsId @@ -148,16 +145,7 @@ export const fnSecretBulkInsert = async ({ await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); } - const secretsWithTags = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) - } - }, - { tx } - ); - - return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); + return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkUpdate = async ({ @@ -295,15 +283,7 @@ export const fnSecretBulkUpdate = async ({ tx ); - const secretsWithTags = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) - } - }, - { tx } - ); - return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); + return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkDelete = async ({ @@ -536,7 +516,7 @@ export const expandSecretReferencesFactory = ({ const referredValue = await fetchSecret(environment, secretPath, secretKey); if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.` + message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` }); const cacheKey = getCacheUniqueKey(environment, secretPath); @@ -555,7 +535,7 @@ export const expandSecretReferencesFactory = ({ const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.` + message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` }); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); @@ -643,13 +623,13 @@ export const reshapeBridgeSecret = ( name: string; }[]; secretMetadata?: ResourceMetadataDTO; - }, - secretValueHidden: boolean + } ) => ({ secretKey: secret.key, secretPath, workspace: workspaceId, environment, + secretValue: secret.value || "", secretComment: secret.comment || "", version: secret.version, type: secret.type, @@ -663,15 +643,5 @@ export const reshapeBridgeSecret = ( metadata: secret.metadata, secretMetadata: secret.secretMetadata, createdAt: secret.createdAt, - updatedAt: secret.updatedAt, - - ...(secretValueHidden - ? { - secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, - secretValueHidden: true - } - : { - secretValue: secret.value || "", - secretValueHidden: false - }) + updatedAt: secret.updatedAt }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 7f69ae7f1..0ffb0ea4c 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -11,11 +11,7 @@ import { TSecretsV2 } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -77,13 +73,7 @@ type TSecretV2BridgeServiceFactoryDep = { projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - | "findBySecretPath" - | "updateById" - | "findById" - | "findByManySecretPath" - | "find" - | "findBySecretPathMultiEnv" - | "findSecretPathByFolderIds" + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" >; secretImportDAL: Pick; secretQueueService: Pick; @@ -262,7 +252,7 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, type, ...inputSecretData } = inputSecret; ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -281,8 +271,8 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const secret = await secretDAL.transaction(async (tx) => { - const [createdSecret] = await fnSecretBulkInsert({ + const secret = await secretDAL.transaction((tx) => + fnSecretBulkInsert({ folderId, orgId: actorOrgId, inputSecrets: [ @@ -312,10 +302,8 @@ export const secretV2BridgeServiceFactory = ({ secretTagDAL, secretVersionTagDAL, tx - }); - - return createdSecret; - }); + }) + ); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); @@ -329,17 +317,11 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret( - projectId, - environment, - secretPath, - { - ...secret, - value: inputSecret.secretValue, - comment: inputSecret.secretComment || "" - }, - false - ); + return reshapeBridgeSecret(projectId, environment, secretPath, { + ...secret[0], + value: inputSecret.secretValue, + comment: inputSecret.secretComment || "" + }); }; const updateSecret = async ({ @@ -408,7 +390,7 @@ export const secretV2BridgeServiceFactory = ({ } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -419,22 +401,18 @@ export const secretV2BridgeServiceFactory = ({ // validate tags // fetch all tags and if not same count throw error meaning one was invalid tags - const newTags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; - if ((inputSecret.tagIds || []).length !== newTags.length) - throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` }); - - const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags; + const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; + if ((inputSecret.tagIds || []).length !== tags.length) + throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); // now check with new ids ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.secretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) - }) + secretTags: tags?.map((el) => el.slug) }) ); @@ -446,14 +424,12 @@ export const secretV2BridgeServiceFactory = ({ }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.newSecretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) - }) + secretTags: tags?.map((el) => el.slug) }) ); } @@ -531,29 +507,11 @@ export const secretV2BridgeServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: inputSecret.secretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) - }) - }) - ); - - return reshapeBridgeSecret( - projectId, - environment, - secretPath, - { - ...updatedSecret[0], - value: inputSecret.secretValue || "", - comment: inputSecret.secretComment || "" - }, - secretValueHidden - ); + return reshapeBridgeSecret(projectId, environment, secretPath, { + ...updatedSecret[0], + value: inputSecret.secretValue || "", + comment: inputSecret.secretComment || "" + }); }; const deleteSecret = async ({ @@ -599,7 +557,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Delete, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -641,32 +599,15 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: secretToDelete.key, - secretTags: secretToDelete.tags?.map((el) => el.slug) - }) - ); - - return reshapeBridgeSecret( - projectId, - environment, - secretPath, - { - ...deletedSecret[0], - value: deletedSecret[0].encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() - : "", - comment: deletedSecret[0].encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() - : "" - }, - secretValueHidden - ); + return reshapeBridgeSecret(projectId, environment, secretPath, { + ...deletedSecret[0], + value: deletedSecret[0].encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() + : "", + comment: deletedSecret[0].encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() + : "" + }); }; // get unique secrets count for multiple envs @@ -694,10 +635,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -744,10 +682,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -758,13 +693,7 @@ export const secretV2BridgeServiceFactory = ({ }; const getSecretsByFolderMappings = async ( - { - projectId, - userId, - filters, - folderMappings, - filterByAction = ProjectPermissionSecretActions.ReadValue - }: TGetSecretsRawByFolderMappingsDTO, + { projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO, projectPermission: Awaited>["permission"] ) => { const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); @@ -784,7 +713,7 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => projectPermission.can( - filterByAction, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, secretPath: groupedFolderMappings[el.folderId][0].path, @@ -793,19 +722,8 @@ export const secretV2BridgeServiceFactory = ({ }) ) ) - .map((secret) => { - // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. - const secretValueHidden = !projectPermission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: groupedFolderMappings[secret.folderId][0].environment, - secretPath: groupedFolderMappings[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); - - return reshapeBridgeSecret( + .map((secret) => + reshapeBridgeSecret( projectId, groupedFolderMappings[secret.folderId][0].environment, groupedFolderMappings[secret.folderId][0].path, @@ -817,10 +735,9 @@ export const secretV2BridgeServiceFactory = ({ comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" - }, - secretValueHidden - ); - }); + } + ) + ); return decryptedSecrets; }; @@ -849,10 +766,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -872,8 +786,7 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderMappings, filters: params, - userId: actorId, - filterByAction: ProjectPermissionSecretActions.DescribeSecret + userId: actorId }, permission ); @@ -888,12 +801,10 @@ export const secretV2BridgeServiceFactory = ({ projectId, actor, actorOrgId, - viewSecretValue, actorAuthMethod, includeImports, recursive, expandSecretReferences: shouldExpandSecretReferences, - throwOnMissingReadValuePermission = true, ...params }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission({ @@ -905,14 +816,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretTags: params.tagSlugs - }) - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); let paths: { folderId: string; path: string }[] = []; @@ -950,87 +854,28 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => { - const canDescribeSecret = permission.can( - ProjectPermissionSecretActions.DescribeSecret, + .filter((el) => + permission.can( + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, secretTags: el.tags.map((i) => i.slug) }) - ); - - if (!canDescribeSecret) { - return false; - } - - if (viewSecretValue) { - // Recursive secret, should be filtered out - if (groupedPaths[el.folderId][0].path !== path) { - const canReadRecursiveSecretValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); - - if (!canReadRecursiveSecretValue) { - return false; - } - } - - if (throwOnMissingReadValuePermission) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); - } - // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error. - // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value. - } - - return canDescribeSecret; - }) - .map((secret) => { - const isPersonalSecret = secret.userId === actorId && secret.type === SecretType.Personal; - - const secretValueHidden = - !viewSecretValue || - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); - - return reshapeBridgeSecret( - projectId, - environment, - groupedPaths[secret.folderId][0].path, - { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }, - secretValueHidden && !isPersonalSecret - ); - }); + ) + ) + .map((secret) => + reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }) + ); const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, @@ -1039,7 +884,7 @@ export const secretV2BridgeServiceFactory = ({ decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -1078,36 +923,22 @@ export const secretV2BridgeServiceFactory = ({ const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ - viewSecretValue, secretImports: allowedImports, secretDAL, folderDAL, secretImportDAL, expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - const canDescribe = permission.can( - ProjectPermissionSecretActions.DescribeSecret, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + permission.can( + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ); - - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); - - return viewSecretValue ? canDescribe && canReadValue : canDescribe; - } + ) }); return { @@ -1127,7 +958,6 @@ export const secretV2BridgeServiceFactory = ({ type, secretName, version, - viewSecretValue, includeImports, expandSecretReferences: shouldExpandSecretReferences }: TGetASecretDTO) => { @@ -1189,7 +1019,7 @@ export const secretV2BridgeServiceFactory = ({ )); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, @@ -1198,93 +1028,56 @@ export const secretV2BridgeServiceFactory = ({ }) ); - // this will throw if the user doesn't have read value permission no matter what - // because if its an expansion, it will fully depend on the value. const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return permission.can( - ProjectPermissionSecretActions.ReadValue, + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + permission.can( + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ); - } + ) }); // now if secret is not found // then search for imported secrets // here we consider the import order also thus starting from bottom - - // currently filters out the secrets that the user doesn't have access to read value on if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ secretImports, - viewSecretValue, secretDAL, folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return permission.can( - ProjectPermissionSecretActions.DescribeSecret, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + permission.can( + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ); - } + ) }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { const importedSecret = importedSecrets[i].secrets[j]; if (secretName === importedSecret.key) { - let secretValueHidden = true; - - if (viewSecretValue) { - if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: importedSecret.environment, - secretPath: importedSecrets[i].secretPath, - secretName: importedSecret.key, - secretTags: (importedSecret.secretTags || []).map((el) => el.slug) - }) - ) && - secretType !== SecretType.Personal - ) { - throw new ForbiddenRequestError({ - message: `You do not have permission to view secret import value on secret with name '${secretName}'`, - name: "ForbiddenReadSecretError" - }); - } - - secretValueHidden = false; - } - - return reshapeBridgeSecret( - projectId, - importedSecrets[i].environment, - importedSecrets[i].secretPath, - { - ...importedSecret, - value: importedSecret.secretValue || "", - comment: importedSecret.secretComment || "" - }, - secretValueHidden - ); + return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, { + ...importedSecret, + value: importedSecret.secretValue || "", + comment: importedSecret.secretComment || "" + }); } } } @@ -1306,43 +1099,13 @@ export const secretV2BridgeServiceFactory = ({ secretValue = expandedSecretValue || ""; } - let secretValueHidden = true; - - if (viewSecretValue) { - if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ) && - secretType !== SecretType.Personal - ) { - throw new ForbiddenRequestError({ - message: `You do not have permission to view secret value on secret with name '${secretName}'`, - name: "ForbiddenReadSecretError" - }); - } - - secretValueHidden = false; - } - - return reshapeBridgeSecret( - projectId, - environment, - path, - { - ...secret, - value: secretValue, - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }, - secretValueHidden - ); + return reshapeBridgeSecret(projectId, environment, path, { + ...secret, + value: secretValue, + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }); }; const createManySecret = async ({ @@ -1410,7 +1173,7 @@ export const secretV2BridgeServiceFactory = ({ inputSecrets.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1481,29 +1244,13 @@ export const secretV2BridgeServiceFactory = ({ environmentSlug: folder.environment.slug }); - return newSecrets.map((el) => { - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.key, - secretTags: el.tags?.map((i) => i.slug) - }) - ); - - return reshapeBridgeSecret( - projectId, - environment, - secretPath, - { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }, - secretValueHidden - ); - }); + return newSecrets.map((el) => + reshapeBridgeSecret(projectId, environment, secretPath, { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }) + ); }; const updateManySecret = async ({ @@ -1546,17 +1293,7 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const updatedSecrets: Array< - TSecretsV2 & { - secretPath: string; - tags: { - id: string; - slug: string; - color?: string | null; - name: string; - }[]; - } - > = []; + const updatedSecrets: Array = []; await secretDAL.transaction(async (tx) => { for await (const folder of folders) { if (!folder) throw new NotFoundError({ message: "Folder not found" }); @@ -1607,7 +1344,7 @@ export const secretV2BridgeServiceFactory = ({ secretsToUpdateInDB.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1627,7 +1364,7 @@ export const secretV2BridgeServiceFactory = ({ if (updateMode === SecretUpdateMode.Upsert) { secretsToCreate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1641,7 +1378,7 @@ export const secretV2BridgeServiceFactory = ({ // check again to avoid non authorized tags are removed secretsToUpdate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1693,7 +1430,7 @@ export const secretV2BridgeServiceFactory = ({ secretsWithNewName.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1755,7 +1492,6 @@ export const secretV2BridgeServiceFactory = ({ secretVersionTagDAL, resourceMetadataDAL }); - updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); if (updateMode === SecretUpdateMode.Upsert) { const bulkInsertedSecrets = await fnSecretBulkInsert({ @@ -1788,7 +1524,6 @@ export const secretV2BridgeServiceFactory = ({ secretVersionTagDAL, tx }); - updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); } } @@ -1810,33 +1545,13 @@ export const secretV2BridgeServiceFactory = ({ ) ); - return updatedSecrets.map((el) => { - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: el.secretPath, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); - - return { - ...reshapeBridgeSecret( - projectId, - environment, - el.secretPath, - { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : "" - }, - secretValueHidden - ) - }; - }); + return updatedSecrets.map((el) => + reshapeBridgeSecret(projectId, environment, el.secretPath, { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }) + ); }; const deleteManySecret = async ({ @@ -1898,7 +1613,7 @@ export const secretV2BridgeServiceFactory = ({ }); secretsToDelete.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Delete, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1937,35 +1652,13 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - return secretsDeleted.map((el) => { - const secretToDeleteMatch = secretsToDelete.find( - (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type - ); - - const secretValueHidden = - !secretToDeleteMatch || - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.key, - secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) - }) - ); - - return reshapeBridgeSecret( - projectId, - environment, - secretPath, - { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }, - secretValueHidden - ); - }); + return secretsDeleted.map((el) => + reshapeBridgeSecret(projectId, environment, secretPath, { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }) + ); }; const getSecretVersions = async ({ @@ -1978,18 +1671,11 @@ export const secretV2BridgeServiceFactory = ({ secretId }: TGetSecretVersionsDTO) => { const secret = await secretDAL.findById(secretId); - if (!secret) throw new NotFoundError({ message: `Secret with ID '${secretId}' not found` }); const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); - const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); - - if (!folderWithPath) { - throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); - } - const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -2003,36 +1689,14 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId: folder.projectId }); - - const secretVersions = await secretVersionDAL.findBySecretId(secretId, { - offset, - limit, - sort: [["createdAt", "desc"]] - }); - return secretVersions.map((el) => { - const secretValueHidden = permission.cannot( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: el.key, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) - }) - ); - return reshapeBridgeSecret( - folder.projectId, - folder.environment.envSlug, - folderWithPath.path, - { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }, - secretValueHidden - ); - }); + const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); + return secretVersions.map((el) => + reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }) + ); }; // this is a backfilling API for secret references @@ -2127,26 +1791,16 @@ export const secretV2BridgeServiceFactory = ({ [`${TableName.SecretV2}.id` as "id"]: secretIds } }); - - const sourceActions = [ - ProjectPermissionSecretActions.Delete, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.DescribeSecret - ] as const; - const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; - sourceSecrets.forEach((secret) => { - for (const sourceAction of sourceActions) { - ForbiddenError.from(permission).throwUnlessCan( - sourceAction, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); - } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); }); if (sourceSecrets.length !== secretIds.length) { @@ -2221,17 +1875,27 @@ export const secretV2BridgeServiceFactory = ({ // permission check whether can create or edit the ones in the destination folder locallyCreatedSecrets.forEach((secret) => { - for (const destinationAction of destinationActions) { - ForbiddenError.from(permission).throwUnlessCan( - destinationAction, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationFolder.path, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); - } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationEnvironment, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + }); + + locallyUpdatedSecrets.forEach((secret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationEnvironment, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); }); const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( @@ -2461,7 +2125,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2485,7 +2149,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -2494,7 +2158,7 @@ export const secretV2BridgeServiceFactory = ({ }) ); - const decryptedSecretValue = secret.encryptedValue + const secretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; @@ -2505,7 +2169,7 @@ export const secretV2BridgeServiceFactory = ({ decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -2515,26 +2179,10 @@ export const secretV2BridgeServiceFactory = ({ ) }); - if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ) - ) { - throw new ForbiddenRequestError({ - message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.` - }); - } - const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ environment, secretPath, - value: decryptedSecretValue + value: secretValue }); return { tree: stackTrace, value: expandedValue }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 9cfe49b83..ad8264e81 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -1,7 +1,6 @@ import { Knex } from "knex"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; -import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -37,8 +36,6 @@ export type TGetSecretsDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; - viewSecretValue: boolean; - throwOnMissingReadValuePermission?: boolean; metadataFilter?: { key?: string; value?: string; @@ -51,11 +48,6 @@ export type TGetSecretsDTO = { keys?: string[]; } & TProjectPermission; -export type TGetSecretsMissingReadValuePermissionDTO = Omit< - TGetSecretsDTO, - "viewSecretValue" | "recursive" | "expandSecretReferences" ->; - export type TGetASecretDTO = { secretName: string; path: string; @@ -65,7 +57,6 @@ export type TGetASecretDTO = { includeImports?: boolean; version?: number; projectId: string; - viewSecretValue: boolean; } & Omit; export type TCreateSecretDTO = TProjectPermission & { @@ -173,9 +164,9 @@ export type TFnSecretBulkInsert = { } >; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; }; @@ -197,9 +188,9 @@ export type TFnSecretBulkUpdate = { data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; }[]; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; tx?: Knex; }; @@ -341,5 +332,4 @@ export type TGetSecretsRawByFolderMappingsDTO = { folderMappings: { folderId: string; path: string; environment: string }[]; userId: string; filters: TFindSecretsByFolderIdsFilter; - filterByAction?: ProjectPermissionSecretActions; }; diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index f974ab71e..7772b8518 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; +import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,58 +12,6 @@ export type TSecretVersionV2DALFactory = ReturnType { const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); - const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { - try { - const query = (tx || db.replicaNode())(TableName.SecretVersionV2) - .where(`${TableName.SecretVersionV2}.secretId`, secretId) - .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) - .leftJoin( - TableName.SecretV2JnTag, - `${TableName.SecretV2}.id`, - `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` - ) - .leftJoin( - TableName.SecretTag, - `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, - `${TableName.SecretTag}.id` - ) - .select(selectAllTableCols(TableName.SecretVersionV2)) - .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) - .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); - - if (limit) void query.limit(limit); - if (offset) void query.offset(offset); - if (sort) { - void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); - } - - const docs = await query; - - const data = sqlNestRelationships({ - data: docs, - key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }), - childrenMapper: [ - { - key: "tagId", - label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ - id, - color, - slug, - name: slug - }) - } - ] - }); - - return data; - } catch (error) { - throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` }); - } - }; - // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -176,7 +124,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { pruneExcessVersions, findLatestVersionMany, bulkUpdate, - findLatestVersionByFolderId, - findBySecretId + findLatestVersionByFolderId }; }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index dc41d129f..cbaf7ddcd 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -169,48 +169,6 @@ export const secretDALFactory = (db: TDbClient) => { } }; - const findManySecretsWithTags = async ( - filter: { - secretIds: string[]; - type: SecretType; - }, - tx?: Knex - ) => { - try { - const secrets = await (tx || db.replicaNode())(TableName.Secret) - .whereIn(`${TableName.Secret}.id` as "id", filter.secretIds) - .where("type", filter.type) - .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) - .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) - .select(selectAllTableCols(TableName.Secret)) - .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) - .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); - - const data = sqlNestRelationships({ - data: secrets, - key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), - childrenMapper: [ - { - key: "tagId", - label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ - id, - color, - slug, - name: slug - }) - } - ] - }); - - return data; - } catch (error) { - throw new DatabaseError({ error, name: "get many secrets with tags" }); - } - }; - const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { try { // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) @@ -485,7 +443,6 @@ export const secretDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - pruneSecretReminders, - findManySecretsWithTags + pruneSecretReminders }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 084aa2ca1..1775d1f44 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -13,7 +13,7 @@ import { TSecrets } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -51,8 +51,6 @@ import { TUpdateManySecretsRawFnFactory } from "./secret-types"; -export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = ""; - export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { const appCfg = getConfig(); const secretBlindIndex = await buildSecretBlindIndexFromName({ @@ -192,7 +190,7 @@ export const recursivelyGetSecretPaths = ({ const allowedPaths = paths.filter( (folder) => permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: folder.path @@ -346,7 +344,6 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD export const decryptSecretRaw = ( secret: TSecrets & { - secretValueHidden: boolean; workspace: string; environment: string; secretPath: string; @@ -365,14 +362,12 @@ export const decryptSecretRaw = ( key }); - const secretValue = !secret.secretValueHidden - ? decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }) - : INFISICAL_SECRET_VALUE_HIDDEN_MASK; + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); let secretComment = ""; @@ -390,7 +385,6 @@ export const decryptSecretRaw = ( secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, - secretValueHidden: secret.secretValueHidden, secretValue, secretComment, version: secret.version, @@ -1203,23 +1197,3 @@ export const fnDeleteProjectSecretReminders = async ( } } }; - -export const conditionallyHideSecretValue = ( - shouldHideValue: boolean, - { - secretValueCiphertext, - secretValueIV, - secretValueTag - }: { - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - } -) => { - return { - secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext, - secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV, - secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag, - secretValueHidden: shouldHideValue - }; -}; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 65e3eaf4f..00b0e7da8 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -402,8 +402,7 @@ export const secretQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true, - viewSecretValue: true + hasSecretAccess: () => true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 27981a555..93f68e813 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -6,7 +6,6 @@ import { ActionProjectType, ProjectMembershipRole, ProjectUpgradeStatus, - ProjectVersion, SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, @@ -14,11 +13,7 @@ import { } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -53,7 +48,6 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretDALFactory } from "./secret-dal"; import { - conditionallyHideSecretValue, decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkDelete, @@ -101,7 +95,7 @@ type TSecretServiceFactoryDep = { projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds" + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" >; secretV2BridgeService: TSecretV2BridgeServiceFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; @@ -210,7 +204,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -328,7 +322,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -450,22 +444,7 @@ export const secretServiceFactory = ({ environmentSlug: folder.environment.slug }); } - - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path - }) - ); - - return { - ...updatedSecret[0], - ...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]), - workspace: projectId, - environment, - secretPath: path - }; + return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path }; }; const deleteSecret = async ({ @@ -488,7 +467,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Delete, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -561,19 +540,7 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); - - return { - ...deletedSecret[0], - ...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]), - _id: deletedSecret[0].id, - workspace: projectId, - environment, - secretPath: path - }; + return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; }; const getSecrets = async ({ @@ -622,7 +589,7 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -647,7 +614,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -704,7 +671,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -754,7 +721,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -772,7 +739,6 @@ export const secretServiceFactory = ({ if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { return { ...importedSecrets[i].secrets[j], - secretValueHidden: false, workspace: projectId, environment: importedSecrets[i].environment, secretPath: importedSecrets[i].secretPath @@ -783,13 +749,7 @@ export const secretServiceFactory = ({ } if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); - return { - ...secret, - secretValueHidden: false, // Always false because we check permission at the beginning of the function - workspace: projectId, - environment, - secretPath: path - }; + return { ...secret, workspace: projectId, environment, secretPath: path }; }; const createManySecret = async ({ @@ -811,7 +771,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -899,7 +859,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -941,8 +901,8 @@ export const secretServiceFactory = ({ if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); const references = await getSecretReference(projectId); - const secrets = await secretDAL.transaction(async (tx) => { - const updatedSecrets = await fnSecretBulkUpdate({ + const secrets = await secretDAL.transaction(async (tx) => + fnSecretBulkUpdate({ folderId, projectId, tx, @@ -972,18 +932,8 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL - }); - - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); - - return updatedSecrets.map((secret) => ({ - ...secret, - ...conditionallyHideSecretValue(secretValueHidden, secret) - })); - }); + }) + ); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -1017,7 +967,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Delete, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -1069,15 +1019,7 @@ export const secretServiceFactory = ({ } } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); - - return secrets.map((secret) => ({ - ...secret, - ...conditionallyHideSecretValue(secretValueHidden, secret) - })); + return secrets; }); await snapshotService.performSnapshot(folderId); @@ -1238,7 +1180,6 @@ export const secretServiceFactory = ({ secretName, path: secretPath, environment, - viewSecretValue: false, type: "shared" }); @@ -1253,11 +1194,10 @@ export const secretServiceFactory = ({ | (typeof groupPermissions)[number] ) => { const allowedActions = [ - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Delete, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Edit + ProjectPermissionActions.Read, + ProjectPermissionActions.Delete, + ProjectPermissionActions.Create, + ProjectPermissionActions.Edit ].filter((action) => entityPermission.permission.can( action, @@ -1294,13 +1234,11 @@ export const secretServiceFactory = ({ actorId, actorOrgId, actorAuthMethod, - viewSecretValue, environment, includeImports, expandSecretReferences, recursive, tagSlugs = [], - throwOnMissingReadValuePermission = true, ...paramsV2 }: TGetSecretsRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1311,8 +1249,6 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, - viewSecretValue, - throwOnMissingReadValuePermission, environment, path, recursive, @@ -1321,7 +1257,6 @@ export const secretServiceFactory = ({ tagSlugs, ...paramsV2 }); - return { secrets, imports }; } @@ -1350,20 +1285,14 @@ export const secretServiceFactory = ({ recursive }); - const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey)); + const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); const filteredSecrets = tagSlugs.length ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) : decryptedSecrets; const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const decryptedImportSecrets = importedSecrets.map((sec) => decryptSecretRaw( - { - ...sec, - environment: el.environment, - workspace: projectId, - secretPath: el.secretPath, - secretValueHidden: false - }, + { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, botKey ) ); @@ -1374,7 +1303,6 @@ export const secretServiceFactory = ({ const importedEntries = decryptedImportSecrets.reduce( ( accum: { - secretValueHidden: boolean; secretKey: string; secretPath: string; workspace: string; @@ -1418,7 +1346,6 @@ export const secretServiceFactory = ({ Object.keys(secretsGroupByPath).map((groupedPath) => Promise.allSettled( secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { - if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: groupedPath, @@ -1435,7 +1362,6 @@ export const secretServiceFactory = ({ processedImports.map((processedImport) => Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { - if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: path, @@ -1461,7 +1387,6 @@ export const secretServiceFactory = ({ path, actor, environment, - viewSecretValue, projectId: workspaceId, expandSecretReferences, projectSlug, @@ -1481,7 +1406,6 @@ export const secretServiceFactory = ({ includeImports, actorAuthMethod, path, - viewSecretValue, actorOrgId, actor, actorId, @@ -1512,7 +1436,6 @@ export const secretServiceFactory = ({ message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, name: "bot_not_found_error" }); - const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); if (expandSecretReferences) { @@ -1531,10 +1454,7 @@ export const secretServiceFactory = ({ decryptedSecret.secretValue = expandedSecretValue || ""; } - return { - secretMetadata: undefined, - ...decryptedSecret - }; + return { secretMetadata: undefined, ...decryptedSecret }; }; const createSecretRaw = async ({ @@ -1685,16 +1605,7 @@ export const secretServiceFactory = ({ tags: tagIds }); - return { - type: SecretProtectionType.Direct as const, - secret: decryptSecretRaw( - { - ...secret, - secretValueHidden: false - }, - botKey - ) - }; + return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; }; const updateSecretRaw = async ({ @@ -2090,7 +2001,7 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secrets: secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey) + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) ) }; }; @@ -2379,12 +2290,6 @@ export const secretServiceFactory = ({ const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); - const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); - - if (!folderWithPath) { - throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); - } - const { botKey } = await projectBotService.getBotKey(folder.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); @@ -2398,42 +2303,18 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - const secretVersions = await secretVersionDAL.findBySecretId(secretId, { - offset, - limit, - sort: [["createdAt", "desc"]] - }); - return secretVersions.map((el) => { - const secretKey = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key: botKey - }); - - const secretValueHidden = permission.cannot( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: secretKey, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) - }) - ); - - return decryptSecretRaw( + const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); + return secretVersions.map((el) => + decryptSecretRaw( { - secretValueHidden, ...el, workspace: folder.projectId, environment: folder.environment.envSlug, - secretPath: folderWithPath.path + secretPath: "/" }, botKey - ); - }); + ) + ); }; const attachTags = async ({ @@ -2459,7 +2340,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2565,7 +2446,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2731,7 +2612,7 @@ export const secretServiceFactory = ({ message: `Project with slug '${projectSlug}' not found` }); } - if (project.version === ProjectVersion.V3) { + if (project.version === 3) { return secretV2BridgeService.moveSecrets({ sourceEnvironment, sourceSecretPath, @@ -2756,6 +2637,30 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath + }) + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath + }) + ); + const { botKey } = await projectBotService.getBotKey(project.id); if (!botKey) { throw new NotFoundError({ @@ -2783,9 +2688,11 @@ export const secretServiceFactory = ({ }); } - const sourceSecrets = await secretDAL.findManySecretsWithTags({ + const sourceSecrets = await secretDAL.find({ type: SecretType.Shared, - secretIds + $in: { + id: secretIds + } }); if (sourceSecrets.length !== secretIds.length) { @@ -2794,52 +2701,21 @@ export const secretServiceFactory = ({ }); } - const sourceActions = [ - ProjectPermissionSecretActions.Delete, - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue - ] as const; - const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; - - const decryptedSourceSecrets = sourceSecrets.map((secret) => { - const secretKey = decryptSymmetric128BitHexKeyUTF8({ + const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ + ...secret, + secretKey: decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, key: botKey - }); - - for (const destinationAction of destinationActions) { - ForbiddenError.from(permission).throwUnlessCan( - destinationAction, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); - } - - for (const sourceAction of sourceActions) { - ForbiddenError.from(permission).throwUnlessCan( - sourceAction, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath - }) - ); - } - - return { - ...secret, - secretKey, - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key: botKey - }) - }; - }); + }), + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: botKey + }) + })); let isSourceUpdated = false; let isDestinationUpdated = false; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 8ac9f58c0..158605276 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -180,8 +180,6 @@ export type TGetSecretsRawDTO = { expandSecretReferences?: boolean; path: string; environment: string; - viewSecretValue: boolean; - throwOnMissingReadValuePermission?: boolean; includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; @@ -207,7 +205,6 @@ export type TGetASecretRawDTO = { secretName: string; path: string; environment: string; - viewSecretValue: boolean; expandSecretReferences?: boolean; type: "shared" | "personal"; includeImports?: boolean; @@ -412,7 +409,7 @@ export type TCreateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -449,7 +446,7 @@ export type TUpdateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 8e4544c19..8e77858a5 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; +import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,50 +12,6 @@ export type TSecretVersionDALFactory = ReturnType { const secretVersionOrm = ormify(db, TableName.SecretVersion); - const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { - try { - const query = (tx || db.replicaNode())(TableName.SecretVersion) - .where(`${TableName.SecretVersion}.secretId`, secretId) - .leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`) - .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) - .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) - .select(selectAllTableCols(TableName.SecretVersion)) - .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) - .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); - - if (limit) void query.limit(limit); - if (offset) void query.offset(offset); - if (sort) { - void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); - } - - const docs = await query; - - const data = sqlNestRelationships({ - data: docs, - key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }), - childrenMapper: [ - { - key: "tagId", - label: "tags" as const, - mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ - id, - color, - slug, - name: slug - }) - } - ] - }); - - return data; - } catch (error) { - throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` }); - } - }; - // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -193,7 +149,6 @@ export const secretVersionDALFactory = (db: TDbClient) => { findLatestVersionMany, bulkUpdate, findLatestVersionByFolderId, - findBySecretId, bulkUpdateNoVersionIncrement }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 9b87c29f8..654917feb 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -5,11 +5,7 @@ import bcrypt from "bcrypt"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - ProjectPermissionActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -71,7 +67,7 @@ export const serviceTokenServiceFactory = ({ scopes.forEach(({ environment, secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); }); diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts index d06d90a9d..f1a700908 100644 --- a/backend/src/services/service-token/service-token-types.ts +++ b/backend/src/services/service-token/service-token-types.ts @@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = { iv: string; tag: string; expiresIn?: number | null; - permissions: ("read" | "write" | "readValue")[]; + permissions: ("read" | "write")[]; } & TProjectPermission; export type TGetServiceTokenInfoDTO = Omit; diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx index b8724f5dc..8164353d8 100644 --- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx +++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx @@ -1,21 +1,13 @@ -/* eslint-disable no-nested-ternary */ -import { useEffect, useState } from "react"; -import { - faChevronRight, - faExclamationTriangle, - faEye, - faEyeSlash -} from "@fortawesome/free-solid-svg-icons"; +import { useState } from "react"; +import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Collapsible from "@radix-ui/react-collapsible"; -import { AxiosError } from "axios"; import { twMerge } from "tailwind-merge"; -import { createNotification } from "@app/components/notifications"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useGetSecretReferenceTree } from "@app/hooks/api"; -import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types"; +import { TSecretReferenceTraceNode } from "@app/hooks/api/types"; import style from "./SecretReferenceDetails.module.css"; @@ -92,7 +84,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - const { data, isPending, isError, error } = useGetSecretReferenceTree({ + const { data, isPending } = useGetSecretReferenceTree({ secretPath, environmentSlug: environment, projectId, @@ -102,26 +94,6 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const tree = data?.tree; const secretValue = data?.value; - useEffect(() => { - if (error instanceof AxiosError) { - const err = error?.response?.data as TApiErrors; - - if (err?.error === ApiErrorTypes.CustomForbiddenError) { - createNotification({ - title: "You don't have permission to view reference tree", - text: "You don't have permission to view one or more of the referenced secrets.", - type: "error" - }); - return; - } - createNotification({ - title: "Error fetching secret reference tree", - text: "Please try again later.", - type: "error" - }); - } - }, [error]); - if (isPending) { return (
@@ -142,16 +114,11 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
- {isError ? ( -
- -

Error fetching secret reference tree

-
- ) : tree ? ( + {tree && (
- ) : null} + )}
Click a secret key to view its sub-references. diff --git a/frontend/src/components/v2/Blur/Blur.tsx b/frontend/src/components/v2/Blur/Blur.tsx deleted file mode 100644 index bd1ded40a..000000000 --- a/frontend/src/components/v2/Blur/Blur.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -import { Tooltip } from "../Tooltip/Tooltip"; - -interface IProps { - className?: string; - tooltipText?: string; -} - -export const Blur = ({ className, tooltipText }: IProps) => { - return ( - -
- ******** -
-
- ); -}; diff --git a/frontend/src/components/v2/Blur/index.tsx b/frontend/src/components/v2/Blur/index.tsx deleted file mode 100644 index 50fb7ccc6..000000000 --- a/frontend/src/components/v2/Blur/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { Blur } from "./Blur"; diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index dd6d3575e..6fbe96238 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -120,7 +120,6 @@ export const InfisicalSecretInput = forwardRef( const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused; const { data: secrets } = useGetProjectSecrets({ - viewSecretValue: false, environment: suggestionSource.environment || "", secretPath: suggestionSource.secretPath || "", workspaceId, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 260ffce09..d368a949f 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -7,14 +7,6 @@ export enum ProjectPermissionActions { Delete = "delete" } -export enum ProjectPermissionSecretActions { - DescribeSecret = "read", - ReadValue = "readValue", - Create = "create", - Edit = "edit", - Delete = "delete" -} - export enum ProjectPermissionDynamicSecretActions { ReadRootCredential = "read-root-credential", CreateRootCredential = "create-root-credential", @@ -146,7 +138,7 @@ export type SecretImportSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionSecretActions, + ProjectPermissionActions, ( | ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index f704081a9..4b63be988 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -207,7 +207,6 @@ export const useGetProjectSecretsDetails = ( search = "", includeSecrets, includeFolders, - viewSecretValue, includeImports, includeDynamicSecrets, tags @@ -232,7 +231,6 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, - viewSecretValue, offset, projectId, environment, @@ -249,7 +247,6 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, - viewSecretValue, offset, projectId, environment, diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 4808b463c..786b2b43a 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -69,7 +69,6 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit< TGetDashboardProjectSecretsOverviewDTO, "environments" > & { - viewSecretValue: boolean; environment: string; includeImports?: boolean; tags: Record; diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx index 39fa1058a..74acf744b 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -79,7 +79,6 @@ export const decryptSecrets = ( id: encSecret.id, env: encSecret.environment, key: secretKey, - secretValueHidden: encSecret.secretValueHidden, value: secretValue, tags: encSecret.tags, comment: secretComment, diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index dfc8c6497..c2ddd2fbe 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -137,7 +137,6 @@ export const useGetImportedSecretsSingleEnv = ({ env: encSecret.environment, key: encSecret.secretKey, value: encSecret.secretValue, - secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, @@ -177,7 +176,6 @@ export const useGetImportedSecretsAllEnvs = ({ env: encSecret.environment, key: encSecret.secretKey, value: encSecret.secretValue, - secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index d273bd5ea..8a16b4180 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -75,7 +75,6 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) => id: secretVersion.secretId, env: data.environment.slug, key: secretVersion.secretKey, - secretValueHidden: secretVersion.secretValueHidden, value: secretVersion.secretValue || "", tags: secretVersion.tags, comment: secretVersion.secretComment, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 3e82c3b90..a6dd959b0 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -26,13 +26,8 @@ import { export const secretKeys = { // this is also used in secretSnapshot part - getProjectSecret: ({ - workspaceId, - environment, - secretPath, - viewSecretValue - }: TGetProjectSecretsKey) => - [{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const, + getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) => + [{ workspaceId, environment, secretPath }, "secrets"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretAccessList: ({ workspaceId, @@ -49,15 +44,13 @@ export const fetchProjectSecrets = async ({ environment, secretPath, includeImports, - expandSecretReferences, - viewSecretValue + expandSecretReferences }: TGetProjectSecretsKey) => { const { data } = await apiRequest.get("/api/v3/secrets/raw", { params: { environment, workspaceId, secretPath, - viewSecretValue, expandSecretReferences, include_imports: includeImports } @@ -75,7 +68,6 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { env: el.environment, key: el.secretKey, value: el.secretValue, - secretValueHidden: el.secretValueHidden, tags: el.tags || [], comment: el.secretComment || "", reminderRepeatDays: el.secretReminderRepeatDays, @@ -115,7 +107,6 @@ export const useGetProjectSecrets = ({ workspaceId, environment, secretPath, - viewSecretValue, options }: TGetProjectSecretsDTO & { options?: Omit< @@ -132,13 +123,8 @@ export const useGetProjectSecrets = ({ ...options, // wait for all values to be available enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), - queryKey: secretKeys.getProjectSecret({ - workspaceId, - environment, - secretPath, - viewSecretValue - }), - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }), + queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }), + queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), select: useCallback( (data: Awaited>) => mergePersonalSecrets(data.secrets), [] diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index d7b6cbcad..92dc220b8 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -19,7 +19,6 @@ export type EncryptedSecret = { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; - secretValueHidden: boolean; __v: number; createdAt: string; updatedAt: string; @@ -38,7 +37,6 @@ export type SecretV3RawSanitized = { version: number; key: string; value?: string; - secretValueHidden: boolean; comment?: string; reminderRepeatDays?: number | null; reminderNote?: string | null; @@ -63,7 +61,6 @@ export type SecretV3Raw = { environment: string; version: number; type: string; - secretValueHidden: boolean; secretKey: string; secretPath: string; secretValue?: string; @@ -98,7 +95,6 @@ export type SecretVersions = { envId: string; secretKey: string; secretValue?: string; - secretValueHidden: boolean; secretComment?: string; tags: WsTag[]; __v: number; @@ -113,7 +109,6 @@ export type TGetProjectSecretsKey = { environment: string; secretPath?: string; includeImports?: boolean; - viewSecretValue?: boolean; expandSecretReferences?: boolean; }; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 029d8a605..c03358b42 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -46,8 +46,7 @@ export enum ApiErrorTypes { ValidationError = "ValidationFailure", BadRequestError = "BadRequest", UnauthorizedError = "UnauthorizedError", - ForbiddenError = "PermissionDenied", - CustomForbiddenError = "ForbiddenError" + ForbiddenError = "PermissionDenied" } export type TApiErrors = @@ -70,12 +69,6 @@ export type TApiErrors = details: PureAbility["rules"]; statusCode: 403; } - | { - reqId: string; - error: ApiErrorTypes.CustomForbiddenError; - message: string; - statusCode: 403; - } | { reqId: string; statusCode: 400; diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index a669a8360..46a9c5680 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -58,7 +58,6 @@ const schema = z.object({ permissions: z .object({ read: z.boolean(), - readValue: z.boolean(), write: z.boolean() }) .required() @@ -297,19 +296,14 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { name="permissions" defaultValue={{ read: true, - readValue: false, write: false }} render={({ field: { onChange, value }, fieldState: { error } }) => { const options = [ { - label: "Describe Secret (default)", + label: "Read (default)", value: "read" }, - { - label: "Read Value (optional)", - value: "readValue" - }, { label: "Write (optional)", value: "write" diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index 8aca40a34..de2231fc9 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -117,27 +117,24 @@ export const GeneralPermissionPolicies = {actions.map(({ label, value }) => { if (typeof value !== "string") return undefined; - return ( { - return ( -
- - {label} - -
- ); - }} + render={({ field }) => ( +
+ + {label} + +
+ )} /> ); })} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index e26ec8bc4..7dcb118fd 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -9,7 +9,6 @@ import { PermissionConditionOperators, ProjectPermissionDynamicSecretActions, ProjectPermissionKmipActions, - ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, TPermissionCondition, TPermissionConditionOperators @@ -23,14 +22,6 @@ const GeneralPolicyActionSchema = z.object({ create: z.boolean().optional() }); -const SecretPolicyActionSchema = z.object({ - read: z.boolean().optional(), // describe secret - edit: z.boolean().optional(), - delete: z.boolean().optional(), - create: z.boolean().optional(), - readValue: z.boolean().optional() -}); - const CmekPolicyActionSchema = z.object({ read: z.boolean().optional(), edit: z.boolean().optional(), @@ -123,7 +114,7 @@ export const projectRoleFormSchema = z.object({ .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ - [ProjectPermissionSub.Secrets]: SecretPolicyActionSchema.extend({ + [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ inverted: z.boolean().optional(), conditions: ConditionSchema }) @@ -292,28 +283,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { }); return; } - - if (subject === ProjectPermissionSub.Secrets) { - const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret); - const canEdit = action.includes(ProjectPermissionSecretActions.Edit); - const canDelete = action.includes(ProjectPermissionSecretActions.Delete); - const canCreate = action.includes(ProjectPermissionSecretActions.Create); - const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); - - // from above statement we are sure it won't be undefined - formVal[subject]!.push({ - read: canRead, - create: canCreate, - edit: canEdit, - delete: canDelete, - readValue: canReadValue, - conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], - inverted - }); - - return; - } - // for other subjects const canRead = action.includes(ProjectPermissionActions.Read); const canEdit = action.includes(ProjectPermissionActions.Edit); @@ -514,9 +483,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Secrets]: { title: "Secrets", actions: [ - { label: "Describe Secret", value: "read" }, + { label: "Read", value: "read" }, { label: "Create", value: "create" }, - { label: "Read Value", value: "readValue" }, { label: "Modify", value: "edit" }, { label: "Remove", value: "delete" } ] diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index 4847feb69..c894d6b14 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -19,7 +19,6 @@ import { import { getKeyValue } from "@app/helpers/parseEnvVar"; import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api"; import { SecretType } from "@app/hooks/api/types"; -import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; const typeSchema = z .object({ @@ -276,7 +275,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { isMulti options={environments.filter((environment) => permission.can( - ProjectPermissionSecretActions.Create, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment: environment.slug, secretPath, diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 4887d4fd7..d3dfd49e7 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -25,7 +25,6 @@ import { ModalTrigger, Tooltip } from "@app/components/v2"; -import { Blur } from "@app/components/v2/Blur"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; @@ -40,7 +39,6 @@ type Props = { isVisible?: boolean; isImportedSecret: boolean; environment: string; - secretValueHidden: boolean; secretPath: string; onSecretCreate: (env: string, key: string, value: string) => Promise; onSecretUpdate: ( @@ -60,7 +58,6 @@ export const SecretEditRow = ({ isImportedSecret, onSecretUpdate, secretName, - secretValueHidden, onSecretCreate, onSecretDelete, environment, @@ -143,29 +140,24 @@ export const SecretEditRow = ({ />
- {secretValueHidden ? ( - - ) : ( - ( - - )} - /> - )} + ( + + )} + />
-
- +
NO ACCESS
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index 3a66f6896..b9376f6b4 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -221,13 +221,10 @@ export const SecretOverviewTableRow = ({ secretPath={secretPath} isVisible={isSecretVisible} secretName={secretKey} - secretValueHidden={secret?.secretValueHidden || false} defaultValue={ - secret?.secretValueHidden - ? "" - : secret?.valueOverride || - secret?.value || - importedSecret?.secret?.value + secret?.valueOverride || + secret?.value || + importedSecret?.secret?.value } secretId={secret?.id} isOverride={Boolean(secret?.valueOverride)} diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 3b05f10fa..ae1916b86 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -10,8 +10,12 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; -import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; -import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; @@ -51,8 +55,8 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secretTags: (secretDetails?.tags || []).map((i) => i.slug) }); const isSecretInEnvReadOnly = - permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) && - permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); + permission.can(ProjectPermissionActions.Read, secretPermissionSubject) && + permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject); if (isSecretInEnvReadOnly) { return true; } diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx index 9943de5b6..e901a4ff7 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx @@ -110,31 +110,21 @@ export const QuickSearchSecretItem = ({ )} {isSingleEnv ? ( - { + e.stopPropagation(); + const el = envSlugMap.get(groupSecret.env)?.name; + if (el) { + handleCopy(groupSecret.value!, el); + } + }} > - { - e.stopPropagation(); - const el = envSlugMap.get(groupSecret.env)?.name; - if (el) { - handleCopy(groupSecret.value!, el); - } - }} - > - - - + + ) : ( @@ -168,24 +158,14 @@ export const QuickSearchSecretItem = ({ )} - - - - - + + Hover to Reveal... diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx index 4e048b941..4738e411e 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -11,7 +11,6 @@ import { useProjectPermission, useWorkspace } from "@app/context"; -import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { @@ -59,7 +58,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri // user should have the ability to delete secrets/folders in at least one of the envs const shouldShowDelete = userAvailableEnvs.some((env) => permission.can( - ProjectPermissionSecretActions.Delete, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, @@ -111,7 +110,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri (accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => { const entry = secretRecord[env.slug]; const canDeleteSecret = permission.can( - ProjectPermissionSecretActions.Delete, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx index a81fa3e53..8a27a24c0 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx @@ -25,8 +25,7 @@ import { Spinner, Switch } from "@app/components/v2"; -import { ProjectPermissionSub, useProjectPermission } from "@app/context"; -import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { useDebounce } from "@app/hooks"; import { useMoveSecrets } from "@app/hooks/api"; import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard"; @@ -96,7 +95,7 @@ const Content = ({ env.slug, { missingPermissions: permission.cannot( - ProjectPermissionSecretActions.Delete, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: sourceSecretPath, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 5cbefe9c3..daf875a78 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -25,7 +25,6 @@ import { useProjectPermission, useWorkspace } from "@app/context"; -import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useGetImportedSecretsSingleEnv, @@ -104,16 +103,7 @@ const Page = () => { const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; const canReadSecret = permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: "*", - secretTags: ["*"] - }) - ); - const canReadSecretValue = permission.can( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -186,7 +176,6 @@ const Page = () => { orderDirection, includeImports: canReadSecretImports && filter.include.import, includeFolders: filter.include.folder, - viewSecretValue: canReadSecretValue, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index d545ba748..03ff3dd57 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -21,7 +21,6 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AxiosError } from "axios"; import FileSaver from "file-saver"; import { twMerge } from "tailwind-merge"; @@ -55,7 +54,7 @@ import { import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; -import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; +import { SecretType, WsTag } from "@app/hooks/api/types"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { @@ -153,71 +152,51 @@ export const ActionBar = ({ }; const handleSecretDownload = async () => { - try { - const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ - workspaceId, - expandSecretReferences: true, - includeImports: true, - environment, - secretPath - }); - const secretsPicked = new Set(); - const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; - localSecrets.forEach((el) => { - secretsPicked.add(el.secretKey); - secretsToDownload.push({ - key: el.secretKey, - value: el.secretValue, - comment: el.secretComment - }); + const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ + workspaceId, + expandSecretReferences: true, + includeImports: true, + environment, + secretPath + }); + const secretsPicked = new Set(); + const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; + localSecrets.forEach((el) => { + secretsPicked.add(el.secretKey); + secretsToDownload.push({ + key: el.secretKey, + value: el.secretValue, + comment: el.secretComment }); + }); - for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { - for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { - const secret = localImportedSecrets[i].secrets[j]; - if (!secretsPicked.has(secret.secretKey)) { - secretsToDownload.push({ - key: secret.secretKey, - value: secret.secretValue, - comment: secret.secretComment - }); - } - secretsPicked.add(secret.secretKey); - } - } - - const file = secretsToDownload - .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) - .reduce( - (prev, { key, comment, value }, index) => - prev + - (comment - ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` - : `${key}=${value}\n`), - "" - ); - - const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); - FileSaver.saveAs(blob, `${environment}.env`); - } catch (err) { - if (err instanceof AxiosError) { - const error = err?.response?.data as TApiErrors; - - if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) { - createNotification({ - title: "You don't have permission to download secrets", - text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.", - type: "error" + for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { + for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { + const secret = localImportedSecrets[i].secrets[j]; + if (!secretsPicked.has(secret.secretKey)) { + secretsToDownload.push({ + key: secret.secretKey, + value: secret.secretValue, + comment: secret.secretComment }); - return; } + secretsPicked.add(secret.secretKey); } - createNotification({ - title: "Failed to download secrets", - text: "Please try again later.", - type: "error" - }); } + + const file = secretsToDownload + .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) + .reduce( + (prev, { key, comment, value }, index) => + prev + + (comment + ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` + : `${key}=${value}\n`), + "" + ); + + const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, `${environment}.env`); }; const handleSecretBulkDelete = async () => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index cc60b29d0..168cd6f43 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -9,14 +9,12 @@ import { faPlus, faShare, faTag, - faTrash, - faTriangleExclamation + faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { Link } from "@tanstack/react-router"; import { format } from "date-fns"; -import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -46,7 +44,6 @@ import { useProjectPermission, useWorkspace } from "@app/context"; -import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp, useToggle } from "@app/hooks"; import { useGetSecretVersion } from "@app/hooks/api"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; @@ -125,7 +122,7 @@ export const SecretDetailSidebar = ({ const selectTagSlugs = selectedTags.map((i) => i.slug); const cannotEditSecret = permission.cannot( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -133,29 +130,16 @@ export const SecretDetailSidebar = ({ secretTags: selectTagSlugs }) ); - - const cannotReadSecretValue = permission.cannot( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: secretKey, - secretTags: selectTagSlugs - }) - ); - const isReadOnly = permission.can( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: secretKey, secretTags: selectTagSlugs }) - ) && - cannotEditSecret && - cannotReadSecretValue; + ) && cannotEditSecret; const overrideAction = watch("overrideAction"); const isOverridden = @@ -208,16 +192,9 @@ export const SecretDetailSidebar = ({ await onSaveSecret(secret, { ...secret, ...data }, () => reset()); }; - const handleReminderSubmit = async ( - reminderRepeatDays: number | null | undefined, - reminderNote: string | null | undefined - ) => { - await onSaveSecret( - secret, - { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, - () => {} - ); - }; + const handleReminderSubmit = async (reminderRepeatDays: number | null | undefined, reminderNote: string | null | undefined) => { + await onSaveSecret(secret, { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, () => { }); + } const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false); @@ -236,7 +213,7 @@ export const SecretDetailSidebar = ({ if (data) { setValue("reminderRepeatDays", data.days, { shouldDirty: false }); setValue("reminderNote", data.note, { shouldDirty: false }); - handleReminderSubmit(data.days, data.note); + handleReminderSubmit(data.days, data.note) } }} /> @@ -284,63 +261,38 @@ export const SecretDetailSidebar = ({ key="secret-value" control={control} render={({ field }) => ( -
- - - - The value of this secret is hidden because you do not have the - read secret value permission. - -
- ) : undefined - } - label="Value" - > -
- - - - -
- -
+ + + )} /> )}
+
+ +
@@ -666,34 +618,51 @@ export const SecretDetailSidebar = ({
Version History
- {secretVersion?.map( - ({ createdAt, secretValue, version, id, secretValueHidden }, index) => ( -
-
-
-
-
- v{version} -
+ {secretVersion?.map(({ createdAt, secretValue, version, id }) => ( +
+
+
+
+
+ v{version}
-
{format(new Date(createdAt), "Pp")}
-
-
-
+
{format(new Date(createdAt), "Pp")}
+
+
+
+
+
+
+
+ Value:
-
-
- Value: -
-
-
- - + -
- - {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} - +
+ + {secretValue?.replace(/./g, "*")} + - -
+ } + }} + > + + +
-
- - setValue("value", secretValue)} - > - - - -
- ) - )} +
+ + setValue("value", secretValue)} + > + + + +
+
+ ))}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 02f7a0aa9..86905f351 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-nested-ternary */ /* eslint-disable simple-import-sort/imports */ import { ProjectPermissionCan } from "@app/components/permissions"; import { @@ -45,8 +44,6 @@ import { SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails"; -import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; -import { Blur } from "@app/components/v2/Blur"; import { FontAwesomeSpriteName, formSchema, @@ -102,14 +99,8 @@ export const SecretItem = memo( trigger, formState: { isDirty, isSubmitting, errors } } = useForm({ - defaultValues: { - ...secret, - value: secret.secretValueHidden ? "" : secret.value - }, - values: { - ...secret, - value: secret.secretValueHidden ? "" : secret.value - }, + defaultValues: secret, + values: secret, resolver: zodResolver(formSchema) }); @@ -132,7 +123,7 @@ export const SecretItem = memo( const isReadOnly = permission.can( - ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -141,7 +132,7 @@ export const SecretItem = memo( }) ) && permission.cannot( - ProjectPermissionSecretActions.Edit, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -150,8 +141,6 @@ export const SecretItem = memo( }) ); - const { secretValueHidden } = secret; - const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); useEffect(() => { let timer: NodeJS.Timeout; @@ -283,8 +272,6 @@ export const SecretItem = memo( /> )} /> - ) : secretValueHidden ? ( - ) : ( )} @@ -307,7 +293,6 @@ export const SecretItem = memo(
{ className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent" />
- +
+ ******** +
))} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx index 076cf3b6d..d6680bcef 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx @@ -20,7 +20,6 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { Blur } from "@app/components/v2/Blur"; import { useToggle } from "@app/hooks"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; @@ -121,25 +120,11 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => { Value {isModified && ( - {preSecret?.secretValueHidden ? ( - - ) : ( - - )} + )} - {postSecret?.secretValueHidden ? ( - - ) : ( - - )} + {Boolean(preSecret?.idOverride || postSecret?.idOverride) && ( From aa76924ee60c00ce6c9724ad246aa49bb57e2927 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 5 Mar 2025 14:48:28 -0500 Subject: [PATCH 54/54] fix import --- backend/src/lib/gateway/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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) => {