Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,11 @@ def _target(
):
raise PermissionInvalidResourceError()

cross_tenant = record.tenant_id != actor.current_tenant_id and not actor.super_admin
read_only = record.shared_read_only or record.system_read_only
if record.tenant_id != actor.current_tenant_id and not actor.super_admin and not read_only:
if cross_tenant and not read_only:
raise PermissionInvalidResourceError()
if read_only and action is not None and action not in READ_ONLY_CHANNEL_ACTIONS:
if cross_tenant and read_only and action is not None and action not in READ_ONLY_CHANNEL_ACTIONS:
raise InvalidCatalogActionError(msg=f"Read-only channel does not support action: {action}")
return VerifiedPermissionTarget.from_business_service(
tenant_id=record.tenant_id,
Expand Down
24 changes: 24 additions & 0 deletions src/backend/test/channel/test_f048_channel_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,30 @@ async def test_shared_and_system_channels_are_read_only(channel) -> None:
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
"channel",
(
_channel(shared_read_only=True),
_channel(system_read_only=True),
),
)
async def test_read_only_marker_does_not_lock_owner_tenant(channel) -> None:
permission = _Permission()
adapter = F048ChannelPermissionAdapter(
loader=_Loader(channel),
source_service=GrantSourceService(),
permission=permission,
)

assert await adapter.check_action(
resource_id="channel-1",
actor=_actor(),
action="manage_permission",
)
assert permission.calls[0][1][2] == "manage_permission"


@pytest.mark.asyncio
async def test_wrong_tenant_and_fga_failure_fail_closed() -> None:
permission = _Permission()
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/client/src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,7 @@
"sensitive_review_blocked": "This article contains prohibited content and cannot be viewed",
"articles_count": "articles",
"at_least_one_source": "At least 1 source is required",
"back_to_channel_list": "Back to channels",
"cancel": "Cancel",
"channel_content_needs_approval": "Channel content requires approval to view",
"channel_create_success": "Channel created successfully",
Expand All @@ -1659,6 +1660,8 @@
"channel_load_failed": "Channel failed to load",
"channel_name": "Channel Name",
"channel_settings": "Channel Settings",
"channel_settings_load_failed": "Channel settings failed to load",
"channel_settings_load_failed_desc": "Please try again later, or return to the channel list and reopen it.",
"channel_updated": "Channel updated",
"delete_channel": "Delete channel",
"edit_channel": "Edit channel",
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/client/src/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,7 @@
"sensitive_review_blocked": "この記事には違反内容が含まれているため詳細を表示できません",
"articles_count": "件のコンテンツ",
"at_least_one_source": "少なくとも1つの情報源を追加してください",
"back_to_channel_list": "チャンネル一覧に戻る",
"cancel": "キャンセル",
"channel_content_needs_approval": "このチャンネルのコンテンツを表示するには承認が必要です",
"channel_create_success": "チャンネルの作成に成功しました",
Expand All @@ -1582,6 +1583,8 @@
"channel_load_failed": "チャンネルの読み込みに失敗しました",
"channel_name": "チャンネル名",
"channel_settings": "チャンネル設定",
"channel_settings_load_failed": "チャンネル設定の読み込みに失敗しました",
"channel_settings_load_failed_desc": "しばらくしてから再試行するか、チャンネル一覧に戻って開き直してください。",
"channel_updated": "チャンネルが更新されました",
"delete_channel": "チャンネルを削除",
"edit_channel": "チャンネルを編集",
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/client/src/locales/zh-Hans/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,7 @@
"sensitive_review_blocked": "该文章包含违规内容,无法查看详情",
"articles_count": "篇内容",
"at_least_one_source": "至少需添加 1 个信息源",
"back_to_channel_list": "返回频道列表",
"cancel": "取消",
"channel_content_needs_approval": "该频道内容需申请通过后方可查看",
"channel_create_success": "频道创建成功",
Expand All @@ -1586,6 +1587,8 @@
"channel_load_failed": "频道加载失败",
"channel_name": "频道名称",
"channel_settings": "频道设置",
"channel_settings_load_failed": "频道设置加载失败",
"channel_settings_load_failed_desc": "请稍后重试,或返回频道列表后重新打开。",
"channel_updated": "频道已更新",
"delete_channel": "删除频道",
"edit_channel": "编辑频道",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,11 @@ describe("F050 channel full-page UI contract", () => {
expect(source).toContain("settings.showPermissionSection");
expect(source).not.toContain("permission_ids");
});

