enh: channel read/write perm

This commit is contained in:
Timothy Jaeryang Baek 2025-09-24 10:09:59 -05:00
parent 6d69ea3ac7
commit ac879513e5
8 changed files with 58 additions and 14 deletions

View file

@ -57,6 +57,10 @@ class ChannelModel(BaseModel):
#################### ####################
class ChannelResponse(ChannelModel):
write_access: bool = False
class ChannelForm(BaseModel): class ChannelForm(BaseModel):
name: str name: str
description: Optional[str] = None description: Optional[str] = None

View file

@ -10,7 +10,13 @@ from pydantic import BaseModel
from open_webui.socket.main import sio, get_user_ids_from_room from open_webui.socket.main import sio, get_user_ids_from_room
from open_webui.models.users import Users, UserNameResponse from open_webui.models.users import Users, UserNameResponse
from open_webui.models.channels import Channels, ChannelModel, ChannelForm from open_webui.models.groups import Groups
from open_webui.models.channels import (
Channels,
ChannelModel,
ChannelForm,
ChannelResponse,
)
from open_webui.models.messages import ( from open_webui.models.messages import (
Messages, Messages,
MessageModel, MessageModel,
@ -80,7 +86,7 @@ async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user
############################ ############################
@router.get("/{id}", response_model=Optional[ChannelModel]) @router.get("/{id}", response_model=Optional[ChannelResponse])
async def get_channel_by_id(id: str, user=Depends(get_verified_user)): async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
channel = Channels.get_channel_by_id(id) channel = Channels.get_channel_by_id(id)
if not channel: if not channel:
@ -95,7 +101,16 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
) )
return ChannelModel(**channel.model_dump()) write_access = has_access(
user.id, type="write", access_control=channel.access_control, strict=False
)
return ChannelResponse(
**{
**channel.model_dump(),
"write_access": write_access or user.role == "admin",
}
)
############################ ############################
@ -362,7 +377,7 @@ async def new_message_handler(
) )
if user.role != "admin" and not has_access( if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control user.id, type="write", access_control=channel.access_control, strict=False
): ):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
@ -658,7 +673,7 @@ async def add_reaction_to_message(
) )
if user.role != "admin" and not has_access( if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control user.id, type="write", access_control=channel.access_control, strict=False
): ):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
@ -724,7 +739,7 @@ async def remove_reaction_by_id_and_user_id_and_name(
) )
if user.role != "admin" and not has_access( if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control user.id, type="write", access_control=channel.access_control, strict=False
): ):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
@ -806,7 +821,9 @@ async def delete_message_by_id(
if ( if (
user.role != "admin" user.role != "admin"
and message.user_id != user.id and message.user_id != user.id
and not has_access(user.id, type="read", access_control=channel.access_control) and not has_access(
user.id, type="write", access_control=channel.access_control, strict=False
)
): ):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()

View file

@ -110,9 +110,13 @@ def has_access(
type: str = "write", type: str = "write",
access_control: Optional[dict] = None, access_control: Optional[dict] = None,
user_group_ids: Optional[Set[str]] = None, user_group_ids: Optional[Set[str]] = None,
strict: bool = True,
) -> bool: ) -> bool:
if access_control is None: if access_control is None:
return type == "read" if strict:
return type == "read"
else:
return True
if user_group_ids is None: if user_group_ids is None:
user_groups = Groups.get_groups_by_member_id(user_id) user_groups = Groups.get_groups_by_member_id(user_id)

View file

@ -14,6 +14,7 @@
import Drawer from '../common/Drawer.svelte'; import Drawer from '../common/Drawer.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte'; import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import Thread from './Thread.svelte'; import Thread from './Thread.svelte';
import i18n from '$lib/i18n';
export let id = ''; export let id = '';
@ -252,6 +253,10 @@
{typingUsers} {typingUsers}
userSuggestions={true} userSuggestions={true}
channelSuggestions={true} channelSuggestions={true}
disabled={!channel?.write_access}
placeholder={!channel?.write_access
? $i18n.t('You do not have permission to send messages in this channel.')
: $i18n.t('Type here...')}
{onChange} {onChange}
onSubmit={submitHandler} onSubmit={submitHandler}
{scrollToBottom} {scrollToBottom}

