Skip to content

Commit 080d4e0

Browse files
committed
feat: data permission
1 parent a817cfc commit 080d4e0

File tree

10 files changed

+366
-8
lines changed

10 files changed

+366
-8
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""025_ds_num
2+
3+
Revision ID: 97dcdbedaaf3
4+
Revises: 4c6d18a18bd4
5+
Create Date: 2025-07-15 15:50:56.942959
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel.sql.sqltypes
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '97dcdbedaaf3'
15+
down_revision = '806bc67ff45f'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.add_column('core_datasource', sa.Column('num', sqlmodel.sql.sqltypes.AutoString(length=256), nullable=True))
23+
# ### end Alembic commands ###
24+
25+
26+
def downgrade():
27+
# ### commands auto generated by Alembic - please adjust! ###
28+
op.drop_column('core_datasource', 'num')
29+
# ### end Alembic commands ###
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""026_row_column_permission
2+
3+
Revision ID: 4c6d18a18bd4
4+
Revises: 863105882eba
5+
Create Date: 2025-06-25 17:32:09.183257
6+
7+
"""
8+
import sqlalchemy as sa
9+
import sqlmodel.sql.sqltypes
10+
from alembic import op
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '4c6d18a18bd4'
15+
down_revision = '97dcdbedaaf3'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.create_table('ds_permission',
23+
sa.Column('id', sa.Integer(), sa.Identity(always=True), nullable=False),
24+
sa.Column('enable', sa.Boolean(), nullable=False),
25+
sa.Column('auth_target_type', sa.String(128), nullable=False),
26+
sa.Column('auth_target_id', sa.BigInteger(), nullable=True),
27+
sa.Column('type', sa.String(64), nullable=False),
28+
sa.Column('ds_id', sa.BigInteger(), nullable=True),
29+
sa.Column('table_id', sa.BigInteger(), nullable=True),
30+
sa.Column('expression_tree', sa.Text(), nullable=True),
31+
sa.Column('permissions', sa.Text(), nullable=True),
32+
sa.Column('white_list_user', sa.Text(), nullable=True),
33+
sa.Column('create_time', sa.DateTime(), nullable=True),
34+
sa.PrimaryKeyConstraint('id')
35+
)
36+
op.create_table('ds_rules',
37+
sa.Column('id', sa.Integer(), sa.Identity(always=True), nullable=False),
38+
sa.Column('enable', sa.Boolean(), nullable=False),
39+
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=128), nullable=False),
40+
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=512), nullable=True),
41+
sa.Column('permission_list', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
42+
sa.Column('user_list', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
43+
sa.Column('white_list_user', sa.Text(), nullable=True),
44+
sa.Column('create_time', sa.DateTime(), nullable=True),
45+
sa.PrimaryKeyConstraint('id')
46+
)
47+
# ### end Alembic commands ###
48+
49+
50+
def downgrade():
51+
# ### commands auto generated by Alembic - please adjust! ###
52+
op.drop_table('ds_rules')
53+
op.drop_table('ds_permission')
54+
# ### end Alembic commands ###

backend/apps/chat/models/chat_model.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from apps.template.generate_predict.generator import get_predict_template
1212
from apps.template.generate_sql.generator import get_sql_template
1313
from apps.template.select_datasource.generator import get_datasource_template
14+
from apps.template.filter.generator import get_permissions_template
1415

1516

1617
class Chat(SQLModel, table=True):
@@ -98,6 +99,7 @@ class AiModelQuestion(BaseModel):
9899
fields: str = ""
99100
data: str = ""
100101
lang: str = "zh-CN"
102+
filter: str = []
101103

102104
def sql_sys_question(self):
103105
return get_sql_template()['system'].format(engine=self.engine, schema=self.db_schema, question=self.question)
@@ -137,6 +139,12 @@ def guess_user_question(self, old_questions: str = "[]"):
137139
return get_guess_question_template()['user'].format(question=self.question, schema=self.db_schema,
138140
old_questions=old_questions, lang=self.lang)
139141

142+
def filter_sys_question(self):
143+
return get_permissions_template()['system']
144+
145+
def filter_user_question(self):
146+
return get_permissions_template()['user'].format(sql=self.sql, filter=self.filter, lang=self.lang)
147+
140148

141149
class ChatQuestion(AiModelQuestion):
142150
question: str = ''

backend/apps/chat/task/llm.py

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
import traceback
34
import warnings
@@ -10,8 +11,13 @@
1011
from langchain.chat_models.base import BaseChatModel
1112
from langchain_community.utilities import SQLDatabase
1213
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage, BaseMessageChunk
14+
from sqlalchemy import and_, cast
1315
from sqlalchemy import select
16+
from sqlalchemy.dialects.postgresql import JSONB
1417
from sqlalchemy.orm import load_only
18+
from sqlbot_xpack.permissions.api.permission import transRecord2DTO
19+
from sqlbot_xpack.permissions.models.ds_permission import DsPermission, PermissionDTO
20+
from sqlbot_xpack.permissions.models.ds_rules import DsRules
1521

1622
from apps.ai_model.model_factory import LLMConfig, LLMFactory, get_default_config
1723
from apps.chat.curd.chat import save_question, save_full_sql_message, save_full_sql_message_and_answer, save_sql, \
@@ -21,7 +27,8 @@
2127
get_old_questions, save_analysis_predict_record, list_base_records, rename_chat
2228
from apps.chat.models.chat_model import ChatQuestion, ChatRecord, Chat, RenameChat
2329
from apps.datasource.crud.datasource import get_table_schema
24-
from apps.datasource.models.datasource import CoreDatasource
30+
from apps.datasource.crud.row_permission import transFilterTree
31+
from apps.datasource.models.datasource import CoreDatasource, CoreTable
2532
from apps.db.db import exec_sql
2633
from apps.system.crud.assistant import get_assistant_ds
2734
from common.core.config import settings
@@ -77,7 +84,7 @@ def __init__(self, session: SessionDep, current_user: CurrentUser, chat_question
7784

7885
# get schema
7986
if ds:
80-
chat_question.db_schema = get_table_schema(session=self.session, ds=ds)
87+
chat_question.db_schema = get_table_schema(session=self.session, current_user=current_user, ds=ds)
8188

8289
chat_question.lang = current_user.language
8390

@@ -467,6 +474,78 @@ def generate_sql(self):
467474
[{'type': msg.type, 'content': msg.content} for msg in
468475
self.sql_message]).decode())
469476

477+
def generate_filter(self, sql: str, tables: List):
478+
table_list = self.session.query(CoreTable).filter(
479+
and_(CoreTable.ds_id == self.ds.id, CoreTable.table_name.in_(tables))
480+
).all()
481+
482+
filters = []
483+
for table in table_list:
484+
row_permissions = self.session.query(DsPermission).filter(
485+
and_(DsPermission.table_id == table.id, DsPermission.type == 'row')).all()
486+
res: List[PermissionDTO] = []
487+
if row_permissions is not None:
488+
for permission in row_permissions:
489+
# check permission and user in same rules
490+
obj = self.session.query(DsRules).filter(
491+
and_(DsRules.permission_list.op('@>')(cast([permission.id], JSONB)),
492+
DsRules.user_list.op('@>')(cast([self.current_user.id], JSONB)))
493+
).first()
494+
if obj is not None:
495+
res.append(transRecord2DTO(self.session, permission))
496+
wheres = transFilterTree(self.session, res, self.ds)
497+
filters.append({"table": table.table_name, "filter": wheres})
498+
499+
filter = json.dumps(filters, ensure_ascii=False)
500+
# filter = f"""[{{"table":"{tables[0]}","filter":"省份 = '广东省' or 销售额(万元) > 10000"}}]""" # todo get filters
501+
self.chat_question.sql = sql
502+
self.chat_question.filter = filter
503+
msg: List[Union[BaseMessage, dict[str, Any]]] = []
504+
msg.append(SystemMessage(content=self.chat_question.filter_sys_question()))
505+
msg.append(HumanMessage(content=self.chat_question.filter_user_question()))
506+
507+
history_msg = []
508+
# if self.record.full_analysis_message and self.record.full_analysis_message.strip() != '':
509+
# history_msg = orjson.loads(self.record.full_analysis_message)
510+
511+
# self.record = save_full_analysis_message_and_answer(session=self.session, record_id=self.record.id, answer='',
512+
# full_message=orjson.dumps(history_msg +
513+
# [{'type': msg.type,
514+
# 'content': msg.content} for msg
515+
# in
516+
# msg]).decode())
517+
full_thinking_text = ''
518+
full_filter_text = ''
519+
res = self.llm.stream(msg)
520+
token_usage = {}
521+
for chunk in res:
522+
print(chunk)
523+
reasoning_content_chunk = ''
524+
if 'reasoning_content' in chunk.additional_kwargs:
525+
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
526+
# else:
527+
# reasoning_content_chunk = chunk.get('reasoning_content')
528+
if reasoning_content_chunk is None:
529+
reasoning_content_chunk = ''
530+
full_thinking_text += reasoning_content_chunk
531+
532+
full_filter_text += chunk.content
533+
# yield {'content': chunk.content, 'reasoning_content': reasoning_content_chunk}
534+
get_token_usage(chunk, token_usage)
535+
536+
msg.append(AIMessage(full_filter_text))
537+
# self.record = save_full_analysis_message_and_answer(session=self.session, record_id=self.record.id,
538+
# token_usage=token_usage,
539+
# answer=orjson.dumps({'content': full_analysis_text,
540+
# 'reasoning_content': full_thinking_text}).decode(),
541+
# full_message=orjson.dumps(history_msg +
542+
# [{'type': msg.type,
543+
# 'content': msg.content} for msg
544+
# in
545+
# analysis_msg]).decode())
546+
print(full_filter_text)
547+
return full_filter_text
548+
470549
def generate_chart(self):
471550
# append current question
472551
self.chart_message.append(HumanMessage(self.chat_question.chart_user_question()))
@@ -663,7 +742,15 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
663742

664743
# filter sql
665744
print(full_sql_text)
666-
sql = llm_service.check_save_sql(res=full_sql_text)
745+
746+
# todo row permission
747+
sql_json_str = extract_nested_json(full_sql_text)
748+
data = orjson.loads(sql_json_str)
749+
sql_result = llm_service.generate_filter(data['sql'], data['tables'])
750+
print(sql_result)
751+
sql = llm_service.check_save_sql(res=sql_result)
752+
# sql = llm_service.check_save_sql(res=full_sql_text)
753+
667754
print(sql)
668755
if in_chat:
669756
yield orjson.dumps({'content': sql, 'type': 'sql'}).decode() + '\n\n'

backend/apps/datasource/crud/datasource.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import json
33
from typing import List
44

5-
from sqlalchemy import and_, text
5+
from sqlalchemy import and_, text, cast
6+
from sqlalchemy.dialects.postgresql import JSONB
7+
from sqlbot_xpack.permissions.models.ds_permission import DsPermission
8+
from sqlbot_xpack.permissions.models.ds_rules import DsRules
69
from sqlmodel import select
710

811
from apps.datasource.utils.utils import aes_decrypt
@@ -12,6 +15,7 @@
1215
from apps.db.type import db_type_relation
1316
from common.core.deps import SessionDep, CurrentUser
1417
from common.utils.utils import deepcopy_ignore_extra
18+
from .table import get_tables_by_ds_id
1519
from ..crud.field import delete_field_by_ds_id, update_field
1620
from ..crud.table import delete_table_by_ds_id, update_table
1721
from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, CoreField, ColumnSchema, TableObj, \
@@ -71,12 +75,14 @@ def create_ds(session: SessionDep, user: CurrentUser, create_ds: CreateDatasourc
7175

7276
# save tables and fields
7377
sync_table(session, ds, create_ds.tables)
78+
updateNum(session, ds)
7479
return ds
7580

7681

7782
def chooseTables(session: SessionDep, id: int, tables: List[CoreTable]):
7883
ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first()
7984
sync_table(session, ds, tables)
85+
updateNum(session, ds)
8086

8187

8288
def update_ds(session: SessionDep, ds: CoreDatasource):
@@ -239,20 +245,49 @@ def preview(session: SessionDep, id: int, data: TableObj):
239245
return exec_sql(ds, sql)
240246

241247

242-
def get_table_obj_by_ds(session: SessionDep, ds: CoreDatasource) -> List[TableAndFields]:
248+
def updateNum(session: SessionDep, ds: CoreDatasource):
249+
all_tables = get_tables(ds)
250+
selected_tables = get_tables_by_ds_id(session, ds.id)
251+
num = f'{len(selected_tables)}/{len(all_tables)}'
252+
253+
record = session.exec(select(CoreDatasource).where(CoreDatasource.id == ds.id)).first()
254+
update_data = ds.model_dump(exclude_unset=True)
255+
for field, value in update_data.items():
256+
setattr(record, field, value)
257+
record.num = num
258+
session.add(record)
259+
session.commit()
260+
261+
262+
def get_table_obj_by_ds(session: SessionDep, current_user: CurrentUser, ds: CoreDatasource) -> List[TableAndFields]:
243263
_list: List = []
244264
tables = session.query(CoreTable).filter(CoreTable.ds_id == ds.id).all()
245265
conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config()
246266
schema = conf.dbSchema if conf.dbSchema is not None and conf.dbSchema != "" else conf.database
247267
for table in tables:
248268
fields = session.query(CoreField).filter(and_(CoreField.table_id == table.id, CoreField.checked == True)).all()
269+
270+
# do column permissions, filter fields
271+
column_permissions = session.query(DsPermission).filter(
272+
and_(DsPermission.table_id == table.id, DsPermission.type == 'column')).all()
273+
if column_permissions is not None:
274+
for permission in column_permissions:
275+
# check permission and user in same rules
276+
obj = session.query(DsRules).filter(
277+
and_(DsRules.permission_list.op('@>')(cast([permission.id], JSONB)),
278+
DsRules.user_list.op('@>')(cast([current_user.id], JSONB)))
279+
).first()
280+
if obj is not None:
281+
permission_list = json.loads(permission.permissions)
282+
fields = filter_list(fields, permission_list)
283+
249284
_list.append(TableAndFields(schema=schema, table=table, fields=fields))
250285
return _list
251286

252287

253-
def get_table_schema(session: SessionDep, ds: CoreDatasource) -> str:
288+
def get_table_schema(session: SessionDep, current_user: CurrentUser, ds: CoreDatasource) -> str:
254289
schema_str = ""
255-
table_objs = get_table_obj_by_ds(session=session, ds=ds)
290+
table_objs = get_table_obj_by_ds(session=session, current_user=current_user, ds=ds)
256291
if len(table_objs) == 0:
257292
return schema_str
258293
db_name = table_objs[0].schema
@@ -279,3 +314,12 @@ def get_table_schema(session: SessionDep, ds: CoreDatasource) -> str:
279314
schema_str += ",\n".join(field_list)
280315
schema_str += '\n]\n'
281316
return schema_str
317+
318+
319+
def filter_list(list_a, list_b):
320+
id_to_invalid = {}
321+
for b in list_b:
322+
if not b['enable']:
323+
id_to_invalid[b['field_id']] = True
324+
325+
return [a for a in list_a if not id_to_invalid.get(a.id, False)]

0 commit comments

Comments
 (0)