fix: enforce public sharing permission checks across all resource types (#21358)
The sharePublic prop in editor components (Knowledge, Tools, Skills, Prompts, Models) incorrectly included an "|| edit" / "|| write_access" condition, allowing users with write access to see and use the "Public" sharing option regardless of their actual public sharing permission. Additionally, all backend access/update endpoints only verified write authorization but did not check the corresponding sharing.public_* permission, allowing direct API calls to bypass frontend restrictions entirely. Frontend: removed the edit/write_access bypass from sharePublic in all five editor components so visibility is gated solely by the user's sharing.public_* permission or admin role. Backend: added has_public_read_access_grant checks to the access/update endpoints in knowledge.py, tools.py, prompts.py, skills.py, models.py, and notes.py. Public grants are silently stripped when the user lacks the corresponding permission. Fixes #21356
This commit is contained in:
@@ -511,6 +511,7 @@ class KnowledgeAccessGrantsForm(BaseModel):
|
|||||||
|
|
||||||
@router.post("/{id}/access/update", response_model=Optional[KnowledgeFilesResponse])
|
@router.post("/{id}/access/update", response_model=Optional[KnowledgeFilesResponse])
|
||||||
async def update_knowledge_access_by_id(
|
async def update_knowledge_access_by_id(
|
||||||
|
request: Request,
|
||||||
id: str,
|
id: str,
|
||||||
form_data: KnowledgeAccessGrantsForm,
|
form_data: KnowledgeAccessGrantsForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
@@ -539,6 +540,24 @@ async def update_knowledge_access_by_id(
|
|||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public sharing if user lacks permission
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and has_public_read_access_grant(form_data.access_grants)
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_knowledge",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_grants = [
|
||||||
|
g for g in form_data.access_grants
|
||||||
|
if not (
|
||||||
|
g.get("principal_type") == "user"
|
||||||
|
and g.get("principal_id") == "*"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db)
|
AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db)
|
||||||
|
|
||||||
return KnowledgeFilesResponse(
|
return KnowledgeFilesResponse(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from open_webui.models.models import (
|
|||||||
ModelAccessResponse,
|
ModelAccessResponse,
|
||||||
Models,
|
Models,
|
||||||
)
|
)
|
||||||
from open_webui.models.access_grants import AccessGrants
|
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
@@ -506,6 +506,7 @@ class ModelAccessGrantsForm(BaseModel):
|
|||||||
|
|
||||||
@router.post("/model/access/update", response_model=Optional[ModelModel])
|
@router.post("/model/access/update", response_model=Optional[ModelModel])
|
||||||
async def update_model_access_by_id(
|
async def update_model_access_by_id(
|
||||||
|
request: Request,
|
||||||
form_data: ModelAccessGrantsForm,
|
form_data: ModelAccessGrantsForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: Session = Depends(get_session),
|
db: Session = Depends(get_session),
|
||||||
@@ -533,6 +534,24 @@ async def update_model_access_by_id(
|
|||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public sharing if user lacks permission
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and has_public_read_access_grant(form_data.access_grants)
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_models",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_grants = [
|
||||||
|
g for g in form_data.access_grants
|
||||||
|
if not (
|
||||||
|
g.get("principal_type") == "user"
|
||||||
|
and g.get("principal_id") == "*"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
AccessGrants.set_access_grants(
|
AccessGrants.set_access_grants(
|
||||||
"model", form_data.id, form_data.access_grants, db=db
|
"model", form_data.id, form_data.access_grants, db=db
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -345,6 +345,24 @@ async def update_note_access_by_id(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public sharing if user lacks permission
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and has_public_read_access_grant(form_data.access_grants)
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_notes",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_grants = [
|
||||||
|
g for g in form_data.access_grants
|
||||||
|
if not (
|
||||||
|
g.get("principal_type") == "user"
|
||||||
|
and g.get("principal_id") == "*"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db)
|
AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db)
|
||||||
|
|
||||||
return Notes.get_note_by_id(id, db=db)
|
return Notes.get_note_by_id(id, db=db)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from open_webui.models.prompts import (
|
|||||||
PromptModel,
|
PromptModel,
|
||||||
Prompts,
|
Prompts,
|
||||||
)
|
)
|
||||||
from open_webui.models.access_grants import AccessGrants
|
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.prompt_history import (
|
from open_webui.models.prompt_history import (
|
||||||
PromptHistories,
|
PromptHistories,
|
||||||
@@ -436,6 +436,7 @@ class PromptAccessGrantsForm(BaseModel):
|
|||||||
|
|
||||||
@router.post("/id/{prompt_id}/access/update", response_model=Optional[PromptModel])
|
@router.post("/id/{prompt_id}/access/update", response_model=Optional[PromptModel])
|
||||||
async def update_prompt_access_by_id(
|
async def update_prompt_access_by_id(
|
||||||
|
request: Request,
|
||||||
prompt_id: str,
|
prompt_id: str,
|
||||||
form_data: PromptAccessGrantsForm,
|
form_data: PromptAccessGrantsForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
@@ -464,6 +465,24 @@ async def update_prompt_access_by_id(
|
|||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public sharing if user lacks permission
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and has_public_read_access_grant(form_data.access_grants)
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_prompts",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_grants = [
|
||||||
|
g for g in form_data.access_grants
|
||||||
|
if not (
|
||||||
|
g.get("principal_type") == "user"
|
||||||
|
and g.get("principal_id") == "*"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db)
|
AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db)
|
||||||
|
|
||||||
return Prompts.get_prompt_by_id(prompt_id, db=db)
|
return Prompts.get_prompt_by_id(prompt_id, db=db)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from open_webui.models.skills import (
|
|||||||
SkillAccessListResponse,
|
SkillAccessListResponse,
|
||||||
Skills,
|
Skills,
|
||||||
)
|
)
|
||||||
from open_webui.models.access_grants import AccessGrants
|
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.access_control import has_access, has_permission
|
from open_webui.utils.access_control import has_access, has_permission
|
||||||
|
|
||||||
@@ -312,6 +312,7 @@ class SkillAccessGrantsForm(BaseModel):
|
|||||||
|
|
||||||
@router.post("/id/{id}/access/update", response_model=Optional[SkillModel])
|
@router.post("/id/{id}/access/update", response_model=Optional[SkillModel])
|
||||||
async def update_skill_access_by_id(
|
async def update_skill_access_by_id(
|
||||||
|
request: Request,
|
||||||
id: str,
|
id: str,
|
||||||
form_data: SkillAccessGrantsForm,
|
form_data: SkillAccessGrantsForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
@@ -340,6 +341,24 @@ async def update_skill_access_by_id(
|
|||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public sharing if user lacks permission
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and has_public_read_access_grant(form_data.access_grants)
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_skills",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_grants = [
|
||||||
|
g for g in form_data.access_grants
|
||||||
|
if not (
|
||||||
|
g.get("principal_type") == "user"
|
||||||
|
and g.get("principal_id") == "*"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db)
|
AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db)
|
||||||
|
|
||||||
return Skills.get_skill_by_id(id, db=db)
|
return Skills.get_skill_by_id(id, db=db)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from open_webui.models.tools import (
|
|||||||
ToolAccessResponse,
|
ToolAccessResponse,
|
||||||
Tools,
|
Tools,
|
||||||
)
|
)
|
||||||
from open_webui.models.access_grants import AccessGrants
|
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||||
from open_webui.utils.plugin import (
|
from open_webui.utils.plugin import (
|
||||||
load_tool_module_by_id,
|
load_tool_module_by_id,
|
||||||
replace_imports,
|
replace_imports,
|
||||||
@@ -526,6 +526,7 @@ class ToolAccessGrantsForm(BaseModel):
|
|||||||
|
|
||||||
@router.post("/id/{id}/access/update", response_model=Optional[ToolModel])
|
@router.post("/id/{id}/access/update", response_model=Optional[ToolModel])
|
||||||
async def update_tool_access_by_id(
|
async def update_tool_access_by_id(
|
||||||
|
request: Request,
|
||||||
id: str,
|
id: str,
|
||||||
form_data: ToolAccessGrantsForm,
|
form_data: ToolAccessGrantsForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
@@ -554,6 +555,24 @@ async def update_tool_access_by_id(
|
|||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public sharing if user lacks permission
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and has_public_read_access_grant(form_data.access_grants)
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_tools",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_grants = [
|
||||||
|
g for g in form_data.access_grants
|
||||||
|
if not (
|
||||||
|
g.get("principal_type") == "user"
|
||||||
|
and g.get("principal_id") == "*"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db)
|
AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db)
|
||||||
|
|
||||||
return Tools.get_tool_by_id(id, db=db)
|
return Tools.get_tool_by_id(id, db=db)
|
||||||
|
|||||||
@@ -837,8 +837,7 @@
|
|||||||
bind:accessGrants={knowledge.access_grants}
|
bind:accessGrants={knowledge.access_grants}
|
||||||
share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'}
|
share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'}
|
||||||
sharePublic={$user?.permissions?.sharing?.public_knowledge ||
|
sharePublic={$user?.permissions?.sharing?.public_knowledge ||
|
||||||
$user?.role === 'admin' ||
|
$user?.role === 'admin'}
|
||||||
knowledge?.write_access}
|
|
||||||
onChange={async () => {
|
onChange={async () => {
|
||||||
try {
|
try {
|
||||||
await updateKnowledgeAccessGrants(localStorage.token, id, knowledge.access_grants ?? []);
|
await updateKnowledgeAccessGrants(localStorage.token, id, knowledge.access_grants ?? []);
|
||||||
|
|||||||
@@ -342,7 +342,7 @@
|
|||||||
bind:accessGrants
|
bind:accessGrants
|
||||||
accessRoles={preset ? ['read', 'write'] : ['read']}
|
accessRoles={preset ? ['read', 'write'] : ['read']}
|
||||||
share={$user?.permissions?.sharing?.models || $user?.role === 'admin'}
|
share={$user?.permissions?.sharing?.models || $user?.role === 'admin'}
|
||||||
sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin' || edit}
|
sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'}
|
||||||
onChange={async () => {
|
onChange={async () => {
|
||||||
if (edit && model?.id) {
|
if (edit && model?.id) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
bind:accessGrants
|
bind:accessGrants
|
||||||
accessRoles={['read', 'write']}
|
accessRoles={['read', 'write']}
|
||||||
share={$user?.permissions?.sharing?.prompts || $user?.role === 'admin'}
|
share={$user?.permissions?.sharing?.prompts || $user?.role === 'admin'}
|
||||||
sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin' || edit}
|
sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'}
|
||||||
onChange={async () => {
|
onChange={async () => {
|
||||||
if (edit && prompt?.id) {
|
if (edit && prompt?.id) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
bind:accessGrants
|
bind:accessGrants
|
||||||
accessRoles={['read', 'write']}
|
accessRoles={['read', 'write']}
|
||||||
share={$user?.permissions?.sharing?.skills || $user?.role === 'admin'}
|
share={$user?.permissions?.sharing?.skills || $user?.role === 'admin'}
|
||||||
sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin' || edit}
|
sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin'}
|
||||||
onChange={async () => {
|
onChange={async () => {
|
||||||
if (edit && skill?.id) {
|
if (edit && skill?.id) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class Tools:
|
|||||||
bind:accessGrants
|
bind:accessGrants
|
||||||
accessRoles={['read', 'write']}
|
accessRoles={['read', 'write']}
|
||||||
share={$user?.permissions?.sharing?.tools || $user?.role === 'admin'}
|
share={$user?.permissions?.sharing?.tools || $user?.role === 'admin'}
|
||||||
sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin' || edit}
|
sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin'}
|
||||||
onChange={async () => {
|
onChange={async () => {
|
||||||
if (edit && id) {
|
if (edit && id) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user