View file

@ -38,7 +38,7 @@
import MentionList from './MessageInput/MentionList.svelte'; import MentionList from './MessageInput/MentionList.svelte';
import Skeleton from '../chat/Messages/Skeleton.svelte'; import Skeleton from '../chat/Messages/Skeleton.svelte';
export let placeholder = $i18n.t('Send a Message'); export let placeholder = $i18n.t('Type here...');
export let id = null; export let id = null;
export let chatInputElement; export let chatInputElement;
@ -53,6 +53,7 @@
export let scrollEnd = true; export let scrollEnd = true;
export let scrollToBottom: Function = () => {}; export let scrollToBottom: Function = () => {};
export let disabled = false;
export let acceptFiles = true; export let acceptFiles = true;
export let showFormattingToolbar = true; export let showFormattingToolbar = true;
@ -731,7 +732,9 @@
</div> </div>
</div> </div>
<div class=""> <div
class="{disabled ? 'opacity-50 pointer-events-none cursor-not-allowed' : ''} relative z-20"
>
{#if recording} {#if recording}
<VoiceRecording <VoiceRecording
bind:recording bind:recording
@ -836,6 +839,8 @@
bind:this={chatInputElement} bind:this={chatInputElement}
json={true} json={true}
messageInput={true} messageInput={true}
editable={!disabled}
{placeholder}
richText={$settings?.richTextInput ?? true} richText={$settings?.richTextInput ?? true}
showFormattingToolbar={$settings?.showFormattingToolbar ?? false} showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) && shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&

View file

@ -201,6 +201,10 @@
<div class=" pb-[1rem] px-2.5 w-full"> <div class=" pb-[1rem] px-2.5 w-full">
<MessageInput <MessageInput
id={threadId} id={threadId}
disabled={!channel?.write_access}
placeholder={!channel?.write_access
? $i18n.t('You do not have permission to send messages in this thread.')
: $i18n.t('Reply to thread...')}
typingUsersClassName="from-gray-50 dark:from-gray-850" typingUsersClassName="from-gray-50 dark:from-gray-850"
{typingUsers} {typingUsers}
userSuggestions={true} userSuggestions={true}

View file

@ -168,7 +168,7 @@
export let documentId = ''; export let documentId = '';
export let className = 'input-prose'; export let className = 'input-prose';
export let placeholder = 'Type here...'; export let placeholder = $i18n.t('Type here...');
let _placeholder = placeholder; let _placeholder = placeholder;
$: if (placeholder !== _placeholder) { $: if (placeholder !== _placeholder) {
@ -689,7 +689,7 @@
link: link link: link
}), }),
...(dragHandle ? [ListItemDragHandle] : []), ...(dragHandle ? [ListItemDragHandle] : []),
Placeholder.configure({ placeholder: () => _placeholder }), Placeholder.configure({ placeholder: () => _placeholder, showOnlyWhenEditable: false }),
SelectionDecoration, SelectionDecoration,
...(richText ...(richText
@ -1123,4 +1123,9 @@
</div> </div>
{/if} {/if}
<div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}" /> <div
bind:this={element}
class="relative w-full min-w-full h-full min-h-fit {className} {!editable
? 'cursor-not-allowed'
: ''}"
/>

View file

@ -126,7 +126,7 @@
<div class="my-2 -mx-2"> <div class="my-2 -mx-2">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl"> <div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
<AccessControl bind:accessControl /> <AccessControl bind:accessControl accessRoles={['read', 'write']} />
</div> </div>
</div> </div>