[OND211-2329]: Updated permission on dataset and canvas to share to a specific team.

This commit is contained in:
Hetavi Shah 2025-11-21 16:58:00 +05:30
parent 4c3db69e91
commit 60a6265b86
7 changed files with 176 additions and 28 deletions

View file

@ -18,6 +18,7 @@ import logging
import re
import sys
from functools import partial
from typing import Dict, Any, Optional
import trio
from quart import request, Response, make_response
from agent.component import LLM
@ -44,7 +45,8 @@ from rag.nlp import search
from rag.utils.redis_conn import REDIS_CONN
from common import settings
from api.apps import login_required, current_user
from api.db.services.user_service import UserTenantService
from common.constants import StatusEnum
@manager.route('/templates', methods=['GET']) # noqa: F821
@login_required
@ -69,11 +71,31 @@ async def rm():
@manager.route('/set', methods=['POST']) # noqa: F821
@validate_request("dsl", "title")
@login_required
async def save():
req = await request_json()
async def save() -> Any:
req: Dict[str, Any] = await request_json()
if not isinstance(req["dsl"], str):
req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False)
req["dsl"] = json.loads(req["dsl"])
# Validate shared_tenant_id if provided
shared_tenant_id: Optional[str] = req.get("shared_tenant_id")
if shared_tenant_id:
if req.get("permission") != "team":
return get_json_result(
data=False,
message="shared_tenant_id can only be set when permission is 'team'",
code=RetCode.ARGUMENT_ERROR
)
# Verify user is a member of the shared tenant
user_tenant = UserTenantService.filter_by_tenant_and_user_id(shared_tenant_id, current_user.id)
if not user_tenant or user_tenant.status != StatusEnum.VALID.value:
return get_json_result(
data=False,
message=f"You are not a member of the selected team",
code=RetCode.PERMISSION_ERROR
)
cate = req.get("canvas_category", CanvasCategory.Agent)
if "id" not in req:
req["user_id"] = current_user.id

View file

