From 8dac31b71fd9dfa686e80adbe545ad665344e4c6 Mon Sep 17 00:00:00 2001 From: KodekaTeam Date: Sun, 9 Aug 2026 00:37:15 +0700 Subject: [PATCH] feat: implement sensitive data redaction for viewer roles - Introduced SensitiveConnectionValue component to handle redaction of sensitive connection details across the frontend. - Updated API to include canViewServerConnection and canViewEnvironmentValues functions to determine access based on user roles. - Enhanced backend response sanitization to remove sensitive data for viewer roles, ensuring no sensitive information is exposed in API responses. - Applied redaction logic in various components including ServersTable, DomainsTable, and EnvironmentHeader to maintain UI consistency while hiding sensitive information. - Added comprehensive audit documentation outlining the implementation and maintenance rules for sensitive data handling. --- src/app/domains/components/DomainsTable.tsx | 9 +- src/app/page.tsx | 8 +- .../components/header/EnvironmentHeader.tsx | 8 +- .../environment/EnvironmentTabPanel.tsx | 39 +++++++-- .../components/header/AppDetailHeader.tsx | 10 ++- src/app/projects/[projectId]/page.tsx | 14 +++- src/app/servers/components/ServersTable.tsx | 16 ++-- src/components/SensitiveConnectionValue.tsx | 35 ++++++++ src/lib/access-policy.ts | 24 ++++++ src/lib/api.ts | 34 ++++++-- src/server/index.ts | 5 +- src/server/lib/access-policy.ts | 12 +++ src/server/lib/viewer-response-sanitizer.ts | 82 +++++++++++++++++++ src/server/routes/containers.ts | 8 +- src/server/routes/domains.ts | 45 ++++++++-- 15 files changed, 312 insertions(+), 37 deletions(-) create mode 100644 src/components/SensitiveConnectionValue.tsx create mode 100644 src/lib/access-policy.ts create mode 100644 src/server/lib/access-policy.ts create mode 100644 src/server/lib/viewer-response-sanitizer.ts diff --git a/src/app/domains/components/DomainsTable.tsx b/src/app/domains/components/DomainsTable.tsx index 1ef7328..a9d6374 100644 --- a/src/app/domains/components/DomainsTable.tsx +++ b/src/app/domains/components/DomainsTable.tsx @@ -1,5 +1,6 @@ import TablePagination from "@/components/TablePagination"; -import { Domain, SslCert } from "@/lib/api"; +import SensitiveConnectionValue from "@/components/SensitiveConnectionValue"; +import { canViewServerConnection, Domain, SslCert } from "@/lib/api"; import { ExternalLink, Globe, @@ -62,6 +63,7 @@ export default function DomainsTable({ onDelete, onRenew, }: DomainsTableProps) { + const isConnectionRedacted = !canViewServerConnection(); if (domains.length === 0) { return null; } @@ -186,7 +188,10 @@ export default function DomainsTable({ color: "var(--text-secondary)", }} > - {domain.value} + diff --git a/src/app/page.tsx b/src/app/page.tsx index f51a4da..5e20b98 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,7 @@ "use client"; import DashboardLayout from "@/components/DashboardLayout"; +import SensitiveConnectionValue from "@/components/SensitiveConnectionValue"; import TablePagination from "@/components/TablePagination"; import { useTablePagination } from "@/lib/use-table-pagination"; import { useState, useEffect, useCallback, useRef } from "react"; @@ -35,6 +36,7 @@ import { DashboardOverview, DashboardLiveMetrics, AuditLog, + canViewServerConnection, } from "@/lib/api"; import type { Server as ServerInfo } from "@/lib/api"; import { useRequireAuth } from "@/lib/auth-state"; @@ -95,6 +97,7 @@ function formatUpdatedAt(value?: string) { } function DashboardContent() { + const isConnectionRedacted = !canViewServerConnection(); const [overview, setOverview] = useState(null); const [serverData, setServerData] = useState([]); const [liveMetrics, setLiveMetrics] = useState( @@ -625,7 +628,10 @@ function DashboardContent() { margin: 0, }} > - {s.ip} +

diff --git a/src/app/projects/[projectId]/environments/[environmentId]/components/header/EnvironmentHeader.tsx b/src/app/projects/[projectId]/environments/[environmentId]/components/header/EnvironmentHeader.tsx index 0d71a9c..faf460d 100644 --- a/src/app/projects/[projectId]/environments/[environmentId]/components/header/EnvironmentHeader.tsx +++ b/src/app/projects/[projectId]/environments/[environmentId]/components/header/EnvironmentHeader.tsx @@ -1,5 +1,7 @@ import Link from "next/link"; import { ChevronRight, Layers3 } from "lucide-react"; +import SensitiveConnectionValue from "@/components/SensitiveConnectionValue"; +import { canViewServerConnection } from "@/lib/api"; import type { EnvironmentSummary } from "../../types/environment-container-types"; interface EnvironmentHeaderProps { @@ -11,6 +13,7 @@ export default function EnvironmentHeader({ projectId, summary, }: EnvironmentHeaderProps) { + const isConnectionRedacted = !canViewServerConnection(); return (
{summary.environmentKind} environment on {summary.serverName} ( - {summary.serverIp}) + )

Container inventory updated {summary.updatedAt} diff --git a/src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/components/environment/EnvironmentTabPanel.tsx b/src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/components/environment/EnvironmentTabPanel.tsx index c617b72..d3e811f 100644 --- a/src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/components/environment/EnvironmentTabPanel.tsx +++ b/src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/components/environment/EnvironmentTabPanel.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useTablePagination } from "@/lib/use-table-pagination"; +import { canViewEnvironmentValues } from "@/lib/api"; import { CheckCircle2, ClipboardCheck, @@ -103,6 +104,7 @@ export default function EnvironmentTabPanel({ environment, onSaveProjectEnv, }: EnvironmentTabPanelProps) { + const isViewer = !canViewEnvironmentValues(); const [editorDraft, setEditorDraft] = useState(() => ({ source: environment.editor.content, content: environment.editor.content, @@ -160,7 +162,7 @@ export default function EnvironmentTabPanel({ [editorContent], ); const editableSource = getEditableSource(environment.editor.source); - const editorDisabled = !environment.editor.found; + const editorDisabled = isViewer || !environment.editor.found; const canSave = Boolean(environment.editor.found && environment.editor.path) && editableSource !== null; @@ -258,7 +260,11 @@ export default function EnvironmentTabPanel({ size={30} style={{ color: "var(--accent-yellow)" }} /> - Project .env file is unavailable + + {isViewer + ? "Environment variables are restricted" + : "Project .env file is unavailable"} + - {environment.editor.message || + {isViewer + ? "Viewer accounts cannot access environment variable names or values." + : environment.editor.message || "No project .env file was found in the deployment or mounted project path."}

@@ -406,13 +414,13 @@ export default function EnvironmentTabPanel({ ? "Project .env saved. Restart or rebuild the app to apply the changes." : editorStatus === "validated" ? "Draft format checked in the UI." - : editorDirty - ? "You have unsaved editor changes." - : environment.editor.found + : editorDirty + ? "You have unsaved editor changes." + : environment.editor.found ? environment.editor.source === "container" ? "Editor is loaded from container filesystem .env." : "Editor is loaded from project .env file." - : "Editor is disabled because no project .env file was found."} + : "Editor is disabled because no project .env file was found."}

) : null} + {!environment.editor.found ? ( +

+ {environment.editor.message || + "No project .env file was found. The editor is read-only until a .env file exists in the deployment or mounted project path."} +
+ ) : null} + {environment.editor.validation.map((item) => (
{app.path} .{" "} - {app.serverName} ({app.serverIp}){" "} + {app.serverName} ({ + + }){" "} .{" "} {app.environmentName}

diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index 07d5d8a..d7b2925 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import ConfirmActionDialog from "@/components/ConfirmActionDialog"; import DashboardLayout from "@/components/DashboardLayout"; +import SensitiveConnectionValue from "@/components/SensitiveConnectionValue"; import ToastViewport from "@/components/ToastViewport"; import EnvironmentFormModal from "@/app/projects/components/EnvironmentFormModal"; import EnvironmentOpsModal from "@/app/projects/components/EnvironmentOpsModal"; @@ -27,6 +28,7 @@ import { projectsApi, type Server, servers, + canViewServerConnection, } from "@/lib/api"; import { useToastManager } from "@/lib/use-toast-manager"; @@ -44,6 +46,7 @@ function formatKindLabel(kind: string) { } export default function ProjectDetailPage() { + const isConnectionRedacted = !canViewServerConnection(); const params = useParams<{ projectId: string }>(); const router = useRouter(); const projectId = params.projectId; @@ -186,7 +189,7 @@ export default function ProjectDetailPage() { environment.kind.toLowerCase().includes(query) || (environment.description ?? "").toLowerCase().includes(query) || environment.server.name.toLowerCase().includes(query) || - environment.server.ip.toLowerCase().includes(query) + environment.server.ip?.toLowerCase().includes(query) ); }); }, [project, query]); @@ -612,7 +615,14 @@ export default function ProjectDetailPage() { {meta.label === "Server IP" ? ( ) : null} - {meta.value} + {meta.label === "Server IP" ? ( + + ) : ( + meta.value + )}

))} diff --git a/src/app/servers/components/ServersTable.tsx b/src/app/servers/components/ServersTable.tsx index 0df3c8f..293bb2c 100644 --- a/src/app/servers/components/ServersTable.tsx +++ b/src/app/servers/components/ServersTable.tsx @@ -1,9 +1,11 @@ import TablePagination from "@/components/TablePagination"; +import SensitiveConnectionValue from "@/components/SensitiveConnectionValue"; import type { DockerRuntimeStatus, Server as ServerType, ServerMetric, } from "@/lib/api"; +import { canViewServerConnection } from "@/lib/api"; import { Fragment, useCallback, useState } from "react"; import { Activity, @@ -84,6 +86,7 @@ export default function ServersTable({ onEdit, onDelete, }: ServersTableProps) { + const showConnectionDetails = canViewServerConnection(); const [menuPosition, setMenuPosition] = useState<{ top: number; left: number; @@ -210,7 +213,10 @@ export default function ServersTable({ fontFamily: "monospace", }} > - {server.ip}:{server.sshPort} +

@@ -385,7 +391,7 @@ export default function ServersTable({ )} - - + } {canInstallDocker ? (