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.
This commit is contained in:
1 parent
b12f0f9669
commit
8dac31b71f
15 files changed
+312
-37
No files matched your search
@@ -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}
|
||||
<SensitiveConnectionValue
|
||||
value={domain.value}
|
||||
isRedacted={isConnectionRedacted}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SSLBadge status={status} />
|
||||
|
||||
+7
-1
@@ -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<DashboardOverview | null>(null);
|
||||
const [serverData, setServerData] = useState<ServerInfo[]>([]);
|
||||
const [liveMetrics, setLiveMetrics] = useState<DashboardLiveMetrics | null>(
|
||||
@@ -625,7 +628,10 @@ function DashboardContent() {
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{s.ip}
|
||||
<SensitiveConnectionValue
|
||||
value={s.ip}
|
||||
isRedacted={isConnectionRedacted}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -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 (
|
||||
<section className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div
|
||||
@@ -93,7 +96,10 @@ export default function EnvironmentHeader({
|
||||
}}
|
||||
>
|
||||
{summary.environmentKind} environment on {summary.serverName} (
|
||||
{summary.serverIp})
|
||||
<SensitiveConnectionValue
|
||||
value={summary.serverIp}
|
||||
isRedacted={isConnectionRedacted}
|
||||
/>)
|
||||
</p>
|
||||
<p style={{ color: "var(--text-muted)", fontSize: 12, marginTop: 4 }}>
|
||||
Container inventory updated {summary.updatedAt}
|
||||
|
||||
+32
-7
@@ -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)" }}
|
||||
/>
|
||||
<strong>Project .env file is unavailable</strong>
|
||||
<strong>
|
||||
{isViewer
|
||||
? "Environment variables are restricted"
|
||||
: "Project .env file is unavailable"}
|
||||
</strong>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--text-muted)",
|
||||
@@ -267,7 +273,9 @@ export default function EnvironmentTabPanel({
|
||||
maxWidth: 560,
|
||||
}}
|
||||
>
|
||||
{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."}
|
||||
</span>
|
||||
</div>
|
||||
@@ -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."}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
@@ -459,6 +467,23 @@ export default function EnvironmentTabPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!environment.editor.found ? (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 7,
|
||||
background: "rgba(245,158,11,0.1)",
|
||||
color: "var(--accent-yellow)",
|
||||
padding: 11,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
{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."}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{environment.editor.validation.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
|
||||
+9
-1
@@ -1,4 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import SensitiveConnectionValue from "@/components/SensitiveConnectionValue";
|
||||
import { canViewServerConnection } from "@/lib/api";
|
||||
import { ChevronRight, Container, Loader2, MoreHorizontal } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
@@ -23,6 +25,7 @@ export default function AppDetailHeader({
|
||||
activeAction,
|
||||
onAction,
|
||||
}: AppDetailHeaderProps) {
|
||||
const isConnectionRedacted = !canViewServerConnection();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [menuPosition, setMenuPosition] = useState<{
|
||||
top: number;
|
||||
@@ -192,7 +195,12 @@ export default function AppDetailHeader({
|
||||
}}
|
||||
>
|
||||
{app.path} <span style={{ color: "var(--text-secondary)" }}>.</span>{" "}
|
||||
{app.serverName} ({app.serverIp}){" "}
|
||||
{app.serverName} ({
|
||||
<SensitiveConnectionValue
|
||||
value={app.serverIp}
|
||||
isRedacted={isConnectionRedacted}
|
||||
/>
|
||||
}){" "}
|
||||
<span style={{ color: "var(--text-secondary)" }}>.</span>{" "}
|
||||
{app.environmentName}
|
||||
</p>
|
||||
|
||||
@@ -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" ? (
|
||||
<ServerIcon size={12} />
|
||||
) : null}
|
||||
{meta.value}
|
||||
{meta.label === "Server IP" ? (
|
||||
<SensitiveConnectionValue
|
||||
value={String(meta.value ?? "")}
|
||||
isRedacted={isConnectionRedacted}
|
||||
/>
|
||||
) : (
|
||||
meta.value
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
<SensitiveConnectionValue
|
||||
value={`${server.ip}:${server.sshPort}`}
|
||||
isRedacted={!showConnectionDetails}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,7 +391,7 @@ export default function ServersTable({
|
||||
<RefreshCw size={12} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
{showConnectionDetails && <button
|
||||
title="Test SSH"
|
||||
onClick={() => void onTestConnection(server.id)}
|
||||
className="btn btn-ghost"
|
||||
@@ -397,15 +403,15 @@ export default function ServersTable({
|
||||
) : (
|
||||
<Wifi size={12} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
</button>}
|
||||
{showConnectionDetails && <button
|
||||
title="Terminal"
|
||||
onClick={() => onOpenTerminal(server)}
|
||||
className="btn btn-ghost"
|
||||
style={{ padding: "4px 8px", fontSize: 11 }}
|
||||
>
|
||||
<Terminal size={12} />
|
||||
</button>
|
||||
</button>}
|
||||
{canInstallDocker ? (
|
||||
<button
|
||||
title="Install Docker"
|
||||
|
||||
Reference in new issue
Block a user