From 477745cfb9210ceb3534d53ddc87b71b9512b772 Mon Sep 17 00:00:00 2001 From: shaohuzhang1 Date: Wed, 16 Apr 2025 20:05:37 +0800 Subject: [PATCH 1/2] feat: authentication --- apps/common/auth/handle/impl/user_token.py | 124 ++++++++++++------ apps/common/constants/cache_version.py | 16 ++- apps/common/constants/permission_constants.py | 54 ++++++-- apps/common/utils/common.py | 17 +++ apps/maxkb/settings/base.py | 3 +- apps/system_manage/__init__.py | 0 apps/system_manage/admin.py | 3 + apps/system_manage/apps.py | 6 + apps/system_manage/migrations/0001_initial.py | 33 +++++ apps/system_manage/migrations/__init__.py | 0 apps/system_manage/models/__init__.py | 9 ++ .../models/workspace_user_permission.py | 47 +++++++ apps/system_manage/tests.py | 3 + apps/system_manage/views/__init__.py | 8 ++ apps/users/serializers/login.py | 7 +- 15 files changed, 268 insertions(+), 62 deletions(-) create mode 100644 apps/system_manage/__init__.py create mode 100644 apps/system_manage/admin.py create mode 100644 apps/system_manage/apps.py create mode 100644 apps/system_manage/migrations/0001_initial.py create mode 100644 apps/system_manage/migrations/__init__.py create mode 100644 apps/system_manage/models/__init__.py create mode 100644 apps/system_manage/models/workspace_user_permission.py create mode 100644 apps/system_manage/tests.py create mode 100644 apps/system_manage/views/__init__.py diff --git a/apps/common/auth/handle/impl/user_token.py b/apps/common/auth/handle/impl/user_token.py index 570746ad0fd..fc4c02cdc36 100644 --- a/apps/common/auth/handle/impl/user_token.py +++ b/apps/common/auth/handle/impl/user_token.py @@ -6,26 +6,56 @@ @date:2024/3/14 03:02 @desc: 用户认证 """ +import datetime +from functools import reduce + from django.core.cache import cache from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ from common.auth.handle.auth_base_handle import AuthBaseHandle from common.constants.cache_version import Cache_Version -from common.constants.permission_constants import Auth, RoleConstants, get_default_permission_list_by_role +from common.constants.permission_constants import Auth, RoleConstants, get_default_permission_list_by_role, \ + PermissionConstants from common.database_model_manage.database_model_manage import DatabaseModelManage from common.exception.app_exception import AppAuthenticationFailed +from common.utils.common import group_by +from system_manage.models.workspace_user_permission import WorkspaceUserPermission from users.models import User -def get_permission_list(user_id, - workspace_id, +def get_permission(permission_id): + if isinstance(permission_id, PermissionConstants): + permission_id = permission_id.value + return f"{permission_id}" + + +def get_workspace_permission(permission_id, workspace_id): + if isinstance(permission_id, PermissionConstants): + permission_id = permission_id.value + return f"{permission_id}:/WORKSPACE/{workspace_id}" + + +def get_workspace_resource_permission_list(permission_id, workspace_id, workspace_user_permission_dict): + workspace_user_permission_list = workspace_user_permission_dict.get(workspace_id) + if workspace_user_permission_list is None: + return [ + get_workspace_permission(permission_id, workspace_id), get_permission(permission_id)] + return [ + f"{permission_id}:/WORKSPACE/{workspace_id}/{workspace_user_permission.auth_target_type}/{workspace_user_permission.taget}" + for workspace_user_permission in + workspace_user_permission_list if workspace_user_permission.is_auth] + [ + get_workspace_permission(permission_id, workspace_id), get_permission(permission_id)] + + +def get_permission_list(user, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model): - version, get_key = Cache_Version.PERMISSION_LIST.value - key = get_key(user_id, workspace_id) + user_id = user.id + version = Cache_Version.PERMISSION_LIST.get_version() + key = Cache_Version.PERMISSION_LIST.get_key(user_id=user_id) # 获取权限列表 is_query_model = workspace_user_role_mapping_model is not None and workspace_model is not None and role_model is not None and role_permission_mapping_model is not None permission_list = cache.get(key, version=version) @@ -37,44 +67,51 @@ def get_permission_list(user_id, role_permission_mapping_list = QuerySet(role_permission_mapping_model).filter( role_id__in=[workspace_user_role_mapping.role_id for workspace_user_role_mapping in workspace_user_role_mapping_list]) - permission_list = [role_model.id for role_model in role_permission_mapping_list] + role_dict = group_by(role_permission_mapping_list, lambda item: item.get('role_id')) + + workspace_user_permission_list = QuerySet(WorkspaceUserPermission).filter( + workspace_id__in=[workspace_user_role.workspace_id for workspace_user_role in + workspace_user_role_mapping_list]) + workspace_user_permission_dict = group_by(workspace_user_permission_list, + key=lambda item: item.workspace_id) + permission_list = [ + get_workspace_resource_permission_list(role_permission_mapping.permission_id, + role_dict.get(role_permission_mapping.role_id).workspace_id, + workspace_user_permission_dict) + for role_permission_mapping in + role_permission_mapping_list] + + # 将二维数组扁平为一维 + permission_list = reduce(lambda x, y: [*x, *y], permission_list, []) cache.set(key, permission_list, version=version) else: - permission_list = get_default_permission_list_by_role(RoleConstants.ADMIN) + workspace_id_list = ['default'] + workspace_user_permission_list = QuerySet(WorkspaceUserPermission).filter( + workspace_id__in=workspace_id_list) + + workspace_user_permission_dict = group_by(workspace_user_permission_list, + key=lambda item: item.workspace_id) + permission_list = get_default_permission_list_by_role(RoleConstants[user.role]) + permission_list = [ + get_workspace_resource_permission_list(permission, 'default', workspace_user_permission_dict) for + permission + in permission_list] + # 将二维数组扁平为一维 + permission_list = reduce(lambda x, y: [*x, *y], permission_list, []) cache.set(key, permission_list, version=version) return permission_list -def get_workspace_list(user_id, - workspace_id, - workspace_user_role_mapping_model, - workspace_model, - role_model, - role_permission_mapping_model): - version, get_key = Cache_Version.WORKSPACE_LIST.value - key = get_key(user_id) - workspace_list = cache.get(key, version=version) - # 获取权限列表 - is_query_model = workspace_user_role_mapping_model is not None and workspace_model is not None and role_model is not None and role_permission_mapping_model is not None - if workspace_list is None: - if is_query_model: - # 获取工作空间 用户 角色映射数据 - workspace_user_role_mapping_list = QuerySet(workspace_user_role_mapping_model).filter(user_id=user_id) - cache.set(key, [workspace_user_role_mapping.workspace_id for workspace_user_role_mapping in - workspace_user_role_mapping_list], version=version) - else: - return ["default"] - return workspace_list - - def get_role_list(user, - workspace_id, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model): - version, get_key = Cache_Version.ROLE_LIST.value - key = get_key(user.id, workspace_id) + """ + 获取当前用户的角色列表 + """ + version = Cache_Version.ROLE_LIST.get_version() + key = Cache_Version.ROLE_LIST.get_key(user_id=user.id) workspace_list = cache.get(key, version=version) # 获取权限列表 is_query_model = workspace_user_role_mapping_model is not None and workspace_model is not None and role_model is not None and role_permission_mapping_model is not None @@ -82,26 +119,28 @@ def get_role_list(user, if is_query_model: # 获取工作空间 用户 角色映射数据 workspace_user_role_mapping_list = QuerySet(workspace_user_role_mapping_model).filter(user_id=user.id) - cache.set(key, [workspace_user_role_mapping.role_id for workspace_user_role_mapping in - workspace_user_role_mapping_list], version=version) + cache.set(key, + [f"{workspace_user_role_mapping.role_id}:/WORKSPACE/{workspace_user_role_mapping.workspace_id}" + for + workspace_user_role_mapping in + workspace_user_role_mapping_list] + [user.role], version=version) else: cache.set(key, [user.role], version=version) return [user.role] return workspace_list -def get_auth(user, workspace_id): +def get_auth(user): workspace_user_role_mapping_model = DatabaseModelManage.get_model("workspace_user_role_mapping") workspace_model = DatabaseModelManage.get_model("workspace_model") role_model = DatabaseModelManage.get_model("role_model") role_permission_mapping_model = DatabaseModelManage.get_model("role_permission_mapping_model") - workspace_list = get_workspace_list(user.id, workspace_id, workspace_user_role_mapping_model, workspace_model, - role_model, role_permission_mapping_model) - permission_list = get_permission_list(user.id, workspace_id, workspace_user_role_mapping_model, workspace_model, + + permission_list = get_permission_list(user, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model) - role_list = get_role_list(user, workspace_id, workspace_user_role_mapping_model, workspace_model, + role_list = get_role_list(user, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model) - return Auth(workspace_list, workspace_id, role_list, permission_list) + return Auth(role_list, permission_list) class UserToken(AuthBaseHandle): @@ -117,8 +156,7 @@ def handle(self, request, token: str, get_token_details): if cache_token is None: raise AppAuthenticationFailed(1002, _('Login expired')) auth_details = get_token_details() - # 当前工作空间 - current_workspace = auth_details['current_workspace'] + cache.touch(token, timeout=datetime.timedelta(seconds=60 * 60 * 2).seconds, version=version) user = QuerySet(User).get(id=auth_details['id']) - auth = get_auth(user, current_workspace) + auth = get_auth(user) return user, auth diff --git a/apps/common/constants/cache_version.py b/apps/common/constants/cache_version.py index 0fdc48f31de..b4a5a7b6ae0 100644 --- a/apps/common/constants/cache_version.py +++ b/apps/common/constants/cache_version.py @@ -16,10 +16,16 @@ class Cache_Version(Enum): WORKSPACE_LIST = "WORKSPACE::LIST", lambda user_id: user_id # 用户数据 USER = "USER", lambda user_id: user_id - # 当前用户在当前工作空间的角色列表+本身的角色 - ROLE_LIST = "ROLE::LIST", lambda user_id, workspace_id: f"{user_id}::{workspace_id}" - # 当前用户在当前工作空间的权限列表+本身的权限列表 - PERMISSION_LIST = "PERMISSION::LIST", lambda user_id, workspace_id: f"{user_id}::{workspace_id}" + # 当前用户所有的角色 + ROLE_LIST = "ROLE::LIST", lambda user_id: user_id + # 当前用户所有权限 + PERMISSION_LIST = "PERMISSION::LIST", lambda user_id: user_id + def get_version(self): + return self.value[0] -version, get_key = Cache_Version.TOKEN.value + def get_key_func(self): + return self.value[1] + + def get_key(self, **kwargs): + return self.value[1](**kwargs) diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index e4f2c7582cb..c8d45d2e66c 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -15,6 +15,10 @@ class Group(Enum): """ USER = "USER" + APPLICATION = "APPLICATION" + + KNOWLEDGE = "KNOWLEDGE" + class Operate(Enum): """ @@ -38,10 +42,18 @@ class RoleGroup(Enum): class Role: - def __init__(self, name: str, decs: str, group: RoleGroup): + def __init__(self, name: str, decs: str, group: RoleGroup, resource_path=None): self.name = name self.decs = decs self.group = group + self.resource_path = resource_path + + def __str__(self): + return self.name + ( + (":" + self.resource_path) if self.resource_path is not None else '') + + def __eq__(self, other): + return str(self) == str(other) class RoleConstants(Enum): @@ -49,18 +61,25 @@ class RoleConstants(Enum): WORKSPACE_MANAGE = Role("WORKSPACE_MANAGE", '工作空间管理员', RoleGroup.SYSTEM_USER) USER = Role("USER", '普通用户', RoleGroup.SYSTEM_USER) + def get_workspace_role(self): + return lambda r, kwargs: Role(name=self.value.name, + decs=self.value.decs, + group=self.value.group, + resource_path= + f"/WORKSPACE/{kwargs.get('workspace_id')}") + class Permission: """ 权限信息 """ - def __init__(self, group: Group, operate: Operate, dynamic_tag=None, role_list=None): + def __init__(self, group: Group, operate: Operate, resource_path=None, role_list=None): if role_list is None: role_list = [] self.group = group self.operate = operate - self.dynamic_tag = dynamic_tag + self.resource_path = resource_path # 用于获取角色与权限的关系,只适用于没有权限管理的 self.role_list = role_list @@ -76,7 +95,7 @@ def new_instance(permission_str: str): def __str__(self): return self.group.value + ":" + self.operate.value + ( - (":" + self.dynamic_tag) if self.dynamic_tag is not None else '') + (":" + self.resource_path) if self.resource_path is not None else '') def __eq__(self, other): return str(self) == str(other) @@ -91,6 +110,27 @@ class PermissionConstants(Enum): USER_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN]) USER_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN]) + def get_workspace_application_permission(self): + return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate, + resource_path= + f"/WORKSPACE/{kwargs.get('workspace_id')}/APPLICATION/{kwargs.get('application_id')}") + + def get_workspace_knowledge_permission(self): + return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate, + resource_path= + f"/WORKSPACE/{kwargs.get('workspace_id')}/KNOWLEDGE/{kwargs.get('knowledge_id')}") + + def get_workspace_permission(self): + return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate, + resource_path= + f"/WORKSPACE/{kwargs.get('workspace_id')}") + + def __eq__(self, other): + if isinstance(other, PermissionConstants): + return other == self + else: + return self.value == other + def get_default_permission_list_by_role(role: RoleConstants): """ @@ -109,15 +149,9 @@ class Auth: """ def __init__(self, - work_space_list: List, - current_workspace, current_role_list: List[Role], permission_list: List[PermissionConstants | Permission], **keywords): - # 当前用户所有工作空间 - self.work_space_list = work_space_list - # 当前工作空间 - self.current_workspace = current_workspace # 当前工作空间的所有权限+非工作空间权限 self.permission_list = permission_list # 当前工作空间角色列表 diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index e8c51035324..7cd6be01dfd 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -7,6 +7,7 @@ @desc: """ import hashlib +from typing import List def password_encrypt(row_password): @@ -19,3 +20,19 @@ def password_encrypt(row_password): md5.update(row_password.encode()) # 3,对字符串的字节类型加密 result = md5.hexdigest() # 4,加密 return result + + +def group_by(list_source: List, key): + """ + 將數組分組 + :param list_source: 需要分組的數組 + :param key: 分組函數 + :return: key->[] + """ + result = {} + for e in list_source: + k = key(e) + array = result.get(k) if k in result else [] + array.append(e) + result[k] = array + return result diff --git a/apps/maxkb/settings/base.py b/apps/maxkb/settings/base.py index e38468c12d8..43d3d0ad327 100644 --- a/apps/maxkb/settings/base.py +++ b/apps/maxkb/settings/base.py @@ -39,7 +39,8 @@ 'drf_spectacular', 'drf_spectacular_sidecar', 'users.apps.UsersConfig', - 'common' + 'common', + 'system_manage' ] MIDDLEWARE = [ diff --git a/apps/system_manage/__init__.py b/apps/system_manage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/system_manage/admin.py b/apps/system_manage/admin.py new file mode 100644 index 00000000000..8c38f3f3dad --- /dev/null +++ b/apps/system_manage/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/system_manage/apps.py b/apps/system_manage/apps.py new file mode 100644 index 00000000000..30fdfcaad34 --- /dev/null +++ b/apps/system_manage/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SystemManageConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'system_manage' diff --git a/apps/system_manage/migrations/0001_initial.py b/apps/system_manage/migrations/0001_initial.py new file mode 100644 index 00000000000..099465e23d0 --- /dev/null +++ b/apps/system_manage/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2 on 2025-04-16 11:12 + +import django.db.models.deletion +import uuid_utils.compat +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='WorkspaceUserPermission', + fields=[ + ('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), + ('workspace_id', models.CharField(default='default', max_length=128, verbose_name='工作空间id')), + ('auth_target_type', models.CharField(choices=[('KNOWLEDGE', '知识库'), ('APPLICATION', '应用')], default='KNOWLEDGE', max_length=128, verbose_name='授权目标')), + ('target', models.UUIDField(verbose_name='知识库/应用id')), + ('is_auth', models.BooleanField(default=False, verbose_name='是否授权')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user', verbose_name='工作空间下的用户')), + ], + options={ + 'db_table': 'workspace_user_permission', + }, + ), + ] diff --git a/apps/system_manage/migrations/__init__.py b/apps/system_manage/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/system_manage/models/__init__.py b/apps/system_manage/models/__init__.py new file mode 100644 index 00000000000..415c04f3d0b --- /dev/null +++ b/apps/system_manage/models/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/4/16 18:23 + @desc: +""" +from .workspace_user_permission import * diff --git a/apps/system_manage/models/workspace_user_permission.py b/apps/system_manage/models/workspace_user_permission.py new file mode 100644 index 00000000000..a614f7d302f --- /dev/null +++ b/apps/system_manage/models/workspace_user_permission.py @@ -0,0 +1,47 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: workspace_permission.py + @date:2025/4/16 18:25 + @desc: +""" + +import uuid_utils.compat as uuid +from django.db import models + +from common.constants.permission_constants import Group +from users.models import User + + +class AuthTargetType(models.TextChoices): + """授权目标""" + KNOWLEDGE = Group.KNOWLEDGE.value, '知识库' + APPLICATION = Group.APPLICATION.value, '应用' + + +class WorkspaceUserPermission(models.Model): + """ + 工作空间用户资源权限表 + 用于管理当前工作空间是否有权限操作 某一个应用或者知识库 + """ + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") + + workspace_id = models.CharField(max_length=128, verbose_name="工作空间id", default="default") + + user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="工作空间下的用户") + + auth_target_type = models.CharField(verbose_name='授权目标', max_length=128, choices=AuthTargetType.choices, + default=AuthTargetType.KNOWLEDGE) + # 授权的知识库或者应用的id + target = models.UUIDField(max_length=128, verbose_name="知识库/应用id") + + # 是否授权 + is_auth = models.BooleanField(default=False, verbose_name="是否授权") + + create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True) + + update_time = models.DateTimeField(verbose_name="修改时间", auto_now=True) + + class Meta: + db_table = "workspace_user_permission" diff --git a/apps/system_manage/tests.py b/apps/system_manage/tests.py new file mode 100644 index 00000000000..7ce503c2dd9 --- /dev/null +++ b/apps/system_manage/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/system_manage/views/__init__.py b/apps/system_manage/views/__init__.py new file mode 100644 index 00000000000..8a1c561f6b0 --- /dev/null +++ b/apps/system_manage/views/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/4/16 19:07 + @desc: +""" diff --git a/apps/users/serializers/login.py b/apps/users/serializers/login.py index dd178349a32..83581191697 100644 --- a/apps/users/serializers/login.py +++ b/apps/users/serializers/login.py @@ -6,6 +6,8 @@ @date:2025/4/14 11:08 @desc: """ +import datetime + from django.core import signing from django.core.cache import cache from django.db.models import QuerySet @@ -46,8 +48,7 @@ def login(instance): token = signing.dumps({'username': user.username, 'id': str(user.id), 'email': user.email, - 'type': AuthenticationType.SYSTEM_USER.value, - 'current_workspace': 'default'}) + 'type': AuthenticationType.SYSTEM_USER.value}) version, get_key = Cache_Version.TOKEN.value - cache.set(get_key(token), user, version=version) + cache.set(get_key(token), user, timeout=datetime.timedelta(seconds=60 * 60 * 2).seconds, version=version) return {'token': token} From bf9b1cf061c79e50faf122d249f7d747c6dba88f Mon Sep 17 00:00:00 2001 From: shaohuzhang1 Date: Wed, 16 Apr 2025 20:08:11 +0800 Subject: [PATCH 2/2] typos --- apps/common/auth/handle/impl/user_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/common/auth/handle/impl/user_token.py b/apps/common/auth/handle/impl/user_token.py index fc4c02cdc36..ee03b505c2d 100644 --- a/apps/common/auth/handle/impl/user_token.py +++ b/apps/common/auth/handle/impl/user_token.py @@ -42,7 +42,7 @@ def get_workspace_resource_permission_list(permission_id, workspace_id, workspac return [ get_workspace_permission(permission_id, workspace_id), get_permission(permission_id)] return [ - f"{permission_id}:/WORKSPACE/{workspace_id}/{workspace_user_permission.auth_target_type}/{workspace_user_permission.taget}" + f"{permission_id}:/WORKSPACE/{workspace_id}/{workspace_user_permission.auth_target_type}/{workspace_user_permission.target}" for workspace_user_permission in workspace_user_permission_list if workspace_user_permission.is_auth] + [ get_workspace_permission(permission_id, workspace_id), get_permission(permission_id)]