it("uses localized copy for the load error state", () => {
expect(source).toContain("com_subscription.channel_settings_load_failed");
expect(source).toContain("com_subscription.channel_settings_load_failed_desc");
expect(source).toContain("com_subscription.back_to_channel_list");
expect(source).not.toContain("com_load_error");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,22 @@ export function ChannelSettingsPage() {
}
if (settings.loadError) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 text-body text-text-2">
<span>{settings.localize("com_load_error")}</span>
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center text-body text-text-2">
<div className="space-y-1">
<h1 className="text-h4 text-text-1">
{settings.localize("com_subscription.channel_settings_load_failed")}
</h1>
<p className="text-body-sm text-text-3">
{settings.localize("com_subscription.channel_settings_load_failed_desc")}
</p>
</div>
<Button
color="default"
variant="outlined"
size="small"
onClick={settings.cancel}
>
{settings.localize("com_unified_permission.cancel")}
{settings.localize("com_subscription.back_to_channel_list")}
</Button>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
type ResourcePermissionContext,
} from "@/controllers/API/permission"
import { AlertTriangle, Loader2 } from "lucide-react"
import { useCallback, useEffect, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { ModeHeader } from "./ModeHeader"
import { PermissionGrantTab } from "./PermissionGrantTab"
Expand Down Expand Up @@ -45,6 +45,8 @@ export function PermissionDialog({
resourceName,
}: PermissionDialogProps) {
const { t } = useTranslation("permission")
const contentRef = useRef<HTMLDivElement | null>(null)
const grantContentRef = useRef<HTMLDivElement | null>(null)
const [context, setContext] = useState<ResourcePermissionContext | null>(
null,
)
Expand Down Expand Up @@ -119,10 +121,34 @@ export function PermissionDialog({
const canAddPermission =
context?.mode === "CUSTOM" && context.can_manage_permission

const handleContentOpenAutoFocus = useCallback(
(event: Event) => {
event.preventDefault()
requestAnimationFrame(() =>
contentRef.current?.focus({ preventScroll: true }),
)
},
[],
)

const handleGrantContentOpenAutoFocus = useCallback(
(event: Event) => {
event.preventDefault()
requestAnimationFrame(() =>
grantContentRef.current?.focus({ preventScroll: true }),
)
},
[],
)

return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex h-[80vh] max-h-[800px] w-[calc(100vw-80px)] max-w-[800px] min-w-0 flex-col gap-0 overflow-hidden p-0">
<DialogContent
ref={contentRef}
onOpenAutoFocus={handleContentOpenAutoFocus}
className="flex h-[80vh] max-h-[800px] w-[calc(100vw-80px)] max-w-[800px] min-w-0 flex-col gap-0 overflow-hidden p-0"
>
<DialogHeader className="shrink-0 px-5 pb-4 pt-5">
<DialogTitle>
{t("dialog.title")} - {resourceName}
Expand Down Expand Up @@ -218,7 +244,11 @@ export function PermissionDialog({

{context && canAddPermission && (
<Dialog open={grantDialogOpen} onOpenChange={setGrantDialogOpen}>
<DialogContent className="flex h-[80vh] max-h-[800px] w-[calc(100vw-80px)] max-w-[800px] min-w-0 flex-col gap-0 overflow-hidden p-5">
<DialogContent
ref={grantContentRef}
onOpenAutoFocus={handleGrantContentOpenAutoFocus}
className="flex h-[80vh] max-h-[800px] w-[calc(100vw-80px)] max-w-[800px] min-w-0 flex-col gap-0 overflow-hidden p-5"
>
<DialogHeader className="shrink-0">
<DialogTitle>
{t("dialog.tabGrant")} - {resourceName}
Expand Down
18 changes: 18 additions & 0 deletions src/frontend/platform/src/test/f048PermissionDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ describe("F048 PermissionDialog", () => {
).toBeInTheDocument()
})

it("does not focus the close button when opened", async () => {
render(
<PermissionDialog
open
onOpenChange={vi.fn()}
resourceType="knowledge_file"
resourceId="file-1"
resourceName="Report"
/>,
)

expect(await screen.findByText("mode.custom")).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByRole("dialog")).toHaveFocus()
})
expect(screen.getByRole("button", { name: "Close" })).not.toHaveFocus()
})

it("does not add a permission mode control for a resource without a parent", async () => {
vi.mocked(getResourcePermissionContextApi).mockResolvedValue({
...customContext,
Expand Down
Loading