@ -17,6 +17,7 @@ import json
import logging
import random
import re
from typing import Dict, Any, Optional
from quart import request
import numpy as np
@ -42,13 +43,35 @@ from rag.utils.doc_store_conn import OrderByExpr
from common.constants import RetCode, PipelineTaskType, StatusEnum, VALID_TASK_STATUS, FileSource, LLMType, PAGERANK_FLD
from common import settings
from api.apps import login_required, current_user
from common.constants import StatusEnum
@manager.route('/create', methods=['post']) # noqa: F821
@login_required
@validate_request("name")
async def create():
req = await request_json()
async def create() -> Any:
req: Dict[str, Any] = await request_json()
# Validate shared_tenant_id if provided
shared_tenant_id: Optional[str] = req.get("shared_tenant_id")
if shared_tenant_id:
if req.get("permission") != "team":
return get_json_result(
data=False,
message="shared_tenant_id can only be set when permission is 'team'",
code=RetCode.ARGUMENT_ERROR
)
# Verify user is a member of the shared tenant
user_tenant = UserTenantService.filter_by_tenant_and_user_id(shared_tenant_id, current_user.id)
if not user_tenant or user_tenant.status != StatusEnum.VALID.value:
return get_json_result(
data=False,
message=f"You are not a member of the selected team",
code=RetCode.PERMISSION_ERROR
)
e: bool
res: Any
e, res = KnowledgebaseService.create_with_name(
name = req.pop("name", None),
tenant_id = current_user.id,

View file

@ -18,6 +18,7 @@
import logging
import os
import json
from typing import Dict, Any, Optional
from quart import request
from peewee import OperationalError
from api.db.db_models import File
@ -50,11 +51,12 @@ from api.utils.validation_utils import (
from rag.nlp import search
from common.constants import PAGERANK_FLD
from common import settings
from api.db.services.user_service import UserTenantService
from common.constants import StatusEnum
@manager.route("/datasets", methods=["POST"]) # noqa: F821
@token_required
async def create(tenant_id):
async def create(tenant_id: str) -> Any:
"""
Create a new dataset.
---
@ -116,9 +118,24 @@ async def create(tenant_id):
# | embedding_model| embd_id |
# | chunk_method | parser_id |
req: Dict[str, Any]
err: Optional[Any]
req, err = await validate_and_parse_json_request(request, CreateDatasetReq)
if err is not None:
return get_error_argument_result(err)
# Validate shared_tenant_id if provided
shared_tenant_id: Optional[str] = req.get("shared_tenant_id")
if shared_tenant_id:
if req.get("permission") != "team":
return get_error_argument_result("shared_tenant_id can only be set when permission is 'team'")
# Verify user is a member of the shared tenant
user_tenant = UserTenantService.filter_by_tenant_and_user_id(shared_tenant_id, tenant_id)
if not user_tenant or user_tenant.status != StatusEnum.VALID.value:
return get_error_permission_result(message=f"User is not a member of tenant '{shared_tenant_id}'")
e: bool
e, req = KnowledgebaseService.create_with_name(
name = req.pop("name", None),
tenant_id = tenant_id,
@ -130,6 +147,8 @@ async def create(tenant_id):
return req
# Insert embedding model(embd id)
ok: bool
t: Any
ok, t = TenantService.get_by_id(tenant_id)
if not ok:
return get_error_permission_result(message="Tenant not found")

View file

@ -14,43 +14,57 @@
# limitations under the License.
#
from typing import Dict, Any, Optional, List
from api.db import TenantPermission
from api.db.db_models import File, Knowledgebase
from api.db.services.file_service import FileService
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.user_service import TenantService
from api.db.services.user_service import TenantService, UserTenantService
from common.constants import StatusEnum
def check_kb_team_permission(kb: dict | Knowledgebase, other: str) -> bool:
def check_kb_team_permission(kb: Dict[str, Any] | Knowledgebase, other: str) -> bool:
kb = kb.to_dict() if isinstance(kb, Knowledgebase) else kb
kb_tenant_id = kb["tenant_id"]
# If user owns the tenant where the KB was created, always allow
if kb_tenant_id == other:
return True
# If permission is not "team", deny access
if kb["permission"] != TenantPermission.TEAM:
return False
joined_tenants = TenantService.get_joined_tenants_by_user_id(other)
# If shared_tenant_id is specified, check if user is a member of that specific tenant
shared_tenant_id: Optional[str] = kb.get("shared_tenant_id")
if shared_tenant_id:
# Check if user is a member of the shared tenant
user_tenant = UserTenantService.filter_by_tenant_and_user_id(shared_tenant_id, other)
return user_tenant is not None and user_tenant.status == StatusEnum.VALID.value
# Legacy behavior: if no shared_tenant_id, check if user is a member of the KB's tenant
joined_tenants: List[Dict[str, Any]] = TenantService.get_joined_tenants_by_user_id(other)
return any(tenant["tenant_id"] == kb_tenant_id for tenant in joined_tenants)
def check_file_team_permission(file: dict | File, other: str) -> bool:
def check_file_team_permission(file: Dict[str, Any] | File, other: str) -> bool:
file = file.to_dict() if isinstance(file, File) else file
file_tenant_id = file["tenant_id"]
if file_tenant_id == other:
return True
file_id = file["id"]
file_id: str = file["id"]
kb_ids = [kb_info["kb_id"] for kb_info in FileService.get_kb_id_by_file_id(file_id)]
kb_ids: List[str] = [kb_info["kb_id"] for kb_info in FileService.get_kb_id_by_file_id(file_id)]
for kb_id in kb_ids:
ok: bool
kb: Optional[Knowledgebase]
ok, kb = KnowledgebaseService.get_by_id(kb_id)
if not ok:
if not ok or kb is None:
continue
if check_kb_team_permission(kb, other):

View file

@ -740,6 +740,7 @@ class Knowledgebase(DataBaseModel):
description = TextField(null=True, help_text="KB description")
embd_id = CharField(max_length=128, null=False, help_text="default embedding model ID", index=True)
permission = CharField(max_length=16, null=False, help_text="me|team", default="me", index=True)
shared_tenant_id = CharField(max_length=32, null=True, help_text="Specific tenant ID to share with when permission is 'team'", index=True)
created_by = CharField(max_length=32, null=False, index=True)
doc_num = IntegerField(default=0, index=True)
token_num = IntegerField(default=0, index=True)
@ -923,6 +924,7 @@ class UserCanvas(DataBaseModel):
title = CharField(max_length=255, null=True, help_text="Canvas title")
permission = CharField(max_length=16, null=False, help_text="me|team", default="me", index=True)
shared_tenant_id = CharField(max_length=32, null=True, help_text="Specific tenant ID to share with when permission is 'team'", index=True)
description = TextField(null=True, help_text="Canvas description")
canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True)
canvas_category = CharField(max_length=32, null=False, default="agent_canvas", help_text="Canvas category: agent_canvas|dataflow_canvas", index=True)
@ -1201,6 +1203,14 @@ def migrate_db():
migrate(migrator.add_column("user_canvas", "permission", CharField(max_length=16, null=False, help_text="me|team", default="me", index=True)))
except Exception:
pass
try:
migrate(migrator.add_column("knowledgebase", "shared_tenant_id", CharField(max_length=32, null=True, help_text="Specific tenant ID to share with when permission is 'team'", index=True)))
except Exception:
pass
try:
migrate(migrator.add_column("user_canvas", "shared_tenant_id", CharField(max_length=32, null=True, help_text="Specific tenant ID to share with when permission is 'team'", index=True)))
except Exception:
pass
try:
migrate(migrator.add_column("llm", "is_tools", BooleanField(null=False, help_text="support tools", default=False)))
except Exception:

View file

@ -16,6 +16,7 @@
import json
import logging
import time
from typing import List, Dict, Any, Optional, Tuple
from uuid import uuid4
from agent.canvas import Canvas
from api.db import CanvasCategory, TenantPermission
@ -26,7 +27,11 @@ from common.misc_utils import get_uuid
from api.utils.api_utils import get_data_openai
import tiktoken
from peewee import fn
from api.db.services.user_service import UserTenantService
from common.constants import StatusEnum
from api.db.services.user_service import UserTenantService
from api.db import TenantPermission
from common.constants import StatusEnum
class CanvasTemplateService(CommonService):
model = CanvasTemplate
@ -95,7 +100,7 @@ class UserCanvasService(CommonService):
@classmethod
@DB.connection_context()
def get_by_canvas_id(cls, pid):
def get_by_canvas_id(cls, pid: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
try:
fields = [
@ -105,6 +110,7 @@ class UserCanvasService(CommonService):
cls.model.dsl,
cls.model.description,
cls.model.permission,
cls.model.shared_tenant_id,
cls.model.update_time,
cls.model.user_id,
cls.model.create_time,
@ -125,10 +131,17 @@ class UserCanvasService(CommonService):
@classmethod
@DB.connection_context()
def get_by_tenant_ids(cls, joined_tenant_ids, user_id,
page_number, items_per_page,
orderby, desc, keywords, canvas_category=None
):
def get_by_tenant_ids(
cls,
joined_tenant_ids: List[str],
user_id: str,
page_number: Optional[int],
items_per_page: Optional[int],
orderby: str,
desc: bool,
keywords: Optional[str],
canvas_category: Optional[str] = None
) -> Tuple[List[Dict[str, Any]], int]:
fields = [
cls.model.id,
cls.model.avatar,
@ -136,20 +149,41 @@ class UserCanvasService(CommonService):
cls.model.dsl,
cls.model.description,
cls.model.permission,
cls.model.shared_tenant_id,
cls.model.user_id.alias("tenant_id"),
User.nickname,
User.avatar.alias('tenant_avatar'),
cls.model.update_time,
cls.model.canvas_category,
]
# Build permission conditions: user's own canvases OR team canvases they have access to
# For team canvases: check if shared_tenant_id matches user's tenant membership, or legacy behavior (user_id in joined_tenants)
# Get all tenant IDs where user is a member (for checking shared_tenant_id)
user_tenant_relations: List[Any] = UserTenantService.query(user_id=user_id, status=StatusEnum.VALID.value)
user_tenant_ids: List[str] = [str(ut.tenant_id) for ut in user_tenant_relations]
# Condition: user's own canvases OR (team permission AND (shared_tenant_id in user's tenants OR legacy: user_id in joined_tenant_ids))
permission_condition = (
(cls.model.user_id == user_id) |
(
(cls.model.permission == TenantPermission.TEAM.value) &
(
(cls.model.shared_tenant_id.in_(user_tenant_ids)) |
((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.shared_tenant_id.is_null()))
)
)
)
if keywords:
agents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
(((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.permission == TenantPermission.TEAM.value)) | (cls.model.user_id == user_id)),
permission_condition,
(fn.LOWER(cls.model.title).contains(keywords.lower()))
)
else:
agents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
(((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.permission == TenantPermission.TEAM.value)) | (cls.model.user_id == user_id))
permission_condition
)
if canvas_category:
agents = agents.where(cls.model.canvas_category == canvas_category)
@ -165,16 +199,32 @@ class UserCanvasService(CommonService):
@classmethod
@DB.connection_context()
def accessible(cls, canvas_id, tenant_id):
from api.db.services.user_service import UserTenantService
def accessible(cls, canvas_id: str, tenant_id: str) -> bool:
e: bool
c: Optional[Dict[str, Any]]
e, c = UserCanvasService.get_by_canvas_id(canvas_id)
if not e:
if not e or c is None:
return False
tids = [t.tenant_id for t in UserTenantService.query(user_id=tenant_id)]
if c["user_id"] != canvas_id and c["user_id"] not in tids:
# If user owns the canvas, always allow
if c["user_id"] == tenant_id:
return True
# If permission is not "team", deny access
if c.get("permission") != TenantPermission.TEAM.value:
return False
return True
# If shared_tenant_id is specified, check if user is a member of that specific tenant
shared_tenant_id: Optional[str] = c.get("shared_tenant_id")
if shared_tenant_id:
user_tenant = UserTenantService.filter_by_tenant_and_user_id(shared_tenant_id, tenant_id)
return user_tenant is not None and user_tenant.status == StatusEnum.VALID.value
# Legacy behavior: check if user is a member of the canvas owner's tenant
tids: List[str] = [str(t.tenant_id) for t in UserTenantService.query(user_id=tenant_id, status=StatusEnum.VALID.value)]
return str(c["user_id"]) in tids
async def completion(tenant_id, agent_id, session_id=None, **kwargs):

View file

@ -361,11 +361,21 @@ class CreateDatasetReq(Base):
description: Annotated[str | None, Field(default=None, max_length=65535)]
embedding_model: Annotated[str | None, Field(default=None, max_length=255, serialization_alias="embd_id")]
permission: Annotated[Literal["me", "team"], Field(default="me", min_length=1, max_length=16)]
shared_tenant_id: Annotated[str | None, Field(default=None, max_length=32, description="Specific tenant ID to share with when permission is 'team'")]
chunk_method: Annotated[
Literal["naive", "book", "email", "laws", "manual", "one", "paper", "picture", "presentation", "qa", "table", "tag"],
Field(default="naive", min_length=1, max_length=32, serialization_alias="parser_id"),
]
parser_config: Annotated[ParserConfig | None, Field(default=None)]
@field_validator("shared_tenant_id", mode="after")
@classmethod
def validate_shared_tenant_id(cls, v: str | None, info: Any) -> str | None:
"""Validate that shared_tenant_id is only set when permission is 'team'."""
permission: str = info.data.get("permission", "me")
if v is not None and permission != "team":
raise ValueError("shared_tenant_id can only be set when permission is 'team'")
return v
@field_validator("avatar", mode="after")
@classmethod