[OND211-2329]: Added permissions for team users to CRUD on datasets and agents.
This commit is contained in:
parent
60a6265b86
commit
89d27c75b6
10 changed files with 625 additions and 109 deletions
|
|
@ -30,6 +30,7 @@ from api.db.services.pipeline_operation_log_service import PipelineOperationLogS
|
|||
from api.db.services.task_service import queue_dataflow, CANVAS_DEBUG_DOC_ID, TaskService
|
||||
from api.db.services.user_service import TenantService
|
||||
from api.db.services.user_canvas_version import UserCanvasVersionService
|
||||
from api.common.permission_utils import has_permission
|
||||
from common.constants import RetCode
|
||||
from common.misc_utils import get_uuid
|
||||
from api.utils.api_utils import get_json_result, server_error_response, validate_request, get_data_error_result, \
|
||||
|
|
@ -60,10 +61,13 @@ def templates():
|
|||
async def rm():
|
||||
req = await request_json()
|
||||
for i in req["canvas_ids"]:
|
||||
if not UserCanvasService.accessible(i, current_user.id):
|
||||
# Check delete permission
|
||||
if not UserCanvasService.accessible(i, current_user.id, required_permission="delete"):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
data=False,
|
||||
message='You do not have delete permission for this canvas.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
UserCanvasService.delete_by_id(i)
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
|
@ -98,6 +102,17 @@ async def save() -> Any:
|
|||
|
||||
cate = req.get("canvas_category", CanvasCategory.Agent)
|
||||
if "id" not in req:
|
||||
# Check create permission if sharing with team
|
||||
if req.get("permission") == "team":
|
||||
shared_tenant_id: Optional[str] = req.get("shared_tenant_id")
|
||||
target_tenant_id: str = shared_tenant_id if shared_tenant_id else current_user.id
|
||||
if not has_permission(target_tenant_id, current_user.id, "canvas", "create"):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message='You do not have create permission for canvases in this team.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
|
||||
req["user_id"] = current_user.id
|
||||
if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip(), canvas_category=cate):
|
||||
return get_data_error_result(message=f"{req['title'].strip()} already exists.")
|
||||
|
|
@ -105,10 +120,13 @@ async def save() -> Any:
|
|||
if not UserCanvasService.save(**req):
|
||||
return get_data_error_result(message="Fail to save canvas.")
|
||||
else:
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
# Check update permission
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id, required_permission="update"):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
data=False,
|
||||
message='You do not have update permission for this canvas.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
UserCanvasService.update_by_id(req["id"], req)
|
||||
# save version
|
||||
UserCanvasVersionService.insert(user_canvas_id=req["id"], dsl=req["dsl"], title="{0}_{1}".format(req["title"], time.strftime("%Y_%m_%d_%H_%M_%S")))
|
||||
|
|
@ -156,10 +174,13 @@ async def run():
|
|||
files = req.get("files", [])
|
||||
inputs = req.get("inputs", {})
|
||||
user_id = req.get("user_id", current_user.id)
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
# Check read permission (to run the canvas)
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id, required_permission="read"):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
data=False,
|
||||
message='You do not have read permission for this canvas.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
|
||||
e, cvs = UserCanvasService.get_by_id(req["id"])
|
||||
if not e:
|
||||
|
|
@ -247,10 +268,14 @@ def cancel(task_id):
|
|||
@login_required
|
||||
async def reset():
|
||||
req = await request_json()
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
# Check update permission (to reset the canvas)
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id, required_permission="update"):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
data=False,
|
||||
message='You do not have update permission for this canvas.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
e, user_canvas = UserCanvasService.get_by_id(req["id"])
|
||||
if not e:
|
||||
|
|
@ -366,10 +391,13 @@ def input_form():
|
|||
@login_required
|
||||
async def debug():
|
||||
req = await request_json()
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
# Check read permission (to debug the canvas)
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id, required_permission="read"):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
data=False,
|
||||
message='You do not have read permission for this canvas.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
try:
|
||||
e, user_canvas = UserCanvasService.get_by_id(req["id"])
|
||||
canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id)
|
||||
|
|
@ -545,10 +573,13 @@ async def setting():
|
|||
req = await request_json()
|
||||
req["user_id"] = current_user.id
|
||||
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
# Check update permission (to change settings)
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id, required_permission="update"):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
data=False,
|
||||
message='You do not have update permission for this canvas.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
|
||||
e,flow = UserCanvasService.get_by_id(req["id"])
|
||||
if not e:
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ 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
|
||||
|
|
@ -105,19 +104,15 @@ async def update():
|
|||
message=f"Dataset name length is {len(req['name'])} which is large than {DATASET_NAME_LIMIT}")
|
||||
req["name"] = req["name"].strip()
|
||||
|
||||
if not KnowledgebaseService.accessible4deletion(req["kb_id"], current_user.id):
|
||||
# Check if user has update permission
|
||||
if not KnowledgebaseService.accessible(req["kb_id"], current_user.id, required_permission="update"):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message='No authorization.',
|
||||
code=RetCode.AUTHENTICATION_ERROR
|
||||
message='You do not have update permission for this knowledge base.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
if not KnowledgebaseService.query(
|
||||
created_by=current_user.id, id=req["kb_id"]):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of knowledgebase authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
|
||||
e, kb = KnowledgebaseService.get_by_id(req["kb_id"])
|
||||
if not e:
|
||||
return get_data_error_result(
|
||||
|
|
@ -233,11 +228,12 @@ async def list_kbs():
|
|||
@validate_request("kb_id")
|
||||
async def rm():
|
||||
req = await request_json()
|
||||
# Check if user has delete permission
|
||||
if not KnowledgebaseService.accessible4deletion(req["kb_id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message='No authorization.',
|
||||
code=RetCode.AUTHENTICATION_ERROR
|
||||
message='You do not have delete permission for this knowledge base.',
|
||||
code=RetCode.PERMISSION_ERROR
|
||||
)
|
||||
try:
|
||||
kbs = KnowledgebaseService.query(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from api.db.services.file2document_service import File2DocumentService
|
|||
from api.db.services.file_service import FileService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.task_service import GRAPH_RAPTOR_FAKE_DOC_ID, TaskService
|
||||
from api.db.services.user_service import TenantService
|
||||
from api.db.services.user_service import TenantService, UserTenantService
|
||||
from common.constants import RetCode, FileSource, StatusEnum
|
||||
from api.utils.api_utils import (
|
||||
deep_merge,
|
||||
|
|
@ -51,8 +51,6 @@ 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
|
||||
|
|
@ -238,6 +236,11 @@ async def delete(tenant_id):
|
|||
errors = []
|
||||
success_count = 0
|
||||
for kb_id, kb in kb_id_instance_pairs:
|
||||
# Check delete permission for each dataset
|
||||
if not KnowledgebaseService.accessible4deletion(kb_id, tenant_id):
|
||||
errors.append(f"User lacks delete permission for dataset '{kb_id}'")
|
||||
continue
|
||||
|
||||
for doc in DocumentService.query(kb_id=kb_id):
|
||||
if not DocumentService.remove_document(doc, tenant_id):
|
||||
errors.append(f"Remove document '{doc.id}' error for dataset '{kb_id}'")
|
||||
|
|
@ -346,10 +349,15 @@ async def update(tenant_id, dataset_id):
|
|||
return get_error_argument_result(message="No properties were modified")
|
||||
|
||||
try:
|
||||
# Check update permission
|
||||
if not KnowledgebaseService.accessible(dataset_id, tenant_id, required_permission="update"):
|
||||
return get_error_permission_result(
|
||||
message=f"User lacks update permission for dataset '{dataset_id}'")
|
||||
|
||||
kb = KnowledgebaseService.get_or_none(id=dataset_id, tenant_id=tenant_id)
|
||||
if kb is None:
|
||||
return get_error_permission_result(
|
||||
message=f"User '{tenant_id}' lacks permission for dataset '{dataset_id}'")
|
||||
message=f"Dataset '{dataset_id}' not found")
|
||||
|
||||
if req.get("parser_config"):
|
||||
req["parser_config"] = deep_merge(kb.parser_config, req["parser_config"])
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ import logging
|
|||
from threading import Thread
|
||||
from typing import Any, Dict, List, Optional, Set, Union
|
||||
|
||||
from quart import request
|
||||
from api.db import UserTenantRole
|
||||
from flask import Response, request, Blueprint
|
||||
from flask_login import current_user, login_required
|
||||
from quart import request
|
||||
|
||||
from api.apps import smtp_mail_server
|
||||
from api.db import FileType, UserTenantRole
|
||||
|
|
@ -33,10 +32,6 @@ from api.db.services.user_service import (
|
|||
UserService,
|
||||
UserTenantService,
|
||||
)
|
||||
|
||||
from common.constants import RetCode, StatusEnum
|
||||
from common.misc_utils import get_uuid
|
||||
from common.time_utils import delta_seconds
|
||||
from api.utils.api_utils import (
|
||||
get_data_error_result,
|
||||
get_json_result,
|
||||
|
|
@ -44,8 +39,11 @@ from api.utils.api_utils import (
|
|||
validate_request,
|
||||
)
|
||||
from api.utils.web_utils import send_invite_email
|
||||
from api.common.permission_utils import get_user_permissions, update_user_permissions
|
||||
from common import settings
|
||||
from api.apps import smtp_mail_server, login_required, current_user
|
||||
from common.constants import RetCode, StatusEnum
|
||||
from common.misc_utils import get_uuid
|
||||
from common.time_utils import delta_seconds
|
||||
|
||||
manager = Blueprint("tenant", __name__)
|
||||
def is_team_admin_or_owner(tenant_id: str, user_id: str) -> bool:
|
||||
|
|
@ -310,6 +308,12 @@ def create_team() -> Response:
|
|||
owner_user_id: Optional[str] = user_id
|
||||
if not owner_user_id:
|
||||
# Use current authenticated user as default
|
||||
if not current_user or not hasattr(current_user, 'id') or not current_user.id:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="Unable to determine user ID. Please ensure you are authenticated.",
|
||||
code=RetCode.UNAUTHORIZED,
|
||||
)
|
||||
owner_user_id = current_user.id
|
||||
|
||||
# Verify user exists
|
||||
|
|
@ -781,11 +785,17 @@ def update_request(tenant_id: str) -> Response:
|
|||
if accept:
|
||||
# Accept invitation - update role from INVITE to the specified role
|
||||
role: str = UserTenantRole.NORMAL.value
|
||||
|
||||
# Set default permissions: read-only access to datasets and canvases
|
||||
default_permissions: Dict[str, Any] = {
|
||||
"dataset": {"create": False, "read": True, "update": False, "delete": False},
|
||||
"canvas": {"create": False, "read": True, "update": False, "delete": False}
|
||||
}
|
||||
|
||||
# Update role from INVITE to the specified role (defaults to NORMAL)
|
||||
# Update role from INVITE to the specified role (defaults to NORMAL) and set default permissions
|
||||
UserTenantService.filter_update(
|
||||
[UserTenant.tenant_id == tenant_id, UserTenant.user_id == current_user.id],
|
||||
{"role": role, "status": StatusEnum.VALID.value}
|
||||
{"role": role, "status": StatusEnum.VALID.value, "permissions": default_permissions}
|
||||
)
|
||||
return get_json_result(data=True, message=f"Successfully joined the team with role '{role}'.")
|
||||
else:
|
||||
|
|
@ -964,6 +974,12 @@ def add_users(tenant_id: str) -> Response:
|
|||
})
|
||||
continue
|
||||
|
||||
# Set default permissions: read-only access to datasets and canvases
|
||||
default_permissions: Dict[str, Any] = {
|
||||
"dataset": {"create": False, "read": True, "update": False, "delete": False},
|
||||
"canvas": {"create": False, "read": True, "update": False, "delete": False}
|
||||
}
|
||||
|
||||
# Send invitation - create user with INVITE role (user must accept to join)
|
||||
UserTenantService.save(
|
||||
id=get_uuid(),
|
||||
|
|
@ -971,7 +987,8 @@ def add_users(tenant_id: str) -> Response:
|
|||
tenant_id=tenant_id,
|
||||
invited_by=current_user.id,
|
||||
role=UserTenantRole.INVITE, # Start with INVITE role
|
||||
status=StatusEnum.VALID.value
|
||||
status=StatusEnum.VALID.value,
|
||||
permissions=default_permissions
|
||||
)
|
||||
|
||||
# Send invitation email if configured
|
||||
|
|
@ -1382,3 +1399,285 @@ def demote_admin(tenant_id: str, user_id: str) -> Response:
|
|||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/<tenant_id>/users/<user_id>/permissions', methods=['GET']) # noqa: F821
|
||||
@login_required
|
||||
def get_user_permissions(tenant_id: str, user_id: str) -> Response:
|
||||
"""
|
||||
Get CRUD permissions for a team member.
|
||||
|
||||
Only team owners or admins can view permissions.
|
||||
|
||||
---
|
||||
tags:
|
||||
- Team
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: tenant_id
|
||||
required: true
|
||||
type: string
|
||||
description: Team ID
|
||||
- in: path
|
||||
name: user_id
|
||||
required: true
|
||||
type: string
|
||||
description: User ID to get permissions for
|
||||
responses:
|
||||
200:
|
||||
description: Permissions retrieved successfully.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
dataset:
|
||||
type: object
|
||||
properties:
|
||||
create:
|
||||
type: boolean
|
||||
read:
|
||||
type: boolean
|
||||
update:
|
||||
type: boolean
|
||||
delete:
|
||||
type: boolean
|
||||
canvas:
|
||||
type: object
|
||||
properties:
|
||||
create:
|
||||
type: boolean
|
||||
read:
|
||||
type: boolean
|
||||
update:
|
||||
type: boolean
|
||||
delete:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
401:
|
||||
description: Unauthorized.
|
||||
403:
|
||||
description: Forbidden - not team owner or admin.
|
||||
404:
|
||||
description: User not found in team.
|
||||
"""
|
||||
# Check if current user is team owner or admin
|
||||
if not is_team_admin_or_owner(tenant_id, current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="Only team owners or admins can view permissions.",
|
||||
code=RetCode.PERMISSION_ERROR,
|
||||
)
|
||||
|
||||
try:
|
||||
# Check if target user exists in the team
|
||||
user_tenant: Optional[UserTenant] = UserTenantService.filter_by_tenant_and_user_id(
|
||||
tenant_id, user_id
|
||||
)
|
||||
|
||||
if not user_tenant:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="User is not a member of this team.",
|
||||
code=RetCode.DATA_ERROR,
|
||||
)
|
||||
|
||||
permissions: Dict[str, Dict[str, bool]] = get_user_permissions(tenant_id, user_id)
|
||||
|
||||
return get_json_result(
|
||||
data=permissions,
|
||||
message="Permissions retrieved successfully.",
|
||||
)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/<tenant_id>/users/<user_id>/permissions', methods=['PUT']) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("permissions")
|
||||
def update_user_permissions(tenant_id: str, user_id: str) -> Response:
|
||||
"""
|
||||
Update CRUD permissions for a team member.
|
||||
|
||||
Only team owners or admins can update permissions.
|
||||
Owners and admins always have full permissions and cannot be restricted.
|
||||
|
||||
---
|
||||
tags:
|
||||
- Team
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: tenant_id
|
||||
required: true
|
||||
type: string
|
||||
description: Team ID
|
||||
- in: path
|
||||
name: user_id
|
||||
required: true
|
||||
type: string
|
||||
description: User ID to update permissions for
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- permissions
|
||||
properties:
|
||||
permissions:
|
||||
type: object
|
||||
description: Permissions to update (only provided fields will be updated)
|
||||
properties:
|
||||
dataset:
|
||||
type: object
|
||||
properties:
|
||||
create:
|
||||
type: boolean
|
||||
read:
|
||||
type: boolean
|
||||
update:
|
||||
type: boolean
|
||||
delete:
|
||||
type: boolean
|
||||
canvas:
|
||||
type: object
|
||||
properties:
|
||||
create:
|
||||
type: boolean
|
||||
read:
|
||||
type: boolean
|
||||
update:
|
||||
type: boolean
|
||||
delete:
|
||||
type: boolean
|
||||
responses:
|
||||
200:
|
||||
description: Permissions updated successfully.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
description: Updated permissions
|
||||
message:
|
||||
type: string
|
||||
400:
|
||||
description: Invalid request.
|
||||
401:
|
||||
description: Unauthorized.
|
||||
403:
|
||||
description: Forbidden - not team owner or admin, or trying to restrict owner/admin.
|
||||
404:
|
||||
description: User not found in team.
|
||||
"""
|
||||
# Check if current user is team owner or admin
|
||||
if not is_team_admin_or_owner(tenant_id, current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="Only team owners or admins can update permissions.",
|
||||
code=RetCode.PERMISSION_ERROR,
|
||||
)
|
||||
|
||||
if request.json is None:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="Request body is required!",
|
||||
code=RetCode.ARGUMENT_ERROR,
|
||||
)
|
||||
|
||||
req: Dict[str, Any] = request.json
|
||||
permissions: Optional[Dict[str, Any]] = req.get("permissions")
|
||||
|
||||
if not permissions or not isinstance(permissions, dict):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="'permissions' must be a non-empty object.",
|
||||
code=RetCode.ARGUMENT_ERROR,
|
||||
)
|
||||
|
||||
try:
|
||||
# Check if target user exists in the team
|
||||
user_tenant: Optional[UserTenant] = UserTenantService.filter_by_tenant_and_user_id(
|
||||
tenant_id, user_id
|
||||
)
|
||||
|
||||
if not user_tenant:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="User is not a member of this team.",
|
||||
code=RetCode.DATA_ERROR,
|
||||
)
|
||||
|
||||
# Owners and admins always have full permissions - cannot be restricted
|
||||
if user_tenant.role in [UserTenantRole.OWNER, UserTenantRole.ADMIN]:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="Cannot update permissions for team owners or admins. They always have full permissions.",
|
||||
code=RetCode.DATA_ERROR,
|
||||
)
|
||||
|
||||
# Validate permissions structure
|
||||
valid_resource_types = {"dataset", "canvas"}
|
||||
valid_permissions = {"create", "read", "update", "delete"}
|
||||
|
||||
validated_permissions: Dict[str, Dict[str, bool]] = {}
|
||||
for resource_type, perms in permissions.items():
|
||||
if resource_type not in valid_resource_types:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message=f"Invalid resource type '{resource_type}'. Must be one of: {', '.join(valid_resource_types)}",
|
||||
code=RetCode.ARGUMENT_ERROR,
|
||||
)
|
||||
|
||||
if not isinstance(perms, dict):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message=f"Permissions for '{resource_type}' must be an object.",
|
||||
code=RetCode.ARGUMENT_ERROR,
|
||||
)
|
||||
|
||||
validated_permissions[resource_type] = {}
|
||||
for perm_name, perm_value in perms.items():
|
||||
if perm_name not in valid_permissions:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message=f"Invalid permission '{perm_name}' for '{resource_type}'. Must be one of: {', '.join(valid_permissions)}",
|
||||
code=RetCode.ARGUMENT_ERROR,
|
||||
)
|
||||
|
||||
if not isinstance(perm_value, bool):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message=f"Permission value for '{resource_type}.{perm_name}' must be a boolean.",
|
||||
code=RetCode.ARGUMENT_ERROR,
|
||||
)
|
||||
|
||||
validated_permissions[resource_type][perm_name] = perm_value
|
||||
|
||||
# Update permissions
|
||||
success: bool = update_user_permissions(tenant_id, user_id, validated_permissions)
|
||||
|
||||
if not success:
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="Failed to update permissions.",
|
||||
code=RetCode.EXCEPTION_ERROR,
|
||||
)
|
||||
|
||||
# Get updated permissions for response
|
||||
updated_permissions: Dict[str, Dict[str, bool]] = get_user_permissions(tenant_id, user_id)
|
||||
|
||||
return get_json_result(
|
||||
data=updated_permissions,
|
||||
message="Permissions updated successfully.",
|
||||
)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return server_error_response(e)
|
||||
|
|
|
|||
|
|
@ -23,10 +23,8 @@ import time
|
|||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Match
|
||||
|
||||
from quart import redirect, request, session, make_response
|
||||
from quart import redirect, request, session, make_response, Response
|
||||
|
||||
from flask import redirect, request, session, make_response, Response
|
||||
from flask_login import current_user, login_required, login_user, logout_user
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from api.apps.auth import get_auth_client
|
||||
|
|
|
|||
|
|
@ -14,17 +14,20 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from typing import Dict, Any, Optional, List, Literal
|
||||
|
||||
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, UserTenantService
|
||||
from api.common.permission_utils import has_permission
|
||||
from common.constants import StatusEnum
|
||||
|
||||
|
||||
def check_kb_team_permission(kb: Dict[str, Any] | Knowledgebase, other: str) -> bool:
|
||||
def check_kb_team_permission(
|
||||
kb: Dict[str, Any] | Knowledgebase,
|
||||
other: str,
|
||||
required_permission: Literal["create", "read", "update", "delete"] = "read"
|
||||
) -> bool:
|
||||
kb = kb.to_dict() if isinstance(kb, Knowledgebase) else kb
|
||||
|
||||
kb_tenant_id = kb["tenant_id"]
|
||||
|
|
@ -37,19 +40,23 @@ def check_kb_team_permission(kb: Dict[str, Any] | Knowledgebase, other: str) ->
|
|||
if kb["permission"] != TenantPermission.TEAM:
|
||||
return False
|
||||
|
||||
# If shared_tenant_id is specified, check if user is a member of that specific tenant
|
||||
# Determine which tenant to check permissions for
|
||||
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)
|
||||
target_tenant_id: str = shared_tenant_id if shared_tenant_id else kb_tenant_id
|
||||
|
||||
# Check if user is a member of the target tenant
|
||||
user_tenant = UserTenantService.filter_by_tenant_and_user_id(target_tenant_id, other)
|
||||
if not user_tenant or user_tenant.status != StatusEnum.VALID.value:
|
||||
return False
|
||||
|
||||
# Check CRUD permissions
|
||||
return has_permission(target_tenant_id, other, "dataset", required_permission)
|
||||
|
||||
|
||||
def check_file_team_permission(file: Dict[str, Any] | File, other: str) -> bool:
|
||||
# Import here to avoid circular import
|
||||
from api.db.services.file_service import FileService
|
||||
|
||||
file = file.to_dict() if isinstance(file, File) else file
|
||||
|
||||
file_tenant_id = file["tenant_id"]
|
||||
|
|
@ -60,6 +67,9 @@ def check_file_team_permission(file: Dict[str, Any] | File, other: str) -> bool:
|
|||
|
||||
kb_ids: List[str] = [kb_info["kb_id"] for kb_info in FileService.get_kb_id_by_file_id(file_id)]
|
||||
|
||||
# Import here to avoid circular import
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
|
||||
for kb_id in kb_ids:
|
||||
ok: bool
|
||||
kb: Optional[Knowledgebase]
|
||||
|
|
|
|||
147
api/common/permission_utils.py
Normal file
147
api/common/permission_utils.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from typing import Dict, Any, Optional, Literal
|
||||
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from api.db.db_models import UserTenant
|
||||
from api.db import UserTenantRole
|
||||
from common.constants import StatusEnum
|
||||
|
||||
|
||||
def get_user_permissions(tenant_id: str, user_id: str) -> Dict[str, Dict[str, bool]]:
|
||||
"""
|
||||
Get CRUD permissions for a user in a tenant.
|
||||
|
||||
Args:
|
||||
tenant_id: The tenant ID.
|
||||
user_id: The user ID.
|
||||
|
||||
Returns:
|
||||
Dictionary with permissions for dataset and canvas:
|
||||
{
|
||||
"dataset": {"create": bool, "read": bool, "update": bool, "delete": bool},
|
||||
"canvas": {"create": bool, "read": bool, "update": bool, "delete": bool}
|
||||
}
|
||||
Returns default permissions (read-only) if user not found or no permissions set.
|
||||
"""
|
||||
user_tenant = UserTenantService.filter_by_tenant_and_user_id(tenant_id, user_id)
|
||||
|
||||
if not user_tenant or user_tenant.status != StatusEnum.VALID.value:
|
||||
# Return default read-only permissions
|
||||
return {
|
||||
"dataset": {"create": False, "read": True, "update": False, "delete": False},
|
||||
"canvas": {"create": False, "read": True, "update": False, "delete": False}
|
||||
}
|
||||
|
||||
# Owners and admins have full permissions
|
||||
if user_tenant.role in [UserTenantRole.OWNER, UserTenantRole.ADMIN]:
|
||||
return {
|
||||
"dataset": {"create": True, "read": True, "update": True, "delete": True},
|
||||
"canvas": {"create": True, "read": True, "update": True, "delete": True}
|
||||
}
|
||||
|
||||
# Get permissions from user_tenant.permissions, with defaults
|
||||
permissions = user_tenant.permissions if user_tenant.permissions else {}
|
||||
|
||||
default_permissions = {
|
||||
"dataset": {"create": False, "read": True, "update": False, "delete": False},
|
||||
"canvas": {"create": False, "read": True, "update": False, "delete": False}
|
||||
}
|
||||
|
||||
# Merge with defaults to ensure all fields are present
|
||||
result: Dict[str, Dict[str, bool]] = {}
|
||||
for resource_type in ["dataset", "canvas"]:
|
||||
result[resource_type] = {
|
||||
"create": permissions.get(resource_type, {}).get("create", default_permissions[resource_type]["create"]),
|
||||
"read": permissions.get(resource_type, {}).get("read", default_permissions[resource_type]["read"]),
|
||||
"update": permissions.get(resource_type, {}).get("update", default_permissions[resource_type]["update"]),
|
||||
"delete": permissions.get(resource_type, {}).get("delete", default_permissions[resource_type]["delete"]),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def has_permission(
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
resource_type: Literal["dataset", "canvas"],
|
||||
permission: Literal["create", "read", "update", "delete"]
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a user has a specific permission for a resource type in a tenant.
|
||||
|
||||
Args:
|
||||
tenant_id: The tenant ID.
|
||||
user_id: The user ID.
|
||||
resource_type: Type of resource ("dataset" or "canvas").
|
||||
permission: The permission to check ("create", "read", "update", or "delete").
|
||||
|
||||
Returns:
|
||||
True if user has the permission, False otherwise.
|
||||
"""
|
||||
permissions = get_user_permissions(tenant_id, user_id)
|
||||
return permissions.get(resource_type, {}).get(permission, False)
|
||||
|
||||
|
||||
def update_user_permissions(
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
permissions: Dict[str, Dict[str, bool]]
|
||||
) -> bool:
|
||||
"""
|
||||
Update CRUD permissions for a user in a tenant.
|
||||
|
||||
Args:
|
||||
tenant_id: The tenant ID.
|
||||
user_id: The user ID.
|
||||
permissions: Dictionary with permissions to update:
|
||||
{
|
||||
"dataset": {"create": bool, "read": bool, "update": bool, "delete": bool},
|
||||
"canvas": {"create": bool, "read": bool, "update": bool, "delete": bool}
|
||||
}
|
||||
Only provided fields will be updated, others will remain unchanged.
|
||||
|
||||
Returns:
|
||||
True if update was successful, False otherwise.
|
||||
"""
|
||||
user_tenant = UserTenantService.filter_by_tenant_and_user_id(tenant_id, user_id)
|
||||
|
||||
if not user_tenant:
|
||||
return False
|
||||
|
||||
# Get current permissions or defaults
|
||||
current_permissions = user_tenant.permissions if user_tenant.permissions else {
|
||||
"dataset": {"create": False, "read": True, "update": False, "delete": False},
|
||||
"canvas": {"create": False, "read": True, "update": False, "delete": False}
|
||||
}
|
||||
|
||||
# Merge new permissions with current permissions
|
||||
updated_permissions: Dict[str, Dict[str, bool]] = {}
|
||||
for resource_type in ["dataset", "canvas"]:
|
||||
updated_permissions[resource_type] = current_permissions.get(resource_type, {}).copy()
|
||||
if resource_type in permissions:
|
||||
updated_permissions[resource_type].update(permissions[resource_type])
|
||||
|
||||
# Update in database
|
||||
from api.db.db_models import UserTenant as UserTenantModel
|
||||
UserTenantService.filter_update(
|
||||
[UserTenantModel.tenant_id == tenant_id, UserTenantModel.user_id == user_id],
|
||||
{"permissions": updated_permissions}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -649,6 +649,7 @@ class UserTenant(DataBaseModel):
|
|||
role = CharField(max_length=32, null=False, help_text="UserTenantRole", index=True)
|
||||
invited_by = CharField(max_length=32, null=False, index=True)
|
||||
status = CharField(max_length=1, null=True, help_text="is it validate(0: wasted, 1: validate)", default="1", index=True)
|
||||
permissions = JSONField(null=True, help_text="CRUD permissions for datasets and canvases", default={"dataset": {"create": False, "read": True, "update": False, "delete": False}, "canvas": {"create": False, "read": True, "update": False, "delete": False}})
|
||||
|
||||
class Meta:
|
||||
db_table = "user_tenant"
|
||||
|
|
@ -1211,6 +1212,11 @@ def migrate_db():
|
|||
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:
|
||||
default_permissions = {"dataset": {"create": False, "read": True, "update": False, "delete": False}, "canvas": {"create": False, "read": True, "update": False, "delete": False}}
|
||||
migrate(migrator.add_column("user_tenant", "permissions", JSONField(null=True, help_text="CRUD permissions for datasets and canvases", default=default_permissions)))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
migrate(migrator.add_column("llm", "is_tools", BooleanField(null=False, help_text="support tools", default=False)))
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from typing import List, Dict, Any, Optional, Tuple, Literal
|
||||
from uuid import uuid4
|
||||
from agent.canvas import Canvas
|
||||
from api.db import CanvasCategory, TenantPermission
|
||||
|
|
@ -28,10 +28,9 @@ 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
|
||||
from api.common.permission_utils import has_permission
|
||||
|
||||
class CanvasTemplateService(CommonService):
|
||||
model = CanvasTemplate
|
||||
|
|
@ -199,8 +198,12 @@ class UserCanvasService(CommonService):
|
|||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def accessible(cls, canvas_id: str, tenant_id: str) -> bool:
|
||||
|
||||
def accessible(
|
||||
cls,
|
||||
canvas_id: str,
|
||||
tenant_id: str,
|
||||
required_permission: Literal["create", "read", "update", "delete"] = "read"
|
||||
) -> bool:
|
||||
|
||||
e: bool
|
||||
c: Optional[Dict[str, Any]]
|
||||
|
|
@ -208,7 +211,7 @@ class UserCanvasService(CommonService):
|
|||
if not e or c is None:
|
||||
return False
|
||||
|
||||
# If user owns the canvas, always allow
|
||||
# If user owns the canvas, always allow (full permissions)
|
||||
if c["user_id"] == tenant_id:
|
||||
return True
|
||||
|
||||
|
|
@ -216,15 +219,17 @@ class UserCanvasService(CommonService):
|
|||
if c.get("permission") != TenantPermission.TEAM.value:
|
||||
return False
|
||||
|
||||
# If shared_tenant_id is specified, check if user is a member of that specific tenant
|
||||
# Determine which tenant to check permissions for
|
||||
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
|
||||
target_tenant_id: str = shared_tenant_id if shared_tenant_id else str(c["user_id"])
|
||||
|
||||
# Check if user is a member of the target tenant
|
||||
user_tenant = UserTenantService.filter_by_tenant_and_user_id(target_tenant_id, tenant_id)
|
||||
if not user_tenant or user_tenant.status != StatusEnum.VALID.value:
|
||||
return False
|
||||
|
||||
# Check CRUD permissions
|
||||
return has_permission(target_tenant_id, tenant_id, "canvas", required_permission)
|
||||
|
||||
|
||||
async def completion(tenant_id, agent_id, session_id=None, **kwargs):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
from datetime import datetime
|
||||
from typing import Optional, Literal
|
||||
|
||||
from peewee import fn, JOIN
|
||||
|
||||
|
|
@ -27,6 +28,8 @@ from common.misc_utils import get_uuid
|
|||
from common.constants import StatusEnum
|
||||
from api.constants import DATASET_NAME_LIMIT
|
||||
from api.utils.api_utils import get_parser_config, get_data_error_result
|
||||
from api.common.check_team_permission import check_kb_team_permission
|
||||
from common.constants import TaskStatus
|
||||
|
||||
|
||||
class KnowledgebaseService(CommonService):
|
||||
|
|
@ -50,37 +53,37 @@ class KnowledgebaseService(CommonService):
|
|||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def accessible4deletion(cls, kb_id, user_id):
|
||||
def accessible4deletion(cls, kb_id: str, user_id: str) -> bool:
|
||||
"""Check if a knowledge base can be deleted by a specific user.
|
||||
|
||||
This method verifies whether a user has permission to delete a knowledge base
|
||||
by checking if they are the creator of that knowledge base.
|
||||
by checking if they are the creator or have delete permission via CRUD permissions.
|
||||
|
||||
Args:
|
||||
kb_id (str): The unique identifier of the knowledge base to check.
|
||||
user_id (str): The unique identifier of the user attempting the deletion.
|
||||
kb_id: The unique identifier of the knowledge base to check.
|
||||
user_id: The unique identifier of the user attempting the deletion.
|
||||
|
||||
Returns:
|
||||
bool: True if the user has permission to delete the knowledge base,
|
||||
False if the user doesn't have permission or the knowledge base doesn't exist.
|
||||
|
||||
Example:
|
||||
>>> KnowledgebaseService.accessible4deletion("kb123", "user456")
|
||||
True
|
||||
|
||||
Note:
|
||||
- This method only checks creator permissions
|
||||
- A return value of False can mean either:
|
||||
1. The knowledge base doesn't exist
|
||||
2. The user is not the creator of the knowledge base
|
||||
"""
|
||||
# Check if a knowledge base can be deleted by a user
|
||||
docs = cls.model.select(
|
||||
cls.model.id).where(cls.model.id == kb_id, cls.model.created_by == user_id).paginate(0, 1)
|
||||
docs = docs.dicts()
|
||||
if not docs:
|
||||
|
||||
# Get the knowledge base
|
||||
ok: bool
|
||||
kb: Optional[Knowledgebase]
|
||||
ok, kb = cls.get_by_id(kb_id)
|
||||
if not ok or kb is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
# If user is the creator, always allow
|
||||
if kb.created_by == user_id:
|
||||
return True
|
||||
|
||||
# Check team permissions with delete permission required
|
||||
if kb.permission == "team":
|
||||
return check_kb_team_permission(kb, user_id, required_permission="delete")
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
|
|
@ -93,9 +96,9 @@ class KnowledgebaseService(CommonService):
|
|||
# Returns:
|
||||
# If all documents are parsed successfully, returns (True, None)
|
||||
# If any document is not fully parsed, returns (False, error_message)
|
||||
from common.constants import TaskStatus
|
||||
# Import here to avoid circular import
|
||||
from api.db.services.document_service import DocumentService
|
||||
|
||||
|
||||
# Get knowledge base information
|
||||
kbs = cls.query(id=kb_id)
|
||||
if not kbs:
|
||||
|
|
@ -470,20 +473,33 @@ class KnowledgebaseService(CommonService):
|
|||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def accessible(cls, kb_id, user_id):
|
||||
# Check if a knowledge base is accessible by a user
|
||||
# Args:
|
||||
# kb_id: Knowledge base ID
|
||||
# user_id: User ID
|
||||
# Returns:
|
||||
# Boolean indicating accessibility
|
||||
docs = cls.model.select(
|
||||
cls.model.id).join(UserTenant, on=(UserTenant.tenant_id == Knowledgebase.tenant_id)
|
||||
).where(cls.model.id == kb_id, UserTenant.user_id == user_id).paginate(0, 1)
|
||||
docs = docs.dicts()
|
||||
if not docs:
|
||||
def accessible(cls, kb_id: str, user_id: str, required_permission: Literal["create", "read", "update", "delete"] = "read") -> bool:
|
||||
"""Check if a knowledge base is accessible by a user with the required permission.
|
||||
|
||||
Args:
|
||||
kb_id: Knowledge base ID
|
||||
user_id: User ID
|
||||
required_permission: Required permission level ("create", "read", "update", "delete")
|
||||
Returns:
|
||||
Boolean indicating accessibility
|
||||
"""
|
||||
|
||||
# Get the knowledge base
|
||||
ok: bool
|
||||
kb: Optional[Knowledgebase]
|
||||
ok, kb = cls.get_by_id(kb_id)
|
||||
if not ok or kb is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
# If user is the creator, always allow
|
||||
if kb.created_by == user_id:
|
||||
return True
|
||||
|
||||
# Check team permissions
|
||||
if kb.permission == "team":
|
||||
return check_kb_team_permission(kb, user_id, required_permission=required_permission)
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue