From ac193f4e2479d8eb804d50d3c1414f5dbeec9b63 Mon Sep 17 00:00:00 2001 From: fanhe Date: Wed, 11 May 2022 10:59:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E4=B8=AD=E9=97=B4=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- arkid/core/approve_request_middleware.py | 100 ++++++ .../0009_approveaction_approverequest.py | 55 ++++ arkid/core/models.py | 295 ++++++++++++------ arkid/settings.py | 1 + 4 files changed, 362 insertions(+), 89 deletions(-) create mode 100644 arkid/core/approve_request_middleware.py create mode 100644 arkid/core/migrations/0009_approveaction_approverequest.py diff --git a/arkid/core/approve_request_middleware.py b/arkid/core/approve_request_middleware.py new file mode 100644 index 000000000..4de1b9672 --- /dev/null +++ b/arkid/core/approve_request_middleware.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +from os import environ +import re +import io +import json +from django.urls import resolve +from arkid.core.models import Tenant +from django.http import HttpResponse +from arkid.core.models import ApproveAction, ApproveRequest +from arkid.core.models import ExpiringToken +from django.core.handlers.wsgi import WSGIRequest + + +class ApproveRequestMiddleware: + def __init__(self, get_response): + self.get_response = get_response + # One-time configuration and initialization. + + def __call__(self, request): + # Code to be executed for each request before + # the view (and later middleware) are called. + + tenant = request.tenant + path = request.path + method = request.method + + user = self.get_user(request) + approve_action = ApproveAction.valid_objects.filter( + tenant=tenant, path=path, method=method + ).first() + if not user or not approve_action: + response = self.get_response(request) + return response + + approve_request = ApproveRequest.valid_objects.filter( + action=approve_action, user=user + ).first() + if not approve_request: + environ = request.environ + environ.pop("wsgi.input") + environ.pop("wsgi.errors") + environ.pop("wsgi.file_wrapper") + approve_request = ApproveRequest.valid_objects.create( + action=approve_action, + user=user, + environ=environ, + body=request.body, + ) + response = HttpResponse(status=401) + return response + else: + if approve_request.status != "pass": + response = HttpResponse(status=401) + return response + else: + response = self.get_response(request) + return response + # 测试如果通过后,重新执行ninja function view + # self.restore_request(approve_request) + + # Code to be executed for each request/response after + # the view is called. + # + + def process_view(self, request, view_func, view_args, view_kwargs): + return None + + def get_user(self, request): + auth_header = request.headers.get("Authorization") + if not auth_header: + return None + token = auth_header.split(" ")[1] + try: + token = ExpiringToken.objects.get(token=token) + + if not token.user.is_active: + return None + + if token.expired(request.tenant): + return None + + except ExpiringToken.DoesNotExist: + return None + except Exception as err: + return None + + return token.user + + def restore_request(self, approve_request): + environ = approve_request.environ + body = approve_request.body + environ["wsgi.input"] = io.BytesIO(body) + request = WSGIRequest(environ) + request.tenant = approve_request.action.tenant + request.user = approve_request.user + view_func, args, kwargs = resolve(request.path) + klass = view_func.__self__ + operation, _ = klass._find_operation(request) + response = operation.run(request, **kwargs) + print(response) diff --git a/arkid/core/migrations/0009_approveaction_approverequest.py b/arkid/core/migrations/0009_approveaction_approverequest.py new file mode 100644 index 000000000..c50a540c5 --- /dev/null +++ b/arkid/core/migrations/0009_approveaction_approverequest.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2.13 on 2022-05-10 09:47 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('extension', '0005_tenantextension_type'), + ('core', '0008_delete_api'), + ] + + operations = [ + migrations.CreateModel( + name='ApproveAction', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True, verbose_name='UUID')), + ('is_del', models.BooleanField(default=False, verbose_name='是否删除')), + ('is_active', models.BooleanField(default=True, verbose_name='是否可用')), + ('updated', models.DateTimeField(auto_now=True, null=True, verbose_name='更新时间')), + ('created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('path', models.CharField(max_length=100, verbose_name='Request Path')), + ('method', models.CharField(max_length=50, verbose_name='Request Method')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description')), + ('extension', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approve_action_set', related_query_name='actions', to='extension.extension', verbose_name='Extension')), + ('tenant', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='approve_action_set', related_query_name='actions', to='core.tenant', verbose_name='Tenant')), + ], + options={ + 'verbose_name': 'Approve', + 'verbose_name_plural': 'Approve', + }, + ), + migrations.CreateModel( + name='ApproveRequest', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True, verbose_name='UUID')), + ('is_del', models.BooleanField(default=False, verbose_name='是否删除')), + ('is_active', models.BooleanField(default=True, verbose_name='是否可用')), + ('updated', models.DateTimeField(auto_now=True, null=True, verbose_name='更新时间')), + ('created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')), + ('environ', models.JSONField(verbose_name='Request Environ')), + ('body', models.BinaryField(verbose_name='Request Body')), + ('status', models.CharField(choices=[('wait', 'Wait'), ('pass', 'Pass'), ('deny', 'Deny')], default='wait', max_length=100, verbose_name='Status')), + ('action', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='approve_request_set', related_query_name='requests', to='core.approveaction', verbose_name='Request Action')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approve_request_set', related_query_name='requests', to='core.user', verbose_name='User')), + ], + options={ + 'verbose_name': 'Approve', + 'verbose_name_plural': 'Approve', + }, + ), + ] diff --git a/arkid/core/models.py b/arkid/core/models.py index 36accb2d2..e4ef9b87f 100644 --- a/arkid/core/models.py +++ b/arkid/core/models.py @@ -1,11 +1,13 @@ import datetime +from os import environ from django.db import models from django.utils import timezone from arkid.common.model import BaseModel from arkid.core.translation import gettext_default as _ + # from oauth2_provider.generators import generate_client_secret from arkid.core.expand import ExpandManager, ExpandModel -from arkid.extension.models import TenantExtensionConfig +from arkid.extension.models import TenantExtensionConfig, Extension from arkid.core.token import generate_token @@ -14,7 +16,6 @@ class EmptyModel(models.Model): class Tenant(BaseModel): - class Meta(object): verbose_name = _("tenant", "租户") verbose_name_plural = _("tenant", "租户") @@ -35,19 +36,18 @@ def __str__(self) -> str: class User(BaseModel, ExpandModel): - class Meta(object): verbose_name = _("user", "用户") verbose_name_plural = _("user", "用户") unique_together = [['username', 'tenant']] username = models.CharField(max_length=128, blank=False) - avatar = models.URLField(verbose_name=_('Avatar','头像'), blank=True) - is_platform_user = models.BooleanField(default=False, verbose_name=_('is platform user','是否是平台用户')) - - tenant = models.ForeignKey( - 'Tenant', blank=False, on_delete=models.PROTECT + avatar = models.URLField(verbose_name=_('Avatar', '头像'), blank=True) + is_platform_user = models.BooleanField( + default=False, verbose_name=_('is platform user', '是否是平台用户') ) + + tenant = models.ForeignKey('Tenant', blank=False, on_delete=models.PROTECT) scim_external_id = models.CharField(max_length=128, blank=True, null=True) # tenants = models.ManyToManyField( @@ -59,14 +59,11 @@ class Meta(object): class UserGroup(BaseModel, ExpandModel): - class Meta(object): - verbose_name = _("User Group","用户分组") - verbose_name_plural = _("User Group","用户分组") + verbose_name = _("User Group", "用户分组") + verbose_name_plural = _("User Group", "用户分组") - tenant = models.ForeignKey( - 'Tenant', blank=False, on_delete=models.PROTECT - ) + tenant = models.ForeignKey('Tenant', blank=False, on_delete=models.PROTECT) name = models.CharField(max_length=128, blank=False) parent = models.ForeignKey( 'UserGroup', @@ -80,13 +77,18 @@ class Meta(object): blank=True, related_name="user_set", related_query_name="user", - verbose_name=_('User List','用户列表') + verbose_name=_('User List', '用户列表'), ) permission = models.ForeignKey( - 'SystemPermission', blank=True, null=True, default=None,on_delete=models.PROTECT + 'SystemPermission', + blank=True, + null=True, + default=None, + on_delete=models.PROTECT, ) scim_external_id = models.CharField(max_length=128, blank=True, null=True) + def __str__(self) -> str: return f'{self.name}' @@ -96,40 +98,48 @@ def children(self): class App(BaseModel, ExpandModel): - class Meta(object): - verbose_name = _("APP","应用") + verbose_name = _("APP", "应用") verbose_name_plural = _("APP", "应用") - tenant = models.ForeignKey( - 'Tenant', blank=False, on_delete=models.PROTECT + tenant = models.ForeignKey('Tenant', blank=False, on_delete=models.PROTECT) + name = models.CharField(max_length=128, verbose_name=_('name', '名称')) + url = models.CharField(max_length=1024, blank=True, verbose_name=_('url', '地址')) + logo = models.CharField( + max_length=1024, blank=True, null=True, default='', verbose_name=_('logo', '图标') + ) + description = models.TextField( + blank=True, null=True, verbose_name=_('description', '描述') ) - name = models.CharField(max_length=128, verbose_name=_('name','名称')) - url = models.CharField(max_length=1024, blank=True, verbose_name=_('url','地址')) - logo = models.CharField(max_length=1024, blank=True, null=True, default='', verbose_name=_('logo','图标')) - description = models.TextField(blank=True, null=True, verbose_name=_('description','描述')) - type = models.CharField(max_length=128, default='', verbose_name=_('type','类型')) + type = models.CharField(max_length=128, default='', verbose_name=_('type', '类型')) secret = models.CharField( - max_length=255, blank=True, null=True, default='', verbose_name=_('secret','密钥') + max_length=255, + blank=True, + null=True, + default='', + verbose_name=_('secret', '密钥'), ) config = models.OneToOneField( TenantExtensionConfig, blank=False, default=None, on_delete=models.PROTECT ) - package = models.CharField(max_length=128, blank=True, null=True, default='', verbose_name=_('package','包名')) + package = models.CharField( + max_length=128, + blank=True, + null=True, + default='', + verbose_name=_('package', '包名'), + ) def __str__(self) -> str: return f'Tenant: {self.tenant.name}, App: {self.name}' class AppGroup(BaseModel, ExpandModel): - class Meta(object): - verbose_name = _("APP Group","应用分组") - verbose_name_plural = _("APP Group","应用分组") + verbose_name = _("APP Group", "应用分组") + verbose_name_plural = _("APP Group", "应用分组") - tenant = models.ForeignKey( - 'Tenant', blank=False, on_delete=models.PROTECT - ) + tenant = models.ForeignKey('Tenant', blank=False, on_delete=models.PROTECT) name = models.CharField(max_length=128, blank=False) parent = models.ForeignKey( 'AppGroup', @@ -143,7 +153,7 @@ class Meta(object): blank=True, related_name="app_set", related_query_name="app", - verbose_name=_('APP List', '应用列表') + verbose_name=_('APP List', '应用列表'), ) def __str__(self) -> str: @@ -155,61 +165,68 @@ def children(self): class PermissionAbstract(BaseModel, ExpandModel): - class Meta(object): abstract = True CATEGORY_CHOICES = ( - ('entry', _('entry','入口')), - ('api', _('API','接口')), - ('data', _('data','数据')), - ('group', _('group','分组')), - ('ui', _('UI','界面')), - ('other', _('other','其它')), + ('entry', _('entry', '入口')), + ('api', _('API', '接口')), + ('data', _('data', '数据')), + ('group', _('group', '分组')), + ('ui', _('UI', '界面')), + ('other', _('other', '其它')), ) - name = models.CharField(verbose_name=_('Name','名称'), max_length=255) - code = models.CharField(verbose_name=_('Code','编码'), max_length=100) + name = models.CharField(verbose_name=_('Name', '名称'), max_length=255) + code = models.CharField(verbose_name=_('Code', '编码'), max_length=100) tenant = models.ForeignKey( 'Tenant', default=None, null=True, blank=True, on_delete=models.PROTECT, - verbose_name=_('Tenant','租户') + verbose_name=_('Tenant', '租户'), ) - + category = models.CharField( choices=CATEGORY_CHOICES, default="other", max_length=100, - verbose_name=_('category',"类型"), + verbose_name=_('category', "类型"), ) is_system = models.BooleanField( - default=True, - verbose_name=_('System Permission','是否是系统权限') + default=True, verbose_name=_('System Permission', '是否是系统权限') + ) + operation_id = models.CharField( + verbose_name=_('Operation ID', '操作id'), + max_length=255, + blank=True, + null=True, + default='', + ) + describe = models.JSONField( + blank=True, default=dict, verbose_name=_('describe', '描述') ) - operation_id = models.CharField(verbose_name=_('Operation ID','操作id'), max_length=255, blank=True, null=True, default='') - describe = models.JSONField(blank=True, default=dict, verbose_name=_('describe', '描述')) def __str__(self): return '%s' % (self.name) class SystemPermission(PermissionAbstract): - class Meta(object): verbose_name = _('SystemPermission', '系统权限') verbose_name_plural = _('SystemPermission', '系统权限') - sort_id = models.IntegerField(verbose_name=_('Sort ID', '序号'), default=0, auto_created=True) + sort_id = models.IntegerField( + verbose_name=_('Sort ID', '序号'), default=0, auto_created=True + ) container = models.ManyToManyField( 'SystemPermission', blank=True, related_name="system_permission_set", related_query_name="system_permission", - verbose_name=_('SystemPermission List','包含的系统权限') + verbose_name=_('SystemPermission List', '包含的系统权限'), ) parent = models.ForeignKey( 'SystemPermission', @@ -217,7 +234,7 @@ class Meta(object): blank=True, on_delete=models.PROTECT, related_name='children', - verbose_name=_('Parent', '父权限分组') + verbose_name=_('Parent', '父权限分组'), ) @property @@ -226,7 +243,6 @@ def children(self): class Permission(PermissionAbstract): - class Meta(object): verbose_name = _("Permission", "权限") verbose_name_plural = _("Permission", "权限") @@ -237,7 +253,7 @@ class Meta(object): default=None, null=True, blank=True, - verbose_name=_('APP','应用') + verbose_name=_('APP', '应用'), ) parent = models.ForeignKey( 'Permission', @@ -245,14 +261,14 @@ class Meta(object): blank=True, on_delete=models.PROTECT, related_name='children', - verbose_name=_('Parent', '父权限分组') + verbose_name=_('Parent', '父权限分组'), ) container = models.ManyToManyField( 'Permission', blank=True, related_name="permission_set", related_query_name="permission", - verbose_name=_('Permission List','权限列表') + verbose_name=_('Permission List', '权限列表'), ) def __str__(self) -> str: @@ -264,15 +280,23 @@ def children(self): class UserPermissionResult(BaseModel, ExpandModel): - class Meta(object): verbose_name = _("UserPermissionResult", "用户权限结果") verbose_name_plural = _("UserPermissionResult", "用户权限结果") user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') tenant = models.ForeignKey(Tenant, on_delete=models.PROTECT, verbose_name='租户') - app = models.ForeignKey(App, default=None, null=True, blank=True, on_delete=models.PROTECT, verbose_name='App') - result = models.CharField(max_length=1024, blank=True, null=True, verbose_name='权限结果') + app = models.ForeignKey( + App, + default=None, + null=True, + blank=True, + on_delete=models.PROTECT, + verbose_name='App', + ) + result = models.CharField( + max_length=1024, blank=True, null=True, verbose_name='权限结果' + ) is_update = models.BooleanField(default=False, verbose_name='是否更新') def __str__(self) -> str: @@ -280,22 +304,23 @@ def __str__(self) -> str: class Approve(BaseModel, ExpandModel): - class Meta(object): - verbose_name = _('Approve',"审批动作") - verbose_name_plural = _('Approve',"审批动作") + verbose_name = _('Approve', "审批动作") + verbose_name_plural = _('Approve', "审批动作") STATUS_CHOICES = ( - ('wait', _('Wait','待审批')), - ('pass', _('Pass','通过')), - ('deny', _('Deny','拒绝')), + ('wait', _('Wait', '待审批')), + ('pass', _('Pass', '通过')), + ('deny', _('Deny', '拒绝')), ) - name = models.CharField(verbose_name=_('Name','名称'), max_length=255) - code = models.CharField(verbose_name=_('Code','编码'), max_length=100) - description = models.TextField(blank=True, null=True, verbose_name=_('Description','备注')) + name = models.CharField(verbose_name=_('Name', '名称'), max_length=255) + code = models.CharField(verbose_name=_('Code', '编码'), max_length=100) + description = models.TextField( + blank=True, null=True, verbose_name=_('Description', '备注') + ) tenant = models.ForeignKey( - 'Tenant', default=None, on_delete=models.PROTECT, verbose_name=_('Tenant','租户') + 'Tenant', default=None, on_delete=models.PROTECT, verbose_name=_('Tenant', '租户') ) app = models.ForeignKey( App, @@ -303,17 +328,17 @@ class Meta(object): default=None, null=True, blank=True, - verbose_name=_('APP','应用') + verbose_name=_('APP', '应用'), ) status = models.CharField( choices=STATUS_CHOICES, default="wait", max_length=100, - verbose_name=_('Status',"状态"), + verbose_name=_('Status', "状态"), ) data = models.JSONField( default=dict, - verbose_name=_('Data',"数据"), + verbose_name=_('Data', "数据"), ) def __str__(self): @@ -321,18 +346,26 @@ def __str__(self): class ExpiringToken(models.Model): - class Meta(object): - verbose_name = _("Token","秘钥") - verbose_name_plural = _("Token","秘钥") + verbose_name = _("Token", "秘钥") + verbose_name_plural = _("Token", "秘钥") user = models.OneToOneField( - 'User', related_name='auth_token', primary_key=True, - on_delete=models.CASCADE, verbose_name=_("User",'用户') + 'User', + related_name='auth_token', + primary_key=True, + on_delete=models.CASCADE, + verbose_name=_("User", '用户'), + ) + token = models.CharField( + _("Token", '秘钥'), + max_length=40, + unique=True, + db_index=True, + default=generate_token, ) - token = models.CharField(_("Token",'秘钥'), max_length=40, unique=True, db_index=True, default=generate_token) - created = models.DateTimeField(_("Created",'创建时间'), auto_now_add=True) - + created = models.DateTimeField(_("Created", '创建时间'), auto_now_add=True) + def expired(self, tenant): """Return boolean indicating token expiration.""" now = timezone.now() @@ -340,7 +373,7 @@ def expired(self, tenant): if config: token_duration_minutes = config.token_duration_minutes else: - token_duration_minutes = 24*60 + token_duration_minutes = 24 * 60 if self.created < now - datetime.timedelta(minutes=token_duration_minutes): return True return False @@ -350,13 +383,18 @@ def __str__(self): class TenantConfig(BaseModel, ExpandModel): - class Meta(object): - verbose_name = _('Tenant Config',"租户配置") - verbose_name_plural = _('Tenant Config',"租户配置") + verbose_name = _('Tenant Config', "租户配置") + verbose_name_plural = _('Tenant Config', "租户配置") - tenant = models.ForeignKey('Tenant', blank=False, on_delete=models.PROTECT, verbose_name=_('Tenant','租户')) - token_duration_minutes = models.IntegerField(blank=False, default=24*60, verbose_name=_('Token Duration Minutes','Token有效时长(分钟)')) + tenant = models.ForeignKey( + 'Tenant', blank=False, on_delete=models.PROTECT, verbose_name=_('Tenant', '租户') + ) + token_duration_minutes = models.IntegerField( + blank=False, + default=24 * 60, + verbose_name=_('Token Duration Minutes', 'Token有效时长(分钟)'), + ) # from .models import User @@ -377,4 +415,83 @@ class Meta(object): # if action == 'pre_add': # for tenant in tenants: # if User.objects.filter(username=user.username).filter(tenants=tenant): -# raise IntegrityError('User with username %s already exists for tenant %s' % (user.username, tenant)) \ No newline at end of file +# raise IntegrityError('User with username %s already exists for tenant %s' % (user.username, tenant)) + + +class ApproveAction(BaseModel, ExpandModel): + class Meta(object): + verbose_name = _('Approve', "审批动作") + verbose_name_plural = _('Approve', "审批动作") + + name = models.CharField(verbose_name=_('Name', '名称'), max_length=255) + path = models.CharField(verbose_name=_('Request Path', '请求路径'), max_length=100) + method = models.CharField(verbose_name=_('Request Method', '请求方法'), max_length=50) + description = models.TextField( + blank=True, null=True, verbose_name=_('Description', '备注') + ) + extension = models.ForeignKey( + Extension, + default=None, + null=True, + on_delete=models.SET_NULL, + verbose_name=_('Extension', '插件'), + related_name="approve_action_set", + related_query_name="actions", + ) + tenant = models.ForeignKey( + 'Tenant', + default=None, + on_delete=models.CASCADE, + verbose_name=_('Tenant', '租户'), + related_name="approve_action_set", + related_query_name="actions", + ) + + def __str__(self): + return '%s' % (self.name) + + +class ApproveRequest(BaseModel, ExpandModel): + + STATUS_CHOICES = ( + ('wait', _('Wait', '待审批')), + ('pass', _('Pass', '通过')), + ('deny', _('Deny', '拒绝')), + ) + + class Meta(object): + verbose_name = _('Approve', "审批请求") + verbose_name_plural = _('Approve', "审批请求") + + user = models.ForeignKey( + 'User', + on_delete=models.CASCADE, + verbose_name=_('User', '用户'), + related_name="approve_request_set", + related_query_name="requests", + ) + + action = models.ForeignKey( + 'ApproveAction', + default=None, + on_delete=models.CASCADE, + verbose_name=_('Request Action', '审批动作'), + related_name="approve_request_set", + related_query_name="requests", + ) + # path = models.CharField(verbose_name=_('Request Path','请求路径'), max_length=100) + # method = models.CharField(verbose_name=_('Request Method','请求方法'), max_length=50) + environ = models.JSONField(verbose_name=_('Request Environ', '请求环境变量')) + body = models.BinaryField(verbose_name=_('Request Body', '请求负载')) + + status = models.CharField( + choices=STATUS_CHOICES, + default="wait", + max_length=100, + verbose_name=_('Status', "状态"), + ) + + def __str__(self): + return ( + f'{self.action.name}:{self.action.method}:{self.action.path}:{self.status}' + ) diff --git a/arkid/settings.py b/arkid/settings.py index 5d2977191..16368431b 100644 --- a/arkid/settings.py +++ b/arkid/settings.py @@ -57,6 +57,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'arkid.core.tenant_middleware.TenantMiddleware', 'arkid.core.request_json_data_middleware.JSONMiddleware', + 'arkid.core.approve_request_middleware.ApproveRequestMiddleware', ] ROOT_URLCONF = 'arkid.urls' From 2dbb59a5b0d76533df2263c1d006dc4820685bc9 Mon Sep 17 00:00:00 2001 From: fanhe Date: Thu, 12 May 2022 10:10:06 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E7=B3=BB=E7=BB=9F=E6=8F=92=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/pages/approve_manage/__init__.py | 7 +- .../approve_manage/all_approve_requests.py | 24 +++ api/v1/views/approve_action.py | 160 +++++++++++++++--- arkid/core/approve_request_middleware.py | 25 ++- arkid/core/error.py | 3 + arkid/core/extension/approve_system.py | 112 ++++++++++++ .../migrations/0012_merge_20220511_0310.py | 14 ++ .../__init__.py | 44 +++++ .../approve_requests_page.py | 34 ++++ 9 files changed, 384 insertions(+), 39 deletions(-) create mode 100644 api/v1/pages/approve_manage/all_approve_requests.py create mode 100644 arkid/core/extension/approve_system.py create mode 100644 arkid/core/migrations/0012_merge_20220511_0310.py create mode 100644 extension_root/com_longgui_approve_system_arkid/__init__.py create mode 100644 extension_root/com_longgui_approve_system_arkid/approve_requests_page.py diff --git a/api/v1/pages/approve_manage/__init__.py b/api/v1/pages/approve_manage/__init__.py index f601233b8..71a36ce2e 100644 --- a/api/v1/pages/approve_manage/__init__.py +++ b/api/v1/pages/approve_manage/__init__.py @@ -1,4 +1,4 @@ -from . import approve_action,approve_system +from . import approve_action, approve_system, all_approve_requests from arkid.core import routers @@ -7,6 +7,7 @@ name='审批管理', children=[ approve_action.router, - approve_system.router + approve_system.router, + all_approve_requests.router, ], -) \ No newline at end of file +) diff --git a/api/v1/pages/approve_manage/all_approve_requests.py b/api/v1/pages/approve_manage/all_approve_requests.py new file mode 100644 index 000000000..0dfb2a916 --- /dev/null +++ b/api/v1/pages/approve_manage/all_approve_requests.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from arkid.core import routers, pages, actions +from arkid.core.translation import gettext_default as _ + +tag = 'all_approve_requests' +name = '审批请求-ALL' + + +page = pages.TablePage(tag=tag, name=name) + + +router = routers.FrontRouter( + path=tag, + name=name, + page=page, +) + +page.create_actions( + init_action=actions.DirectAction( + path='/api/v1/tenant/{tenant_id}/approve_requests/', + method=actions.FrontActionMethod.GET, + ), +) diff --git a/api/v1/views/approve_action.py b/api/v1/views/approve_action.py index 5d0c08d6f..94576db36 100644 --- a/api/v1/views/approve_action.py +++ b/api/v1/views/approve_action.py @@ -1,35 +1,151 @@ from arkid.core.api import api from arkid.core.translation import gettext_default as _ +from ninja import ModelSchema, Schema +from arkid.core.models import ApproveAction, ApproveRequest +from pydantic import Field +from enum import Enum +from arkid.extension.models import TenantExtensionConfig, Extension +from arkid.core.error import ErrorCode +from typing import List -@api.get("/tenant/{tenant_id}/approve_actions/", tags=["审批动作"],auth=None) +class METHOD_TYPE(str, Enum): + GET = _('GET', 'GET') + POST = _('POST', 'POST') + DELETE = _('DELETE', 'DELETE') + PUT = _('PUT', 'PUT') + + +class ApproveActionSchemaIn(Schema): + name: str = Field(title=_('Name', '名称'), default='') + description: str = Field(title=_('Description', '备注'), default='') + path: str = Field(title=_('Path', '请求路径')) + method: METHOD_TYPE = Field(title=_('Method', '请求方法')) + extension_id: str = Field(title=_('Method', '请求方法')) + + +class ApproveActionSchemaOut(ModelSchema): + class Config: + model = ApproveAction + model_fields = ['id', 'name', 'path', 'method', 'description'] + + package: str + + @staticmethod + def resolve_package(obj): + if obj.extension: + return obj.extension.package + else: + return '' + + +@api.get( + "/tenant/{tenant_id}/approve_actions/", + tags=["审批动作"], + auth=None, + response=List[ApproveActionSchemaOut], +) def get_approve_actions(request, tenant_id: str): - """ 审批动作列表,TODO - """ - return [] + """审批动作列表,TODO""" + tenant = request.tenant + actions = ApproveAction.valid_objects.filter(tenant=tenant) + return actions -@api.get(operation_id="",path="/tenant/{tenant_id}/approve_actions/{id}/", tags=["审批动作"],auth=None) + +@api.get( + operation_id="", + path="/tenant/{tenant_id}/approve_actions/{id}/", + tags=["审批动作"], + auth=None, + response=ApproveActionSchemaOut, +) def get_approve_action(request, tenant_id: str, id: str): - """ 获取审批动作,TODO - """ - return {} + """获取审批动作,TODO""" + tenant = request.tenant + action = ApproveAction.valid_objects.filter(tenant=tenant, id=id).first() + if not action: + return {'error': ErrorCode.APPROVE_ACTION_NOT_EXISTS.value} + else: + return action -@api.post("/tenant/{tenant_id}/approve_actions/", tags=["审批动作"],auth=None) -def create_approve_action(request, tenant_id: str): - """ 创建审批动作,TODO - """ - return {} -@api.put("/tenant/{tenant_id}/approve_actions/{id}/", tags=["审批动作"],auth=None) -def update_approve_action(request, tenant_id: str, id: str): - """ 编辑审批动作,TODO - """ - return {} +@api.post("/tenant/{tenant_id}/approve_actions/", tags=["审批动作"], auth=None) +def create_approve_action(request, tenant_id: str, data: ApproveActionSchemaIn): + """创建审批动作,TODO""" + tenant = request.tenant + name = data.name + description = data.description + path = data.path + method = data.method + extension_id = data.extension_id + extension = Extension.valid_objects.get(id=extension_id) + action = ApproveAction.valid_objects.filter( + path=path, method=method, extension=extension, tenant=tenant + ).first() + if action: + return {'error': ErrorCode.APPROVE_ACTION_DUPLICATED.value} + else: + action = ApproveAction.valid_objects.create( + name=name, + description=description, + path=path, + method=method, + extension=extension, + tenant=tenant, + ) + return {'error': ErrorCode.OK.value} + + +@api.put("/tenant/{tenant_id}/approve_actions/{id}/", tags=["审批动作"], auth=None) +def update_approve_action( + request, tenant_id: str, id: str, data: ApproveActionSchemaIn +): + """编辑审批动作,TODO""" + tenant = request.tenant + name = data.name + description = data.description + path = data.path + method = data.method + extension_id = data.extension_id + extension = Extension.valid_objects.get(id=extension_id) + action = ApproveAction.valid_objects.filter(tenant=tenant, id=id).first() + if not action: + return {'error': ErrorCode.APPROVE_ACTION_NOT_EXISTS.value} + else: + action.name = name + action.description = description + action.path = path + action.method = method + action.extension = extension + action.save() + return {'error': ErrorCode.OK.value} + -@api.delete("/tenant/{tenant_id}/approve_actions/{id}/", tags=["审批动作"],auth=None) +@api.delete("/tenant/{tenant_id}/approve_actions/{id}/", tags=["审批动作"], auth=None) def delete_approve_action(request, tenant_id: str, id: str): - """ 删除审批动作,TODO - """ - return {} + """删除审批动作,TODO""" + tenant = request.tenant + action = ApproveAction.valid_objects.filter(tenant=tenant, id=id).first() + if not action: + return {'error': ErrorCode.APPROVE_ACTION_NOT_EXISTS.value} + else: + action.delete() + return {'error': ErrorCode.OK.value} + + +from arkid.core.extension.approve_system import ApproveRequestOut +@api.get( + "/tenant/{tenant_id}/approve_requests/", + tags=["审批动作"], + auth=None, + response=List[ApproveRequestOut], +) +def get_tenant_approve_requests(request, tenant_id: str): + """ + 返回当前租户所有审批请求 + """ + tenant = request.tenant + requests = ApproveRequest.valid_objects.filter(action__tenant=tenant) + return requests diff --git a/arkid/core/approve_request_middleware.py b/arkid/core/approve_request_middleware.py index 4de1b9672..366694813 100644 --- a/arkid/core/approve_request_middleware.py +++ b/arkid/core/approve_request_middleware.py @@ -19,9 +19,15 @@ def __init__(self, get_response): def __call__(self, request): # Code to be executed for each request before # the view (and later middleware) are called. + response = self.get_response(request) + # Code to be executed for each request/response after + # the view is called. + return response + + def process_view(self, request, view_func, view_args, view_kwargs): tenant = request.tenant - path = request.path + path = ('/' + request.resolver_match.route).replace("<", "{").replace(">", "}") method = request.method user = self.get_user(request) @@ -29,8 +35,9 @@ def __call__(self, request): tenant=tenant, path=path, method=method ).first() if not user or not approve_action: - response = self.get_response(request) - return response + return None + if not approve_action.extension: + return None approve_request = ApproveRequest.valid_objects.filter( action=approve_action, user=user @@ -53,17 +60,7 @@ def __call__(self, request): response = HttpResponse(status=401) return response else: - response = self.get_response(request) - return response - # 测试如果通过后,重新执行ninja function view - # self.restore_request(approve_request) - - # Code to be executed for each request/response after - # the view is called. - # - - def process_view(self, request, view_func, view_args, view_kwargs): - return None + return None def get_user(self, request): auth_header = request.headers.get("Authorization") diff --git a/arkid/core/error.py b/arkid/core/error.py index 06ad9c2a5..7d6a9384d 100644 --- a/arkid/core/error.py +++ b/arkid/core/error.py @@ -47,3 +47,6 @@ class ErrorCode(Enum): EMAIL_ERROR = '12004' ADD_AUTH_TMPL_ERROR = '13001' + + APPROVE_ACTION_DUPLICATED = '14001' + APPROVE_ACTION_NOT_EXISTS = '14002' diff --git a/arkid/core/extension/approve_system.py b/arkid/core/extension/approve_system.py new file mode 100644 index 000000000..1066ea33a --- /dev/null +++ b/arkid/core/extension/approve_system.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +import io +from abc import abstractmethod +from arkid.core.extension import Extension +from arkid.core.translation import gettext_default as _ +from arkid.core.models import App, ApproveRequest +from arkid.core import api as core_api, event as core_event +from arkid.extension.models import TenantExtensionConfig, TenantExtension +from django.urls import re_path +from django.urls import resolve +from django.core.handlers.wsgi import WSGIRequest +from arkid.core.api import api +from ninja import ModelSchema +from typing import List +from ninja.pagination import paginate +from django.shortcuts import get_object_or_404 +from arkid.core.error import ErrorCode + + +class ApproveRequestOut(ModelSchema): + class Config: + model = ApproveRequest + model_fields = ['id', 'status'] + + username: str + path: str + method: str + + @staticmethod + def resolve_username(obj): + return obj.user.username + + @staticmethod + def resolve_path(obj): + return obj.action.path + + @staticmethod + def resolve_method(obj): + return obj.action.method + + +class ApproveSystemExtension(Extension): + + TYPE = "approve_system" + + composite_schema_map = {} + created_composite_schema_list = [] + composite_key = 'type' + composite_model = TenantExtension + + @property + def type(self): + return ApproveSystemExtension.TYPE + + def load(self): + @api.get( + "/tenant/{tenant_id}/approve_system_arkid/approve_requests/", + response=List[ApproveRequestOut], + tags=['审批请求'], + auth=None, + operation_id=f'{self.name}_list_approve_requests', + ) + @paginate + def approve_request_list(request, tenant_id: str): + requests = ApproveRequest.valid_objects.filter( + action__extension__type=self.type + ) + return requests + + @api.put( + "/tenant/{tenant_id}/approve_system_arkid/approve_requests/{request_id}/", + # response=ApproveRequestOut, + tags=['审批请求'], + auth=None, + operation_id=f'{self.name}_process_approve_request', + ) + def approve_request_process( + request, tenant_id: str, request_id: str, action: str = '' + ): + approve_request = get_object_or_404(ApproveRequest, id=request_id) + if action == "pass": + approve_request.status = "pass" + approve_request.save() + response = self.restore_request(approve_request) + return response + elif action == "deny": + approve_request.status = "deny" + approve_request.save() + return {'error': ErrorCode.OK.value} + + super().load() + + def register_approve_system_schema(self, schema, system_type): + self.register_config_schema(schema, self.package + '_' + system_type) + self.register_composite_config_schema( + schema, system_type, exclude=['extension'] + ) + + def restore_request(self, approve_request): + environ = approve_request.environ + body = approve_request.body + environ["wsgi.input"] = io.BytesIO(body) + request = WSGIRequest(environ) + request.tenant = approve_request.action.tenant + request.user = approve_request.user + view_func, args, kwargs = resolve(request.path) + klass = view_func.__self__ + operation, _ = klass._find_operation(request) + response = operation.run(request, **kwargs) + print(response) + return response diff --git a/arkid/core/migrations/0012_merge_20220511_0310.py b/arkid/core/migrations/0012_merge_20220511_0310.py new file mode 100644 index 000000000..cfe43ab8c --- /dev/null +++ b/arkid/core/migrations/0012_merge_20220511_0310.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2.13 on 2022-05-11 03:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_approveaction_approverequest'), + ('core', '0011_alter_systempermission_sort_id'), + ] + + operations = [ + ] diff --git a/extension_root/com_longgui_approve_system_arkid/__init__.py b/extension_root/com_longgui_approve_system_arkid/__init__.py new file mode 100644 index 000000000..0c808e62a --- /dev/null +++ b/extension_root/com_longgui_approve_system_arkid/__init__.py @@ -0,0 +1,44 @@ +from arkid.core.extension.approve_system import ApproveSystemExtension +from arkid.core.extension import create_extension_schema +from arkid.extension.models import TenantExtensionConfig, TenantExtension +import urllib.parse +from pydantic import Field +from arkid.core.translation import gettext_default as _ +from ninja import Schema +from .approve_requests_page import page, router +from api.v1.pages.approve_manage import router as approve_manage_router + +package = 'com.longgui.approve.system.arkid' + + +class ApproveSystemConfigSchema(Schema): + description: str = Field( + title=_('Arpprove System Description', '审批系统描述'), default='' + ) + + +ApproveSystemArkIDConfigSchema = create_extension_schema( + 'ApproveSystemArkIDConfigSchema', package, base_schema=ApproveSystemConfigSchema +) + + +class ApproveSystemArkIDExtension(ApproveSystemExtension): + def load(self): + # 加载url地址 + # self.load_urls() + # 加载相应的配置文件 + super().load() + self.register_approve_system_schema(ApproveSystemArkIDConfigSchema, self.type) + self.register_front_pages(page) + approve_manage_router.children.append(router) + + +extension = ApproveSystemArkIDExtension( + package=package, + description='ArkID审批系统', + version='1.0', + labels='approve-system-arkid', + homepage='https://www.longguikeji.com', + logo='', + author='hanbin@jinji-inc.com', +) diff --git a/extension_root/com_longgui_approve_system_arkid/approve_requests_page.py b/extension_root/com_longgui_approve_system_arkid/approve_requests_page.py new file mode 100644 index 000000000..03c9d092b --- /dev/null +++ b/extension_root/com_longgui_approve_system_arkid/approve_requests_page.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +from arkid.core import routers, pages, actions +from arkid.core.translation import gettext_default as _ + +tag = 'arkid_approve_requests' +name = '审批请求-ArkID' + + +page = pages.TablePage(tag=tag, name=name) + + +router = routers.FrontRouter( + path=tag, + name=name, + page=page, +) + +page.create_actions( + init_action=actions.DirectAction( + path='/api/v1/tenant/{tenant_id}/approve_system_arkid/approve_requests/', + method=actions.FrontActionMethod.GET, + ), + local_actions={ + "pass": actions.DirectAction( + path="/api/v1/tenant/{tenant_id}/approve_system_arkid/approve_requests/{id}/?action=pass", + method=actions.FrontActionMethod.PUT, + ), + "deny": actions.DirectAction( + path="/api/v1/tenant/{tenant_id}/approve_system_arkid/approve_requests/{id}/?action=pass", + method=actions.FrontActionMethod.PUT, + ), + }, +)