feat: enforce RBAC and harden collaboration features (#1327)

* feat: enforce RBAC and harden collaboration features

- Mount requirePermission on hosts/snippets/credentials/automations/AI routes
- Seed and backfill system role permissions on every dialect at startup
- Support personal credential overrides for RDP/VNC/Telnet shared hosts
- Broadcast participant presence in shared terminal sessions
- Make audit log forwarding configurable from the admin panel
- Add role members endpoint and snippet folder sharing

* fix: enforce RBAC across split routes
This commit is contained in:
ZacharyZcR
2026-08-24 19:47:29 +08:00
committed by GitHub
parent 69002e6416
commit f3a1087f51
45 changed files with 1262 additions and 115 deletions
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit";
import { AlertCircle, Eye } from "lucide-react";
import { AlertCircle, Eye, Users } from "lucide-react";
import {
resolveShareLink,
type ResolvedShareLink,
@@ -44,6 +44,41 @@ async function resolveTerminalWsBaseUrl(): Promise<string> {
return `${wsProtocol}://${window.location.host}${getBasePath()}/ssh/websocket/`;
}
export interface SessionParticipantInfo {
isOwner: boolean;
permissionLevel: "read-write" | "read-only";
label: string | null;
}
function ParticipantsBadge({
participants,
ownerLabel,
}: {
participants: SessionParticipantInfo[];
ownerLabel: string;
}) {
if (participants.length < 2) return null;
const names = participants
.map((participant) =>
participant.isOwner ? ownerLabel : (participant.label ?? "?"),
)
.join(", ");
return (
<div
className="absolute top-3 left-3 z-20 flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium"
style={{
backgroundColor: "var(--bg-elevated, rgba(0,0,0,0.6))",
color: "var(--foreground)",
border: "1px solid var(--border-base)",
}}
title={names}
>
<Users className="size-3.5" />
{participants.length}
</div>
);
}
function ReadOnlyBadge({ label }: { label: string }) {
return (
<div
@@ -93,6 +128,9 @@ function GuestTerminalView({
const { t } = useTranslation();
const { instance: terminal, ref: xtermRef } = useXTerm();
const [ended, setEnded] = useState<string | null>(null);
const [participants, setParticipants] = useState<SessionParticipantInfo[]>(
[],
);
const wsRef = useRef<WebSocket | null>(null);
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -140,6 +178,11 @@ function GuestTerminalView({
case "data":
if (typeof msg.data === "string") terminal.write(msg.data);
break;
case "participants":
if (Array.isArray(msg.participants)) {
setParticipants(msg.participants as SessionParticipantInfo[]);
}
break;
case "sessionExpired":
case "sessionTerminatedByOwner":
case "session_ended":
@@ -176,6 +219,10 @@ function GuestTerminalView({
return (
<div className="relative w-full h-full">
<ParticipantsBadge
participants={participants}
ownerLabel={t("sessionSharing.guestView.ownerLabel")}
/>
{share.permissionLevel === "read-only" && (
<ReadOnlyBadge label={t("sessionSharing.guestView.readOnlyBadge")} />
)}