diff --git a/.gitignore b/.gitignore
index 386939c..84a3bfd 100755
--- a/.gitignore
+++ b/.gitignore
@@ -41,7 +41,7 @@ nosetests.xml
acl-api/test-output
# Translations
-*.mo
+#*.mo
# Mr Developer
.mr.developer.cfg
diff --git a/README.md b/README.md
index 50ac63e..b8fccab 100644
--- a/README.md
+++ b/README.md
@@ -58,5 +58,7 @@ docker-compose up -d
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
-
+
+
+
\ No newline at end of file
diff --git a/acl-api/Pipfile b/acl-api/Pipfile
index 901072e..b1ed240 100644
--- a/acl-api/Pipfile
+++ b/acl-api/Pipfile
@@ -5,8 +5,8 @@ name = "pypi"
[packages]
# Flask
-Flask = "==2.3.2"
-Werkzeug = "==2.3.6"
+Flask = "==2.2.5"
+Werkzeug = "==2.2.3"
click = ">=5.0"
# Api
Flask-RESTful = "==0.3.10"
@@ -15,6 +15,7 @@ Flask-SQLAlchemy = "==2.5.0"
SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0"
redis = "==4.6.0"
+python-redis-lock = "==4.0.0"
# Migrations
Flask-Migrate = "==2.5.2"
# Deployment
@@ -24,10 +25,13 @@ supervisor = "==4.0.3"
Flask-Login = "==0.6.2"
Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8"
-python-ldap = "==3.4.0"
+ldap3 = "==2.9.1"
pycryptodome = "==3.12.0"
+lz4 = ">=4.3.2"
# Caching
Flask-Caching = ">=1.0.0"
+# i18n
+flask-babel = "==4.0.0"
# Environment variable parsing
environs = "==4.2.0"
marshmallow = "==2.20.2"
@@ -56,6 +60,7 @@ Jinja2 = "==3.1.2"
jinja2schema = "==0.1.4"
msgpack-python = "==0.5.6"
alembic = "==1.7.7"
+python-magic = "==0.4.27"
[dev-packages]
# Testing
diff --git a/acl-api/api/app.py b/acl-api/api/app.py
index 91eb2c5..8cc1ab4 100644
--- a/acl-api/api/app.py
+++ b/acl-api/api/app.py
@@ -4,33 +4,29 @@
import decimal
import logging
import os
-import sys
from inspect import getmembers
from logging.handlers import RotatingFileHandler
+from pathlib import Path
+import sys
from flask import Flask
-from flask import make_response, jsonify
+from flask import jsonify
+from flask import make_response
+from flask import request
from flask.blueprints import Blueprint
from flask.cli import click
from flask.json.provider import DefaultJSONProvider
+from flask_babel.speaklater import LazyString
import api.views.entry
-from api.extensions import (
- bcrypt,
- cors,
- cache,
- db,
- login_manager,
- migrate,
- celery,
- rd,
-)
-from api.flask_cas import CAS
+from api.extensions import (bcrypt, babel, cache, celery, cors, db, login_manager, migrate, rd)
+from api.lib.perm.authentication.cas import CAS
+from api.lib.perm.authentication.oauth2 import OAuth2
from api.models.acl import User
HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir)
-API_PACKAGE = "api"
+BASE_DIR = Path(__file__).resolve().parent.parent
@login_manager.user_loader
@@ -76,7 +72,7 @@ def __call__(self, environ, start_response):
class MyJSONEncoder(DefaultJSONProvider):
def default(self, o):
- if isinstance(o, (decimal.Decimal, datetime.date, datetime.time)):
+ if isinstance(o, (decimal.Decimal, datetime.date, datetime.time, LazyString)):
return str(o)
if isinstance(o, datetime.datetime):
@@ -85,15 +81,6 @@ def default(self, o):
return o
-def create_acl_app(config_object="settings"):
- app = Flask(__name__.split(".")[0])
- app.config.from_object(config_object)
-
- register_extensions(app)
-
- return app
-
-
def create_app(config_object="settings"):
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
@@ -110,6 +97,7 @@ def create_app(config_object="settings"):
register_shell_context(app)
register_commands(app)
CAS(app)
+ OAuth2(app)
app.wsgi_app = ReverseProxy(app.wsgi_app)
configure_upload_dir(app)
@@ -129,12 +117,18 @@ def configure_upload_dir(app):
def register_extensions(app):
"""Register Flask extensions."""
+
+ def get_locale():
+ accept_languages = app.config.get('ACCEPT_LANGUAGES', ['en', 'zh'])
+ return request.accept_languages.best_match(accept_languages)
+
bcrypt.init_app(app)
+ babel.init_app(app, locale_selector=get_locale)
cache.init_app(app)
db.init_app(app)
cors.init_app(app)
login_manager.init_app(app)
- migrate.init_app(app, db)
+ migrate.init_app(app, db, directory=f"{BASE_DIR}/migrations")
rd.init_app(app)
app.config.update(app.config.get("CELERY"))
@@ -157,10 +151,8 @@ def render_error(error):
error_code = getattr(error, "code", 500)
if not str(error_code).isdigit():
error_code = 400
- if error_code != 500:
- return make_response(jsonify(message=str(error)), error_code)
- else:
- return make_response(jsonify(message=traceback.format_exc(-1)), error_code)
+
+ return make_response(jsonify(message=str(error)), error_code)
for errcode in app.config.get("ERROR_CODES") or [400, 401, 403, 404, 405, 500, 502]:
app.errorhandler(errcode)(render_error)
@@ -183,9 +175,8 @@ def register_commands(app):
for root, _, files in os.walk(os.path.join(HERE, "commands")):
for filename in files:
if not filename.startswith("_") and filename.endswith("py"):
- module_path = os.path.join(API_PACKAGE, root[root.index("commands"):])
- if module_path not in sys.path:
- sys.path.insert(1, module_path)
+ if root not in sys.path:
+ sys.path.insert(1, root)
command = __import__(os.path.splitext(filename)[0])
func_list = [o[0] for o in getmembers(command) if isinstance(o[1], click.core.Command)]
for func_name in func_list:
@@ -203,10 +194,11 @@ def configure_logger(app):
app.logger.addHandler(handler)
log_file = app.config['LOG_PATH']
- file_handler = RotatingFileHandler(log_file,
- maxBytes=2 ** 30,
- backupCount=7)
- file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
- file_handler.setFormatter(formatter)
- app.logger.addHandler(file_handler)
+ if log_file and log_file != "/dev/stdout":
+ file_handler = RotatingFileHandler(log_file,
+ maxBytes=2 ** 30,
+ backupCount=7)
+ file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
+ file_handler.setFormatter(formatter)
+ app.logger.addHandler(file_handler)
app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL']))
diff --git a/acl-api/api/commands/click_acl.py b/acl-api/api/commands/click_acl.py
index d35c6a3..75303ca 100644
--- a/acl-api/api/commands/click_acl.py
+++ b/acl-api/api/commands/click_acl.py
@@ -1,10 +1,15 @@
import click
from flask.cli import with_appcontext
+from api.lib.perm.acl.user import UserCRUD
+
@click.command()
@with_appcontext
def init_acl():
+ """
+ acl init
+ """
from api.models.acl import Role
from api.models.acl import App
from api.tasks.acl import role_rebuild
@@ -20,50 +25,32 @@ def init_acl():
role_rebuild.apply_async(args=(role.id, app.id), queue=ACL_QUEUE)
-# @click.command()
-# @with_appcontext
-# def acl_clean():
-# from api.models.acl import Resource
-# from api.models.acl import Permission
-# from api.models.acl import RolePermission
-#
-# perms = RolePermission.get_by(to_dict=False)
-#
-# for r in perms:
-# perm = Permission.get_by_id(r.perm_id)
-# if perm and perm.app_id != r.app_id:
-# resource_id = r.resource_id
-# resource = Resource.get_by_id(resource_id)
-# perm_name = perm.name
-# existed = Permission.get_by(resource_type_id=resource.resource_type_id, name=perm_name, first=True,
-# to_dict=False)
-# if existed is not None:
-# other = RolePermission.get_by(rid=r.rid, perm_id=existed.id, resource_id=resource_id)
-# if not other:
-# r.update(perm_id=existed.id)
-# else:
-# r.soft_delete()
-# else:
-# r.soft_delete()
-#
-#
-# @click.command()
-# @with_appcontext
-# def acl_has_resource_role():
-# from api.models.acl import Role
-# from api.models.acl import App
-# from api.lib.perm.acl.cache import HasResourceRoleCache
-# from api.lib.perm.acl.role import RoleCRUD
-#
-# roles = Role.get_by(to_dict=False)
-# apps = App.get_by(to_dict=False)
-# for role in roles:
-# if role.app_id:
-# res = RoleCRUD.recursive_resources(role.id, role.app_id)
-# if res.get('resources') or res.get('groups'):
-# HasResourceRoleCache.add(role.id, role.app_id)
-# else:
-# for app in apps:
-# res = RoleCRUD.recursive_resources(role.id, app.id)
-# if res.get('resources') or res.get('groups'):
-# HasResourceRoleCache.add(role.id, app.id)
+@click.command()
+@with_appcontext
+def add_user():
+ """
+ create a user
+
+ is_admin: default is False
+
+ """
+
+ from api.models.acl import App
+ from api.lib.perm.acl.cache import AppCache
+ from api.lib.perm.acl.cache import RoleCache
+ from api.lib.perm.acl.role import RoleCRUD
+ from api.lib.perm.acl.role import RoleRelationCRUD
+
+ username = click.prompt('Enter username', confirmation_prompt=False)
+ password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True)
+ email = click.prompt('Enter email ', confirmation_prompt=False)
+ is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False)
+
+ UserCRUD.add(username=username, password=password, email=email)
+
+ if is_admin:
+ app = AppCache.get('acl') or App.create(name='acl')
+ acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
+ rid = RoleCache.get_by_name(None, username).id
+
+ RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
diff --git a/acl-api/api/commands/init_common_setting.py b/acl-api/api/commands/click_common_setting.py
similarity index 72%
rename from acl-api/api/commands/init_common_setting.py
rename to acl-api/api/commands/click_common_setting.py
index ea5afc9..30ddbcf 100644
--- a/acl-api/api/commands/init_common_setting.py
+++ b/acl-api/api/commands/click_common_setting.py
@@ -4,15 +4,13 @@
from werkzeug.datastructures import MultiDict
from api.lib.common_setting.acl import ACLManager
-from api.lib.common_setting.employee import EmployeeAddForm
+from api.lib.common_setting.employee import EmployeeAddForm, GrantEmployeeACLPerm
from api.lib.common_setting.resp_format import ErrFormat
+from api.lib.common_setting.utils import CheckNewColumn
from api.models.common_setting import Employee, Department
class InitEmployee(object):
- """
- 初始化员工
- """
def __init__(self):
self.log = current_app.logger
@@ -58,7 +56,8 @@ def import_user_from_acl(self):
self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e)))
self.log.error(e)
- def get_rid_by_uid(self, uid):
+ @staticmethod
+ def get_rid_by_uid(uid):
from api.models.acl import Role
role = Role.get_by(first=True, uid=uid)
return role['id'] if role is not None else 0
@@ -71,7 +70,8 @@ def __init__(self):
def init(self):
self.init_wide_company()
- def hard_delete(self, department_id, department_name):
+ @staticmethod
+ def hard_delete(department_id, department_name):
existed_deleted_list = Department.query.filter(
Department.department_name == department_name,
Department.department_id == department_id,
@@ -80,11 +80,12 @@ def hard_delete(self, department_id, department_name):
for existed in existed_deleted_list:
existed.delete()
- def get_department(self, department_name):
+ @staticmethod
+ def get_department(department_name):
return Department.query.filter(
Department.department_name == department_name,
Department.deleted == 0,
- ).order_by(Department.created_at.asc()).first()
+ ).first()
def run(self, department_id, department_name, department_parent_id):
self.hard_delete(department_id, department_name)
@@ -94,7 +95,7 @@ def run(self, department_id, department_name, department_parent_id):
if res.department_id == department_id:
return
else:
- new_d = res.update(
+ res.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
@@ -108,11 +109,11 @@ def run(self, department_id, department_name, department_parent_id):
new_d = self.get_department(department_name)
if new_d.department_id != department_id:
- new_d = new_d.update(
+ new_d.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
- self.log.info(f"初始化 {department_name} 部门成功.")
+ self.log.info(f"init {department_name} success.")
def run_common(self, department_id, department_name, department_parent_id):
try:
@@ -123,19 +124,14 @@ def run_common(self, department_id, department_name, department_parent_id):
raise Exception(e)
def init_wide_company(self):
- """
- 创建 id 0, name 全公司 的部门
- """
department_id = 0
department_name = '全公司'
department_parent_id = -1
self.run_common(department_id, department_name, department_parent_id)
- def create_acl_role_with_department(self):
- """
- 当前所有部门,在ACL创建 role
- """
+ @staticmethod
+ def create_acl_role_with_department():
acl = ACLManager('acl')
role_name_map = {role['name']: role for role in acl.get_all_roles()}
@@ -146,7 +142,7 @@ def create_acl_role_with_department(self):
continue
role = role_name_map.get(department.department_name)
- if role is None:
+ if not role:
payload = {
'app_id': 'acl',
'name': department.department_name,
@@ -161,6 +157,31 @@ def create_acl_role_with_department(self):
info = f"update department acl_rid: {acl_rid}"
current_app.logger.info(info)
+ def init_backend_resource(self):
+ acl = self.check_app('backend')
+ acl_rid = self.get_admin_user_rid()
+
+ if acl_rid == 0:
+ return
+ GrantEmployeeACLPerm(acl).grant_by_rid(acl_rid, True)
+
+ @staticmethod
+ def check_app(app_name):
+ acl = ACLManager(app_name)
+ payload = dict(
+ name=app_name,
+ description=app_name
+ )
+ app = acl.validate_app()
+ if not app:
+ acl.create_app(payload)
+ return acl
+
+ @staticmethod
+ def get_admin_user_rid():
+ admin = Employee.get_by(first=True, username='admin', to_dict=False)
+ return admin.acl_rid if admin else 0
+
@click.command()
@with_appcontext
@@ -177,5 +198,33 @@ def init_department():
"""
Department initialization
"""
- InitDepartment().init()
- InitDepartment().create_acl_role_with_department()
+ cli = InitDepartment()
+ cli.init_wide_company()
+ cli.create_acl_role_with_department()
+ cli.init_backend_resource()
+
+
+@click.command()
+@with_appcontext
+def common_check_new_columns():
+ """
+ add new columns to tables
+ """
+ CheckNewColumn().run()
+
+
+@click.command()
+@with_appcontext
+def common_sync_file_to_db():
+ from api.lib.common_setting.upload_file import CommonFileCRUD
+ CommonFileCRUD.sync_file_to_db()
+
+
+@click.command()
+@with_appcontext
+@click.option('--value', type=click.INT, default=-1)
+def set_auth_auto_redirect_enable(value):
+ if value < 0:
+ return
+ from api.lib.common_setting.common_data import CommonDataCRUD
+ CommonDataCRUD.set_auth_auto_redirect_enable(value)
diff --git a/acl-api/api/commands/common.py b/acl-api/api/commands/common.py
index 1d10f1c..3fd9f7b 100644
--- a/acl-api/api/commands/common.py
+++ b/acl-api/api/commands/common.py
@@ -150,3 +150,40 @@ def db_setup():
"""create tables
"""
db.create_all()
+
+
+@click.group()
+def translate():
+ """Translation and localization commands."""
+
+
+@translate.command()
+@click.argument('lang')
+def init(lang):
+ """Initialize a new language."""
+
+ if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
+ raise RuntimeError('extract command failed')
+ if os.system(
+ 'pybabel init -i messages.pot -d api/translations -l ' + lang):
+ raise RuntimeError('init command failed')
+ os.remove('messages.pot')
+
+
+@translate.command()
+def update():
+ """Update all languages."""
+
+ if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
+ raise RuntimeError('extract command failed')
+ if os.system('pybabel update -i messages.pot -d api/translations'):
+ raise RuntimeError('update command failed')
+ os.remove('messages.pot')
+
+
+@translate.command()
+def compile():
+ """Compile all languages."""
+
+ if os.system('pybabel compile -d api/translations'):
+ raise RuntimeError('compile command failed')
diff --git a/acl-api/api/extensions.py b/acl-api/api/extensions.py
index 6bffa91..85fc2d1 100644
--- a/acl-api/api/extensions.py
+++ b/acl-api/api/extensions.py
@@ -2,6 +2,7 @@
from celery import Celery
+from flask_babel import Babel
from flask_bcrypt import Bcrypt
from flask_caching import Cache
from flask_cors import CORS
@@ -12,6 +13,7 @@
from api.lib.utils import RedisHandler
bcrypt = Bcrypt()
+babel = Babel()
login_manager = LoginManager()
db = SQLAlchemy(session_options={"autoflush": False})
migrate = Migrate()
diff --git a/acl-api/api/lib/common_setting/acl.py b/acl-api/api/lib/common_setting/acl.py
index 163a373..5591bc0 100644
--- a/acl-api/api/lib/common_setting/acl.py
+++ b/acl-api/api/lib/common_setting/acl.py
@@ -1,13 +1,20 @@
# -*- coding:utf-8 -*-
-from flask import abort
from flask import current_app
from api.lib.common_setting.resp_format import ErrFormat
+from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.cache import RoleCache, AppCache
+from api.lib.perm.acl.permission import PermissionCRUD
+from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD
+def validate_app(app_id):
+ app = AppCache.get(app_id)
+ return app.id if app else None
+
+
class ACLManager(object):
def __init__(self, app_name='acl', uid=None):
self.log = current_app.logger
@@ -78,19 +85,69 @@ def edit_role(_id, payload):
return role.to_dict()
@staticmethod
- def delete_role(_id, payload):
+ def delete_role(_id):
RoleCRUD.delete_role(_id)
return dict(rid=_id)
def get_user_info(self, username):
from api.lib.perm.acl.acl import ACLManager as ACL
user_info = ACL().get_user_info(username, self.app_name)
- result = dict(name=user_info.get('nickname') or username,
- username=user_info.get('username') or username,
- email=user_info.get('email'),
- uid=user_info.get('uid'),
- rid=user_info.get('rid'),
- role=dict(permissions=user_info.get('parents')),
- avatar=user_info.get('avatar'))
+ result = dict(
+ name=user_info.get('nickname') or username,
+ username=user_info.get('username') or username,
+ email=user_info.get('email'),
+ uid=user_info.get('uid'),
+ rid=user_info.get('rid'),
+ role=dict(permissions=user_info.get('parents')),
+ avatar=user_info.get('avatar')
+ )
return result
+
+ def validate_app(self):
+ return AppCache.get(self.app_name)
+
+ def get_all_resources_types(self, q=None, page=1, page_size=999999):
+ app_id = self.validate_app().id
+ numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size)
+
+ return dict(
+ numfound=numfound,
+ groups=[i.to_dict() for i in res],
+ id2perms=id2perms
+ )
+
+ def create_resources_type(self, payload):
+ payload['app_id'] = self.validate_app().id
+ rt = ResourceTypeCRUD.add(**payload)
+
+ return rt.to_dict()
+
+ def update_resources_type(self, _id, payload):
+ rt = ResourceTypeCRUD.update(_id, **payload)
+
+ return rt.to_dict()
+
+ def create_resource(self, payload):
+ payload['app_id'] = self.validate_app().id
+ resource = ResourceCRUD.add(**payload)
+
+ return resource.to_dict()
+
+ def get_resource_by_type(self, q, u, rt_id, page=1, page_size=999999):
+ numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
+ return res
+
+ @staticmethod
+ def grant_resource(rid, resource_id, perms):
+ PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
+
+ @staticmethod
+ def create_app(payload):
+ rt = AppCRUD.add(**payload)
+
+ return rt.to_dict()
+
+ def role_has_perms(self, rid, resource_name, resource_type_name, perm):
+ app_id = validate_app(self.app_name)
+ return RoleCRUD.has_permission(rid, resource_name, resource_type_name, app_id, perm)
diff --git a/acl-api/api/lib/common_setting/common_data.py b/acl-api/api/lib/common_setting/common_data.py
new file mode 100644
index 0000000..93c6f6d
--- /dev/null
+++ b/acl-api/api/lib/common_setting/common_data.py
@@ -0,0 +1,282 @@
+import copy
+import json
+
+from flask import abort, current_app
+from ldap3 import Connection
+from ldap3 import Server
+from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
+from ldap3 import AUTO_BIND_NO_TLS
+
+from api.extensions import db
+from api.lib.common_setting.resp_format import ErrFormat
+from api.models.common_setting import CommonData
+from api.lib.utils import AESCrypto
+from api.lib.common_setting.const import AuthCommonConfig, AuthenticateType, AuthCommonConfigAutoRedirect, TestType
+
+
+class CommonDataCRUD(object):
+
+ @staticmethod
+ def get_data_by_type(data_type):
+ CommonDataCRUD.check_auth_type(data_type)
+ return CommonData.get_by(data_type=data_type)
+
+ @staticmethod
+ def get_data_by_id(_id, to_dict=True):
+ return CommonData.get_by(first=True, id=_id, to_dict=to_dict)
+
+ @staticmethod
+ def create_new_data(data_type, **kwargs):
+ try:
+ CommonDataCRUD.check_auth_type(data_type)
+
+ return CommonData.create(data_type=data_type, **kwargs)
+ except Exception as e:
+ db.session.rollback()
+ abort(400, str(e))
+
+ @staticmethod
+ def update_data(_id, **kwargs):
+ existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
+ if not existed:
+ abort(404, ErrFormat.common_data_not_found.format(_id))
+ try:
+ CommonDataCRUD.check_auth_type(existed.data_type)
+ return existed.update(**kwargs)
+ except Exception as e:
+ db.session.rollback()
+ abort(400, str(e))
+
+ @staticmethod
+ def delete(_id):
+ existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
+ if not existed:
+ abort(404, ErrFormat.common_data_not_found.format(_id))
+ try:
+ CommonDataCRUD.check_auth_type(existed.data_type)
+ existed.soft_delete()
+ except Exception as e:
+ db.session.rollback()
+ abort(400, str(e))
+
+ @staticmethod
+ def check_auth_type(data_type):
+ if data_type in list(AuthenticateType.all()) + [AuthCommonConfig]:
+ abort(400, ErrFormat.common_data_not_support_auth_type.format(data_type))
+
+ @staticmethod
+ def set_auth_auto_redirect_enable(_value: int):
+ existed = CommonData.get_by(first=True, data_type=AuthCommonConfig, to_dict=False)
+ if not existed:
+ CommonDataCRUD.create_new_data(AuthCommonConfig, data={AuthCommonConfigAutoRedirect: _value})
+ else:
+ data = existed.data
+ data = copy.deepcopy(existed.data) if data else {}
+ data[AuthCommonConfigAutoRedirect] = _value
+ CommonDataCRUD.update_data(existed.id, data=data)
+ return True
+
+ @staticmethod
+ def get_auth_auto_redirect_enable():
+ existed = CommonData.get_by(first=True, data_type=AuthCommonConfig)
+ if not existed:
+ return 0
+ data = existed.get('data', {})
+ if not data:
+ return 0
+ return data.get(AuthCommonConfigAutoRedirect, 0)
+
+
+class AuthenticateDataCRUD(object):
+ common_type_list = [AuthCommonConfig]
+
+ def __init__(self, _type):
+ self._type = _type
+ self.record = None
+ self.decrypt_data = {}
+
+ def get_support_type_list(self):
+ return list(AuthenticateType.all()) + self.common_type_list
+
+ def get(self):
+ if not self.decrypt_data:
+ self.decrypt_data = self.get_decrypt_data()
+
+ return self.decrypt_data
+
+ def get_by_key(self, _key):
+ if not self.decrypt_data:
+ self.decrypt_data = self.get_decrypt_data()
+
+ return self.decrypt_data.get(_key, None)
+
+ def get_record(self, to_dict=False) -> CommonData:
+ return CommonData.get_by(first=True, data_type=self._type, to_dict=to_dict)
+
+ def get_record_with_decrypt(self) -> dict:
+ record = CommonData.get_by(first=True, data_type=self._type, to_dict=True)
+ if not record:
+ return {}
+ data = self.get_decrypt_dict(record.get('data', ''))
+ record['data'] = data
+ return record
+
+ def get_decrypt_dict(self, data):
+ decrypt_str = self.decrypt(data)
+ try:
+ return json.loads(decrypt_str)
+ except Exception as e:
+ abort(400, str(e))
+
+ def get_decrypt_data(self) -> dict:
+ self.record = self.get_record()
+ if not self.record:
+ return self.get_from_config()
+ return self.get_decrypt_dict(self.record.data)
+
+ def get_from_config(self):
+ return current_app.config.get(self._type, {})
+
+ def check_by_type(self) -> None:
+ existed = self.get_record()
+ if existed:
+ abort(400, ErrFormat.common_data_already_existed.format(self._type))
+
+ def create(self, data) -> CommonData:
+ self.check_by_type()
+ encrypt = data.pop('encrypt', None)
+ if encrypt is False:
+ return CommonData.create(data_type=self._type, data=data)
+ encrypted_data = self.encrypt(data)
+ try:
+ return CommonData.create(data_type=self._type, data=encrypted_data)
+ except Exception as e:
+ db.session.rollback()
+ abort(400, str(e))
+
+ def update_by_record(self, record, data) -> CommonData:
+ encrypt = data.pop('encrypt', None)
+ if encrypt is False:
+ return record.update(data=data)
+ encrypted_data = self.encrypt(data)
+ try:
+ return record.update(data=encrypted_data)
+ except Exception as e:
+ db.session.rollback()
+ abort(400, str(e))
+
+ def update(self, _id, data) -> CommonData:
+ existed = CommonData.get_by(first=True, to_dict=False, id=_id)
+ if not existed:
+ abort(404, ErrFormat.common_data_not_found.format(_id))
+
+ return self.update_by_record(existed, data)
+
+ @staticmethod
+ def delete(_id) -> None:
+ existed = CommonData.get_by(first=True, to_dict=False, id=_id)
+ if not existed:
+ abort(404, ErrFormat.common_data_not_found.format(_id))
+ try:
+ existed.soft_delete()
+ except Exception as e:
+ db.session.rollback()
+ abort(400, str(e))
+
+ @staticmethod
+ def encrypt(data) -> str:
+ if type(data) is dict:
+ try:
+ data = json.dumps(data)
+ except Exception as e:
+ abort(400, str(e))
+ return AESCrypto().encrypt(data)
+
+ @staticmethod
+ def decrypt(data) -> str:
+ return AESCrypto().decrypt(data)
+
+ @staticmethod
+ def get_enable_list():
+ all_records = CommonData.query.filter(
+ CommonData.data_type.in_(AuthenticateType.all()),
+ CommonData.deleted == 0
+ ).all()
+ enable_list = []
+ for auth_type in AuthenticateType.all():
+ record = list(filter(lambda x: x.data_type == auth_type, all_records))
+ if not record:
+ config = current_app.config.get(auth_type, None)
+ if not config:
+ continue
+
+ if config.get('enable', False):
+ enable_list.append(dict(
+ auth_type=auth_type,
+ ))
+
+ continue
+
+ try:
+ decrypt_data = json.loads(AuthenticateDataCRUD.decrypt(record[0].data))
+ except Exception as e:
+ current_app.logger.error(e)
+ continue
+
+ if decrypt_data.get('enable', 0) == 1:
+ enable_list.append(dict(
+ auth_type=auth_type,
+ ))
+
+ auth_auto_redirect = CommonDataCRUD.get_auth_auto_redirect_enable()
+
+ return dict(
+ enable_list=enable_list,
+ auth_auto_redirect=auth_auto_redirect,
+ )
+
+ def test(self, test_type, data):
+ type_lower = self._type.lower()
+ func_name = f'test_{type_lower}'
+ if hasattr(self, func_name):
+ try:
+ return getattr(self, f'test_{type_lower}')(test_type, data)
+ except Exception as e:
+ abort(400, str(e))
+ abort(400, ErrFormat.not_support_test.format(self._type))
+
+ @staticmethod
+ def test_ldap(test_type, data):
+ ldap_server = data.get('ldap_server')
+ ldap_user_dn = data.get('ldap_user_dn', '{}')
+
+ server = Server(ldap_server, connect_timeout=2)
+ if not server.check_availability():
+ raise Exception(ErrFormat.ldap_server_connect_not_available)
+ else:
+ if test_type == TestType.Connect:
+ return True
+
+ username = data.get('username', None)
+ if not username:
+ raise Exception(ErrFormat.ldap_test_username_required)
+ user = ldap_user_dn.format(username)
+ password = data.get('password', None)
+
+ try:
+ Connection(server, user=user, password=password, auto_bind=AUTO_BIND_NO_TLS)
+ except LDAPBindError:
+ ldap_domain = data.get('ldap_domain')
+ user_with_domain = f"{username}@{ldap_domain}"
+ try:
+ Connection(server, user=user_with_domain, password=password, auto_bind=AUTO_BIND_NO_TLS)
+ except Exception as e:
+ raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
+
+ except LDAPSocketOpenError:
+ raise Exception(ErrFormat.ldap_server_connect_timeout)
+
+ except Exception as e:
+ raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
+
+ return True
diff --git a/acl-api/api/lib/common_setting/company_info.py b/acl-api/api/lib/common_setting/company_info.py
index 861bb3f..7031a2f 100644
--- a/acl-api/api/lib/common_setting/company_info.py
+++ b/acl-api/api/lib/common_setting/company_info.py
@@ -1,5 +1,7 @@
# -*- coding:utf-8 -*-
+from urllib.parse import urlparse
+from api.extensions import cache
from api.models.common_setting import CompanyInfo
@@ -11,14 +13,51 @@ def get():
@staticmethod
def create(**kwargs):
- return CompanyInfo.create(**kwargs)
+ CompanyInfoCRUD.check_data(**kwargs)
+ res = CompanyInfo.create(**kwargs)
+ CompanyInfoCache.refresh(res.info)
+ return res
@staticmethod
def update(_id, **kwargs):
kwargs.pop('id', None)
existed = CompanyInfo.get_by_id(_id)
if not existed:
- return CompanyInfoCRUD.create(**kwargs)
+ existed = CompanyInfoCRUD.create(**kwargs)
else:
+ CompanyInfoCRUD.check_data(**kwargs)
existed = existed.update(**kwargs)
- return existed
+ CompanyInfoCache.refresh(existed.info)
+ return existed
+
+ @staticmethod
+ def check_data(**kwargs):
+ info = kwargs.get('info', {})
+ info['messenger'] = CompanyInfoCRUD.check_messenger(info.get('messenger', None))
+
+ kwargs['info'] = info
+
+ @staticmethod
+ def check_messenger(messenger):
+ if not messenger:
+ return messenger
+
+ parsed_url = urlparse(messenger)
+ return f"{parsed_url.scheme}://{parsed_url.netloc}"
+
+
+class CompanyInfoCache(object):
+ key = 'CompanyInfoCache::'
+
+ @classmethod
+ def get(cls):
+ info = cache.get(cls.key)
+ if not info:
+ res = CompanyInfo.get_by(first=True) or {}
+ info = res.get('info', {})
+ cache.set(cls.key, info)
+ return info
+
+ @classmethod
+ def refresh(cls, info):
+ cache.set(cls.key, info)
diff --git a/acl-api/api/lib/common_setting/const.py b/acl-api/api/lib/common_setting/const.py
index 2768f56..a68dcbf 100644
--- a/acl-api/api/lib/common_setting/const.py
+++ b/acl-api/api/lib/common_setting/const.py
@@ -12,3 +12,55 @@ class OperatorType(BaseEnum):
LESS_THAN = 6
IS_EMPTY = 7
IS_NOT_EMPTY = 8
+
+
+BotNameMap = {
+ 'wechatApp': 'wechatBot',
+ 'feishuApp': 'feishuBot',
+ 'dingdingApp': 'dingdingBot',
+}
+
+
+class AuthenticateType(BaseEnum):
+ CAS = 'CAS'
+ OAUTH2 = 'OAUTH2'
+ OIDC = 'OIDC'
+ LDAP = 'LDAP'
+
+
+AuthCommonConfig = 'AuthCommonConfig'
+AuthCommonConfigAutoRedirect = 'auto_redirect'
+
+
+class TestType(BaseEnum):
+ Connect = 'connect'
+ Login = 'login'
+
+
+MIMEExtMap = {
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
+ 'application/msword': '.doc',
+ 'application/vnd.ms-word.document.macroEnabled.12': '.docm',
+ 'application/vnd.ms-excel': '.xls',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
+ 'application/vnd.ms-excel.sheet.macroEnabled.12': '.xlsm',
+ 'application/vnd.ms-powerpoint': '.ppt',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
+ 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': '.pptm',
+ 'application/zip': '.zip',
+ 'application/x-7z-compressed': '.7z',
+ 'application/json': '.json',
+ 'application/pdf': '.pdf',
+ 'image/png': '.png',
+ 'image/bmp': '.bmp',
+ 'image/prs.btif': '.btif',
+ 'image/gif': '.gif',
+ 'image/jpeg': '.jpg',
+ 'image/tiff': '.tif',
+ 'image/vnd.microsoft.icon': '.ico',
+ 'image/webp': '.webp',
+ 'image/svg+xml': '.svg',
+ 'image/vnd.adobe.photoshop': '.psd',
+ 'text/plain': '.txt',
+ 'text/csv': '.csv',
+}
diff --git a/acl-api/api/lib/common_setting/decorator.py b/acl-api/api/lib/common_setting/decorator.py
new file mode 100644
index 0000000..30106e1
--- /dev/null
+++ b/acl-api/api/lib/common_setting/decorator.py
@@ -0,0 +1,38 @@
+import functools
+
+from flask import abort, session
+from api.lib.common_setting.acl import ACLManager
+from api.lib.common_setting.resp_format import ErrFormat
+from api.lib.perm.acl.acl import is_app_admin
+
+
+def perms_role_required(app_name, resource_type_name, resource_name, perm, role_name=None):
+ def decorator_perms_role_required(func):
+ @functools.wraps(func)
+ def wrapper_required(*args, **kwargs):
+ acl = ACLManager(app_name)
+ has_perms = False
+ try:
+ has_perms = acl.role_has_perms(session["acl"]['rid'], resource_name, resource_type_name, perm)
+ except Exception as e:
+ # resource_type not exist, continue check role
+ if role_name:
+ if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):
+ abort(403, ErrFormat.role_required.format(role_name))
+
+ return func(*args, **kwargs)
+ else:
+ abort(403, ErrFormat.resource_no_permission.format(resource_name, perm))
+
+ if not has_perms:
+ if role_name:
+ if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):
+ abort(403, ErrFormat.role_required.format(role_name))
+ else:
+ abort(403, ErrFormat.resource_no_permission.format(resource_name, perm))
+
+ return func(*args, **kwargs)
+
+ return wrapper_required
+
+ return decorator_perms_role_required
diff --git a/acl-api/api/lib/common_setting/department.py b/acl-api/api/lib/common_setting/department.py
index 8da3c58..7d72bc6 100644
--- a/acl-api/api/lib/common_setting/department.py
+++ b/acl-api/api/lib/common_setting/department.py
@@ -1,6 +1,6 @@
# -*- coding:utf-8 -*-
-from flask import abort
+from flask import abort, current_app
from treelib import Tree
from wtforms import Form
from wtforms import IntegerField
@@ -9,6 +9,7 @@
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
+from api.lib.common_setting.acl import ACLManager
from api.lib.perm.acl.role import RoleCRUD
from api.models.common_setting import Department, Employee
@@ -23,7 +24,15 @@ def get_all_department_list(to_dict=True):
*criterion
).order_by(Department.department_id.asc())
results = query.all()
- return [r.to_dict() for r in results] if to_dict else results
+ if to_dict:
+ datas = []
+ for r in results:
+ d = r.to_dict()
+ if r.department_id == 0:
+ d['department_name'] = ErrFormat.company_wide
+ datas.append(d)
+ return datas
+ return results
def get_all_employee_list(block=0, to_dict=True):
@@ -100,6 +109,7 @@ def get_tree_departments(self):
employees = self.get_employees_by_d_id(department_id)
top_d['employees'] = employees
+ top_d['department_name'] = ErrFormat.company_wide
if len(sub_deps) == 0:
top_d[sub_departments_column_name] = []
d_list.append(top_d)
@@ -152,6 +162,10 @@ class DepartmentForm(Form):
class DepartmentCRUD(object):
+ @staticmethod
+ def get_department_by_id(d_id, to_dict=True):
+ return Department.get_by(first=True, department_id=d_id, to_dict=to_dict)
+
@staticmethod
def add(**kwargs):
DepartmentCRUD.check_department_name_unique(kwargs['department_name'])
@@ -186,10 +200,11 @@ def check_department_parent_id_allow(d_id, department_parent_id):
filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list))
if len(target) == 0:
try:
- d = Department.get_by(
+ dep = Department.get_by(
first=True, to_dict=False, department_id=department_parent_id)
- name = d.department_name if d else ErrFormat.department_id_not_found.format(department_parent_id)
+ name = dep.department_name if dep else ErrFormat.department_id_not_found.format(department_parent_id)
except Exception as e:
+ current_app.logger.error(str(e))
name = ErrFormat.department_id_not_found.format(department_parent_id)
abort(400, ErrFormat.cannot_to_be_parent_department.format(name))
@@ -240,7 +255,7 @@ def edit(_id, **kwargs):
return abort(400, ErrFormat.acl_update_role_failed.format(str(e)))
try:
- existed.update(**kwargs)
+ return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@@ -253,7 +268,7 @@ def delete(_id):
try:
RoleCRUD.delete_role(existed.acl_rid)
except Exception as e:
- pass
+ current_app.logger.error(str(e))
return existed.soft_delete()
@@ -268,7 +283,7 @@ def get_allow_parent_d_id_by(department_id):
try:
tree.remove_subtree(department_id)
except Exception as e:
- pass
+ current_app.logger.error(str(e))
[allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in
tree.all_nodes()]
@@ -307,6 +322,7 @@ def get_department_tree_list():
tree_list = []
for top_d in top_deps:
+ top_d['department_name'] = ErrFormat.company_wide
tree = Tree()
identifier_root = top_d['department_id']
tree.create_node(
@@ -377,6 +393,9 @@ def get_departments_and_ids(department_parent_id, block):
d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list)))
+ if int(department_parent_id) == -1:
+ d['department_name'] = ErrFormat.company_wide
+
return all_departments, department_id_list
@staticmethod
@@ -390,6 +409,151 @@ def get_department_id_list_by_root(root_department_id, tree_list=None):
[id_list.append(int(n.identifier))
for n in tmp_tree.all_nodes()]
except Exception as e:
- pass
+ current_app.logger.error(str(e))
return id_list
+
+
+class EditDepartmentInACL(object):
+
+ @staticmethod
+ def add_department_to_acl(department_id, op_uid):
+ db_department = DepartmentCRUD.get_department_by_id(department_id, to_dict=False)
+ if not db_department:
+ return
+
+ from api.models.acl import Role
+ role = Role.get_by(first=True, name=db_department.department_name, app_id=None)
+
+ acl = ACLManager('acl', str(op_uid))
+ if role is None:
+ payload = {
+ 'app_id': 'acl',
+ 'name': db_department.department_name,
+ }
+ role = acl.create_role(payload)
+
+ acl_rid = role.get('id') if role else 0
+
+ db_department.update(
+ acl_rid=acl_rid
+ )
+ info = f"add_department_to_acl, acl_rid: {acl_rid}"
+ current_app.logger.info(info)
+ return info
+
+ @staticmethod
+ def delete_department_from_acl(department_rids, op_uid):
+ acl = ACLManager('acl', str(op_uid))
+
+ result = []
+
+ for rid in department_rids:
+ try:
+ acl.delete_role(rid)
+ except Exception as e:
+ result.append(f"delete_department_in_acl, rid: {rid}, error: {e}")
+ continue
+
+ return result
+
+ @staticmethod
+ def edit_department_name_in_acl(d_rid: int, d_name: str, op_uid: int):
+ acl = ACLManager('acl', str(op_uid))
+ payload = {
+ 'name': d_name
+ }
+ try:
+ acl.edit_role(d_rid, payload)
+ except Exception as e:
+ return f"edit_department_name_in_acl, rid: {d_rid}, error: {e}"
+
+ return f"edit_department_name_in_acl, rid: {d_rid}, success"
+
+ @classmethod
+ def remove_from_old_department_role(cls, e_list, acl):
+ result = []
+ for employee in e_list:
+ employee_acl_rid = employee.get('e_acl_rid')
+ if employee_acl_rid == 0:
+ result.append(f"employee_acl_rid == 0")
+ continue
+ cls.remove_single_employee_from_old_department(acl, employee, result)
+
+ @staticmethod
+ def remove_single_employee_from_old_department(acl, employee, result):
+ from api.models.acl import Role
+ old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
+ if not old_department:
+ return False
+
+ old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None)
+ old_d_rid_in_acl = old_role.get('id') if old_role else 0
+ if old_d_rid_in_acl == 0:
+ return False
+
+ d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
+ payload = {
+ 'app_id': 'acl',
+ 'parent_id': d_acl_rid,
+ }
+ try:
+ acl.remove_user_from_role(employee.get('e_acl_rid'), payload)
+ current_app.logger.info(f"remove {employee.get('e_acl_rid')} from {d_acl_rid}")
+ except Exception as e:
+ result.append(
+ f"remove_user_from_role employee_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}")
+
+ return True
+
+ @staticmethod
+ def add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result):
+ payload = {
+ 'app_id': 'acl',
+ 'child_ids': [employee_acl_rid],
+ }
+ try:
+ acl.add_user_to_role(new_department_acl_rid, payload)
+ current_app.logger.info(f"add {employee_acl_rid} to {new_department_acl_rid}")
+ except Exception as e:
+ result.append(
+ f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {new_department_acl_rid}, \
+ err: {e}")
+
+ @classmethod
+ def edit_employee_department_in_acl(cls, e_list: list, new_d_id: int, op_uid: int):
+ result = []
+ new_department = DepartmentCRUD.get_department_by_id(new_d_id, False)
+ if not new_department:
+ result.append(f"{new_d_id} new_department is None")
+ return result
+
+ from api.models.acl import Role
+ new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None)
+ new_d_rid_in_acl = new_role.get('id') if new_role else 0
+ acl = ACLManager('acl', str(op_uid))
+
+ if new_d_rid_in_acl == 0:
+ # only remove from old department role
+ cls.remove_from_old_department_role(e_list, acl)
+ return
+
+ if new_d_rid_in_acl != new_department.acl_rid:
+ new_department.update(
+ acl_rid=new_d_rid_in_acl
+ )
+ new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \
+ new_d_rid_in_acl
+
+ for employee in e_list:
+ employee_acl_rid = employee.get('e_acl_rid')
+ if employee_acl_rid == 0:
+ result.append(f"employee_acl_rid == 0")
+ continue
+
+ cls.remove_single_employee_from_old_department(acl, employee, result)
+
+ # 在新部门中添加员工
+ cls.add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result)
+
+ return result
diff --git a/acl-api/api/lib/common_setting/employee.py b/acl-api/api/lib/common_setting/employee.py
index 72898f7..ca7173f 100644
--- a/acl-api/api/lib/common_setting/employee.py
+++ b/acl-api/api/lib/common_setting/employee.py
@@ -1,8 +1,9 @@
# -*- coding:utf-8 -*-
-
+import copy
import traceback
from datetime import datetime
+import requests
from flask import abort
from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_
@@ -14,10 +15,13 @@
from api.extensions import db
from api.lib.common_setting.acl import ACLManager
-from api.lib.common_setting.const import COMMON_SETTING_QUEUE, OperatorType
+from api.lib.common_setting.const import OperatorType
+from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department
+from api.tasks.common_setting import refresh_employee_acl_info, edit_employee_department_in_acl
+
acl_user_columns = [
'email',
'mobile',
@@ -120,10 +124,25 @@ def check_acl_user_and_create(user_info):
employee = CreateEmployee().create_single(**data)
return employee.to_dict()
+ @staticmethod
+ def add_employee_from_acl_created(**kwargs):
+ try:
+ kwargs['acl_uid'] = kwargs.pop('uid')
+ kwargs['acl_rid'] = kwargs.pop('rid')
+ kwargs['department_id'] = 0
+
+ Employee.create(
+ **kwargs
+ )
+ except Exception as e:
+ abort(400, str(e))
+
@staticmethod
def add(**kwargs):
try:
- return CreateEmployee().create_single(**kwargs)
+ res = CreateEmployee().create_single(**kwargs)
+ refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
+ return res
except Exception as e:
abort(400, str(e))
@@ -150,10 +169,9 @@ def update(_id, **kwargs):
existed.update(**kwargs)
if len(e_list) > 0:
- from api.tasks.common_setting import edit_employee_department_in_acl
edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid),
- queue=COMMON_SETTING_QUEUE
+ queue=ACL_QUEUE
)
return existed
@@ -164,7 +182,7 @@ def update(_id, **kwargs):
def edit_employee_by_uid(_uid, **kwargs):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
try:
- user = edit_acl_user(_uid, **kwargs)
+ edit_acl_user(_uid, **kwargs)
for column in employee_pop_columns:
if kwargs.get(column):
@@ -176,9 +194,9 @@ def edit_employee_by_uid(_uid, **kwargs):
@staticmethod
def change_password_by_uid(_uid, password):
- existed = EmployeeCRUD.get_employee_by_uid(_uid)
+ EmployeeCRUD.get_employee_by_uid(_uid)
try:
- user = edit_acl_user(_uid, password=password)
+ edit_acl_user(_uid, password=password)
except Exception as e:
return abort(400, str(e))
@@ -277,7 +295,9 @@ def get_employee_list_by_body(department_id, block_status, search='', order='',
employees = []
for r in pagination.items:
d = r.Employee.to_dict()
- d['department_name'] = r.Department.department_name
+ d['department_name'] = r.Department.department_name if r.Department else ''
+ if r.Employee.department_id == 0:
+ d['department_name'] = ErrFormat.company_wide
employees.append(d)
return {
@@ -345,9 +365,11 @@ def check_condition(column, operator, value, relation):
if value and column == "last_login":
try:
- value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
+ return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except Exception as e:
- abort(400, ErrFormat.datetime_format_error.format(column))
+ err = f"{ErrFormat.datetime_format_error.format(column)}: {str(e)}"
+ abort(400, err)
+ return value
@staticmethod
def get_attr_by_column(column):
@@ -368,7 +390,7 @@ def get_query_by_conditions(query, conditions):
relation = condition.get("relation", None)
value = condition.get("value", None)
- EmployeeCRUD.check_condition(column, operator, value, relation)
+ value = EmployeeCRUD.check_condition(column, operator, value, relation)
a, o = EmployeeCRUD.get_expr_by_condition(
column, operator, value, relation)
and_list += a
@@ -421,7 +443,7 @@ def get_employee_list_by(department_id, block_status, search='', order='', page=
employees = []
for r in pagination.items:
d = r.Employee.to_dict()
- d['department_name'] = r.Department.department_name
+ d['department_name'] = r.Department.department_name if r.Department else ''
employees.append(d)
return {
@@ -474,6 +496,224 @@ def get_employees_by_department_id(department_id, block):
return [r.to_dict() for r in results]
+ @staticmethod
+ def remove_bind_notice_by_uid(_platform, _uid):
+ existed = EmployeeCRUD.get_employee_by_uid(_uid)
+ employee_data = existed.to_dict()
+
+ notice_info = employee_data.get('notice_info', {})
+ notice_info = copy.deepcopy(notice_info) if notice_info else {}
+
+ notice_info[_platform] = ''
+
+ existed.update(
+ notice_info=notice_info
+ )
+ return ErrFormat.notice_remove_bind_success
+
+ @staticmethod
+ def bind_notice_by_uid(_platform, _uid):
+ existed = EmployeeCRUD.get_employee_by_uid(_uid)
+ mobile = existed.mobile
+ if not mobile or len(mobile) == 0:
+ abort(400, ErrFormat.notice_bind_err_with_empty_mobile)
+
+ from api.lib.common_setting.notice_config import NoticeConfigCRUD
+ messenger = NoticeConfigCRUD.get_messenger_url()
+ if not messenger or len(messenger) == 0:
+ abort(400, ErrFormat.notice_please_config_messenger_first)
+
+ url = f"{messenger}/v1/uid/getbyphone"
+ try:
+ payload = dict(
+ phone=mobile,
+ sender=_platform
+ )
+ res = requests.post(url, json=payload)
+ result = res.json()
+ if res.status_code != 200:
+ raise Exception(result.get('msg', ''))
+ target_id = result.get('uid', '')
+
+ employee_data = existed.to_dict()
+
+ notice_info = employee_data.get('notice_info', {})
+ notice_info = copy.deepcopy(notice_info) if notice_info else {}
+
+ notice_info[_platform] = '' if not target_id else target_id
+
+ existed.update(
+ notice_info=notice_info
+ )
+ return ErrFormat.notice_bind_success
+
+ except Exception as e:
+ return abort(400, ErrFormat.notice_bind_failed.format(str(e)))
+
+ @staticmethod
+ def get_employee_notice_by_ids(employee_ids):
+ criterion = [
+ Employee.employee_id.in_(employee_ids),
+ Employee.deleted == 0,
+ ]
+ direct_columns = ['email', 'mobile']
+ employees = Employee.query.filter(
+ *criterion
+ ).all()
+ results = []
+ for employee in employees:
+ d = employee.to_dict()
+ tmp = dict(
+ employee_id=employee.employee_id,
+ )
+ for column in direct_columns:
+ tmp[column] = d.get(column, '')
+ notice_info = d.get('notice_info', {})
+ notice_info = copy.deepcopy(notice_info) if notice_info else {}
+ tmp.update(**notice_info)
+ results.append(tmp)
+ return results
+
+ @staticmethod
+ def import_employee(employee_list):
+ res = CreateEmployee().batch_create(employee_list)
+ return res
+
+ @staticmethod
+ def batch_edit_employee_department(employee_id_list, column_value):
+ err_list = []
+ employee_list = []
+ for _id in employee_id_list:
+ try:
+ existed = EmployeeCRUD.get_employee_by_id(_id)
+ employee = dict(
+ e_acl_rid=existed.acl_rid,
+ department_id=existed.department_id
+ )
+ employee_list.append(employee)
+ existed.update(department_id=column_value)
+
+ except Exception as e:
+ err_list.append({
+ 'employee_id': _id,
+ 'err': str(e),
+ })
+ from api.lib.common_setting.department import EditDepartmentInACL
+ EditDepartmentInACL.edit_employee_department_in_acl(
+ employee_list, column_value, current_user.uid
+ )
+ return err_list
+
+ @staticmethod
+ def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False):
+ if column_name == 'block':
+ err_list = []
+ success_list = []
+ for _id in employee_id_list:
+ try:
+ employee = EmployeeCRUD.edit_employee_block_column(
+ _id, is_acl, **{column_name: column_value})
+ success_list.append(employee)
+ except Exception as e:
+ err_list.append({
+ 'employee_id': _id,
+ 'err': str(e),
+ })
+ return err_list
+ else:
+ return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl)
+
+ @staticmethod
+ def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False):
+ err_list = []
+ for _id in employee_id_list:
+ try:
+ EmployeeCRUD.edit_employee_single_column(
+ _id, is_acl, **{column_name: column_value})
+ except Exception as e:
+ err_list.append({
+ 'employee_id': _id,
+ 'err': str(e),
+ })
+
+ return err_list
+
+ @staticmethod
+ def edit_employee_single_column(_id, is_acl=False, **kwargs):
+ existed = EmployeeCRUD.get_employee_by_id(_id)
+ if 'direct_supervisor_id' in kwargs.keys():
+ if kwargs['direct_supervisor_id'] == existed.direct_supervisor_id:
+ raise Exception(ErrFormat.direct_supervisor_is_not_self)
+
+ if is_acl:
+ return edit_acl_user(existed.acl_uid, **kwargs)
+
+ try:
+ for column in employee_pop_columns:
+ if kwargs.get(column):
+ kwargs.pop(column)
+
+ return existed.update(**kwargs)
+ except Exception as e:
+ return abort(400, str(e))
+
+ @staticmethod
+ def edit_employee_block_column(_id, is_acl=False, **kwargs):
+ existed = EmployeeCRUD.get_employee_by_id(_id)
+ value = get_block_value(kwargs.get('block'))
+ if value is True:
+ check_department_director_id_or_direct_supervisor_id(_id)
+ value = 1
+ else:
+ value = 0
+
+ if is_acl:
+ kwargs['block'] = value
+ edit_acl_user(existed.acl_uid, **kwargs)
+
+ existed.update(block=value)
+ data = existed.to_dict()
+ return data
+
+ @staticmethod
+ def batch_employee(column_name, column_value, employee_id_list):
+ if column_value is None:
+ abort(400, ErrFormat.value_is_required)
+ if column_name in ['password', 'block']:
+ return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True)
+
+ elif column_name in ['department_id']:
+ return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value)
+
+ elif column_name in [
+ 'direct_supervisor_id', 'position_name'
+ ]:
+ return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False)
+
+ else:
+ abort(400, ErrFormat.column_name_not_support)
+
+ @staticmethod
+ def update_last_login_by_uid(uid, last_login=None):
+ employee = Employee.get_by(acl_uid=uid, first=True, to_dict=False)
+ if not employee:
+ return
+ if last_login:
+ try:
+ last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S')
+ except Exception as e:
+ last_login = datetime.now()
+ else:
+ last_login = datetime.now()
+
+ try:
+ employee.update(
+ last_login=last_login
+ )
+ return last_login
+ except Exception as e:
+ return
+
def get_user_map(key='uid', acl=None):
"""
@@ -514,6 +754,7 @@ def add_acl_user(self, **kwargs):
try:
existed = self.check_acl_user(user_data)
if not existed:
+ user_data['add_from'] = 'common'
return self.acl.create_user(user_data)
return existed
except Exception as e:
@@ -546,11 +787,14 @@ def create_single_with_import(self, **kwargs):
if existed:
return existed
- return Employee.create(
+ res = Employee.create(
**kwargs
)
+ refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
+ return res
- def get_department_by_name(self, d_name):
+ @staticmethod
+ def get_department_by_name(d_name):
return Department.get_by(first=True, department_name=d_name)
def get_end_department_id(self, department_name_list, department_name_map):
@@ -654,3 +898,75 @@ class EmployeeUpdateByUidForm(Form):
avatar = StringField(validators=[])
sex = StringField(validators=[])
mobile = StringField(validators=[])
+
+
+class GrantEmployeeACLPerm(object):
+ """
+ Grant ACL Permission After Create New Employee
+ """
+
+ def __init__(self, acl=None):
+ self.perms_by_create_resources_type = ['read', 'grant', 'delete', 'update']
+ self.perms_by_common_grant = ['read']
+ self.resource_name_list = ['公司信息', '公司架构', '通知设置']
+
+ self.acl = acl if acl else self.check_app('backend')
+ self.resources_types = self.acl.get_all_resources_types()
+ self.resources_type = self.get_resources_type()
+ self.resource_list = self.acl.get_resource_by_type(None, None, self.resources_type['id'])
+
+ @staticmethod
+ def check_app(app_name):
+ acl = ACLManager(app_name)
+ payload = dict(
+ name=app_name,
+ description=app_name
+ )
+ app = acl.validate_app()
+ if not app:
+ acl.create_app(payload)
+ return acl
+
+ def get_resources_type(self):
+ results = list(filter(lambda t: t['name'] == '操作权限', self.resources_types['groups']))
+ if len(results) == 0:
+ payload = dict(
+ app_id=self.acl.app_name,
+ name='操作权限',
+ description='',
+ perms=self.perms_by_create_resources_type
+ )
+ resource_type = self.acl.create_resources_type(payload)
+ else:
+ resource_type = results[0]
+ resource_type_id = resource_type['id']
+ existed_perms = self.resources_types.get('id2perms', {}).get(resource_type_id, [])
+ existed_perms = [p['name'] for p in existed_perms]
+ new_perms = []
+ for perm in self.perms_by_create_resources_type:
+ if perm not in existed_perms:
+ new_perms.append(perm)
+ if len(new_perms) > 0:
+ resource_type['perms'] = existed_perms + new_perms
+ self.acl.update_resources_type(resource_type_id, resource_type)
+
+ return resource_type
+
+ def grant(self, rid_list):
+ [self.grant_by_rid(rid) for rid in rid_list if rid > 0]
+
+ def grant_by_rid(self, rid, is_admin=False):
+ for name in self.resource_name_list:
+ resource = list(filter(lambda r: r['name'] == name, self.resource_list))
+ if len(resource) == 0:
+ payload = dict(
+ type_id=self.resources_type['id'],
+ app_id=self.acl.app_name,
+ name=name,
+ )
+ resource = self.acl.create_resource(payload)
+ else:
+ resource = resource[0]
+
+ perms = self.perms_by_create_resources_type if is_admin else self.perms_by_common_grant
+ self.acl.grant_resource(rid, resource['id'], perms)
diff --git a/acl-api/api/lib/common_setting/notice_config.py b/acl-api/api/lib/common_setting/notice_config.py
new file mode 100644
index 0000000..152eb53
--- /dev/null
+++ b/acl-api/api/lib/common_setting/notice_config.py
@@ -0,0 +1,165 @@
+import requests
+
+from api.lib.common_setting.const import BotNameMap
+from api.lib.common_setting.resp_format import ErrFormat
+from api.models.common_setting import CompanyInfo, NoticeConfig
+from wtforms import Form
+from wtforms import StringField
+from wtforms import validators
+from flask import abort, current_app
+
+
+class NoticeConfigCRUD(object):
+
+ @staticmethod
+ def add_notice_config(**kwargs):
+ platform = kwargs.get('platform')
+ NoticeConfigCRUD.check_platform(platform)
+ info = kwargs.get('info', {})
+ if 'name' not in info:
+ info['name'] = platform
+ kwargs['info'] = info
+ try:
+ NoticeConfigCRUD.update_messenger_config(**info)
+ res = NoticeConfig.create(
+ **kwargs
+ )
+ return res
+
+ except Exception as e:
+ return abort(400, str(e))
+
+ @staticmethod
+ def check_platform(platform):
+ NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and \
+ abort(400, ErrFormat.notice_platform_existed.format(platform))
+
+ @staticmethod
+ def edit_notice_config(_id, **kwargs):
+ existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
+ try:
+ info = kwargs.get('info', {})
+ if 'name' not in info:
+ info['name'] = existed.platform
+ kwargs['info'] = info
+ NoticeConfigCRUD.update_messenger_config(**info)
+
+ res = existed.update(**kwargs)
+ return res
+ except Exception as e:
+ return abort(400, str(e))
+
+ @staticmethod
+ def get_messenger_url():
+ from api.lib.common_setting.company_info import CompanyInfoCache
+ com_info = CompanyInfoCache.get()
+ if not com_info:
+ return
+ messenger = com_info.get('messenger', '')
+ if len(messenger) == 0:
+ return
+ if messenger[-1] == '/':
+ messenger = messenger[:-1]
+ return messenger
+
+ @staticmethod
+ def update_messenger_config(**kwargs):
+ try:
+ messenger = NoticeConfigCRUD.get_messenger_url()
+ if not messenger or len(messenger) == 0:
+ raise Exception(ErrFormat.notice_please_config_messenger_first)
+
+ url = f"{messenger}/v1/senders"
+ name = kwargs.get('name')
+ bot_list = kwargs.pop('bot', None)
+ for k, v in kwargs.items():
+ if isinstance(v, bool):
+ kwargs[k] = 'true' if v else 'false'
+ else:
+ kwargs[k] = str(v)
+
+ payload = {name: [kwargs]}
+ current_app.logger.info(f"update_messenger_config: {url}, {payload}")
+ res = requests.put(url, json=payload, timeout=2)
+ current_app.logger.info(f"update_messenger_config: {res.status_code}, {res.text}")
+
+ if not bot_list or len(bot_list) == 0:
+ return
+ bot_name = BotNameMap.get(name)
+ payload = {bot_name: bot_list}
+ current_app.logger.info(f"update_messenger_config: {url}, {payload}")
+ bot_res = requests.put(url, json=payload, timeout=2)
+ current_app.logger.info(f"update_messenger_config: {bot_res.status_code}, {bot_res.text}")
+
+ except Exception as e:
+ return abort(400, str(e))
+
+ @staticmethod
+ def get_notice_config_by_id(_id):
+ return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or \
+ abort(400,
+ ErrFormat.notice_not_existed.format(_id))
+
+ @staticmethod
+ def get_all():
+ return NoticeConfig.get_by(to_dict=True)
+
+ @staticmethod
+ def test_send_email(receive_address, **kwargs):
+ messenger = NoticeConfigCRUD.get_messenger_url()
+ if not messenger or len(messenger) == 0:
+ abort(400, ErrFormat.notice_please_config_messenger_first)
+ url = f"{messenger}/v1/message"
+
+ recipient_email = receive_address
+
+ subject = 'Test Email'
+ body = 'This is a test email'
+ payload = {
+ "sender": 'email',
+ "msgtype": "text/plain",
+ "title": subject,
+ "content": body,
+ "tos": [recipient_email],
+ }
+ current_app.logger.info(f"test_send_email: {url}, {payload}")
+ response = requests.post(url, json=payload)
+ if response.status_code != 200:
+ abort(400, response.text)
+
+ return 1
+
+ @staticmethod
+ def get_app_bot():
+ result = []
+ for notice_app in NoticeConfig.get_by(to_dict=False):
+ if notice_app.platform in ['email']:
+ continue
+ info = notice_app.info
+ name = info.get('name', '')
+ if name not in BotNameMap:
+ continue
+ result.append(dict(
+ name=info.get('name', ''),
+ label=info.get('label', ''),
+ bot=info.get('bot', []),
+ ))
+ return result
+
+
+class NoticeConfigForm(Form):
+ platform = StringField(validators=[
+ validators.DataRequired(message="平台 不能为空"),
+ validators.Length(max=255),
+ ])
+ info = StringField(validators=[
+ validators.DataRequired(message="信息 不能为空"),
+ validators.Length(max=255),
+ ])
+
+
+class NoticeConfigUpdateForm(Form):
+ info = StringField(validators=[
+ validators.DataRequired(message="信息 不能为空"),
+ validators.Length(max=255),
+ ])
diff --git a/acl-api/api/lib/common_setting/resp_format.py b/acl-api/api/lib/common_setting/resp_format.py
index 1d3b8d9..6561d65 100644
--- a/acl-api/api/lib/common_setting/resp_format.py
+++ b/acl-api/api/lib/common_setting/resp_format.py
@@ -1,56 +1,84 @@
# -*- coding:utf-8 -*-
+from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat):
- company_info_is_already_existed = "公司信息已存在!无法创建"
-
- no_file_part = "没有文件部分"
- file_is_required = "文件是必须的"
-
- direct_supervisor_is_not_self = "直属上级不能是自己"
- parent_department_is_not_self = "上级部门不能是自己"
- employee_list_is_empty = "员工列表为空"
-
- column_name_not_support = "不支持的列名"
- password_is_required = "密码不能为空"
- employee_acl_rid_is_zero = "员工ACL角色ID不能为0"
-
- generate_excel_failed = "生成excel失败: {}"
- rename_columns_failed = "字段转换为中文失败: {}"
- cannot_block_this_employee_is_other_direct_supervisor = "该员工是其他员工的直属上级, 不能禁用"
- cannot_block_this_employee_is_department_manager = "该员工是部门负责人, 不能禁用"
- employee_id_not_found = "员工ID [{}] 不存在"
- value_is_required = "值是必须的"
- email_already_exists = "邮箱 [{}] 已存在"
- query_column_none_keep_value_empty = "查询 {} 空值时请保持value为空"
- not_support_operator = "不支持的操作符: {}"
- not_support_relation = "不支持的关系: {}"
- conditions_field_missing = "conditions内元素字段缺失,请检查!"
- datetime_format_error = "{} 格式错误,应该为:%Y-%m-%d %H:%M:%S"
- department_level_relation_error = "部门层级关系不正确"
- delete_reserved_department_name = "保留部门,无法删除!"
- department_id_is_required = "部门ID是必须的"
- department_list_is_required = "部门列表是必须的"
- cannot_to_be_parent_department = "{} 不能设置为上级部门"
- department_id_not_found = "部门ID [{}] 不存在"
- parent_department_id_must_more_than_zero = "上级部门ID必须大于0"
- department_name_already_exists = "部门名称 [{}] 已存在"
- new_department_is_none = "新部门是空的"
-
- acl_edit_user_failed = "ACL 修改用户失败: {}"
- acl_uid_not_found = "ACL 用户UID [{}] 不存在"
- acl_add_user_failed = "ACL 添加用户失败: {}"
- acl_add_role_failed = "ACL 添加角色失败: {}"
- acl_update_role_failed = "ACL 更新角色失败: {}"
- acl_get_all_users_failed = "ACL 获取所有用户失败: {}"
- acl_remove_user_from_role_failed = "ACL 从角色中移除用户失败: {}"
- acl_add_user_to_role_failed = "ACL 添加用户到角色失败: {}"
- acl_import_user_failed = "ACL 导入用户[{}]失败: {}"
-
- nickname_is_required = "用户名不能为空"
- username_is_required = "username不能为空"
- email_is_required = "邮箱不能为空"
- email_format_error = "邮箱格式错误"
+ company_info_is_already_existed = _l("Company info already existed") # 公司信息已存在!无法创建
+ no_file_part = _l("No file part") # 没有文件部分
+ file_is_required = _l("File is required") # 文件是必须的
+ file_not_found = _l("File not found") # 文件不存在
+ file_type_not_allowed = _l("File type not allowed") # 文件类型不允许
+ upload_failed = _l("Upload failed: {}") # 上传失败: {}
+
+ direct_supervisor_is_not_self = _l("Direct supervisor is not self") # 直属上级不能是自己
+ parent_department_is_not_self = _l("Parent department is not self") # 上级部门不能是自己
+ employee_list_is_empty = _l("Employee list is empty") # 员工列表为空
+
+ column_name_not_support = _l("Column name not support") # 不支持的列名
+ password_is_required = _l("Password is required") # 密码是必须的
+ employee_acl_rid_is_zero = _l("Employee acl rid is zero") # 员工ACL角色ID不能为0
+
+ generate_excel_failed = _l("Generate excel failed: {}") # 生成excel失败: {}
+ rename_columns_failed = _l("Rename columns failed: {}") # 重命名字段失败: {}
+ cannot_block_this_employee_is_other_direct_supervisor = _l(
+ "Cannot block this employee is other direct supervisor") # 该员工是其他员工的直属上级, 不能禁用
+ cannot_block_this_employee_is_department_manager = _l(
+ "Cannot block this employee is department manager") # 该员工是部门负责人, 不能禁用
+ employee_id_not_found = _l("Employee id [{}] not found") # 员工ID [{}] 不存在
+ value_is_required = _l("Value is required") # 值是必须的
+ email_already_exists = _l("Email already exists") # 邮箱已存在
+ query_column_none_keep_value_empty = _l("Query {} none keep value empty") # 查询 {} 空值时请保持value为空"
+ not_support_operator = _l("Not support operator: {}") # 不支持的操作符: {}
+ not_support_relation = _l("Not support relation: {}") # 不支持的关系: {}
+ conditions_field_missing = _l("Conditions field missing") # conditions内元素字段缺失,请检查!
+ datetime_format_error = _l("Datetime format error: {}") # {} 格式错误,应该为:%Y-%m-%d %H:%M:%S
+ department_level_relation_error = _l("Department level relation error") # 部门层级关系不正确
+ delete_reserved_department_name = _l("Delete reserved department name") # 保留部门,无法删除!
+ department_id_is_required = _l("Department id is required") # 部门ID是必须的
+ department_list_is_required = _l("Department list is required") # 部门列表是必须的
+ cannot_to_be_parent_department = _l("{} Cannot to be parent department") # 不能设置为上级部门
+ department_id_not_found = _l("Department id [{}] not found") # 部门ID [{}] 不存在
+ parent_department_id_must_more_than_zero = _l("Parent department id must more than zero") # 上级部门ID必须大于0
+ department_name_already_exists = _l("Department name [{}] already exists") # 部门名称 [{}] 已存在
+ new_department_is_none = _l("New department is none") # 新部门是空的
+
+ acl_edit_user_failed = _l("ACL edit user failed: {}") # ACL 修改用户失败: {}
+ acl_uid_not_found = _l("ACL uid not found: {}") # ACL 用户UID [{}] 不存在
+ acl_add_user_failed = _l("ACL add user failed: {}") # ACL 添加用户失败: {}
+ acl_add_role_failed = _l("ACL add role failed: {}") # ACL 添加角色失败: {}
+ acl_update_role_failed = _l("ACL update role failed: {}") # ACL 更新角色失败: {}
+ acl_get_all_users_failed = _l("ACL get all users failed: {}") # ACL 获取所有用户失败: {}
+ acl_remove_user_from_role_failed = _l("ACL remove user from role failed: {}") # ACL 从角色中移除用户失败: {}
+ acl_add_user_to_role_failed = _l("ACL add user to role failed: {}") # ACL 添加用户到角色失败: {}
+ acl_import_user_failed = _l("ACL import user failed: {}") # ACL 导入用户失败: {}
+
+ nickname_is_required = _l("Nickname is required") # 昵称不能为空
+ username_is_required = _l("Username is required") # 用户名不能为空
+ email_is_required = _l("Email is required") # 邮箱不能为空
+ email_format_error = _l("Email format error") # 邮箱格式错误
+ email_send_timeout = _l("Email send timeout") # 邮件发送超时
+
+ common_data_not_found = _l("Common data not found {} ") # ID {} 找不到记录
+ common_data_already_existed = _l("Common data {} already existed") # {} 已存在
+ notice_platform_existed = _l("Notice platform {} existed") # {} 已存在
+ notice_not_existed = _l("Notice {} not existed") # {} 配置项不存在
+ notice_please_config_messenger_first = _l("Notice please config messenger first") # 请先配置messenger URL
+ notice_bind_err_with_empty_mobile = _l("Notice bind err with empty mobile") # 绑定错误,手机号为空
+ notice_bind_failed = _l("Notice bind failed: {}") # 绑定失败: {}
+ notice_bind_success = _l("Notice bind success") # 绑定成功
+ notice_remove_bind_success = _l("Notice remove bind success") # 解绑成功
+
+ not_support_test = _l("Not support test type: {}") # 不支持的测试类型: {}
+ not_support_auth_type = _l("Not support auth type: {}") # 不支持的认证类型: {}
+ ldap_server_connect_timeout = _l("LDAP server connect timeout") # LDAP服务器连接超时
+ ldap_server_connect_not_available = _l("LDAP server connect not available") # LDAP服务器连接不可用
+ ldap_test_unknown_error = _l("LDAP test unknown error: {}") # LDAP测试未知错误: {}
+ common_data_not_support_auth_type = _l("Common data not support auth type: {}") # 通用数据不支持auth类型: {}
+ ldap_test_username_required = _l("LDAP test username required") # LDAP测试用户名必填
+
+ company_wide = _l("Company wide") # 全公司
+
+ resource_no_permission = _l("No permission to access resource {}, perm {} ") # 没有权限访问 {} 资源的 {} 权限"
diff --git a/acl-api/api/lib/common_setting/role_perm_base.py b/acl-api/api/lib/common_setting/role_perm_base.py
new file mode 100644
index 0000000..c4fe48f
--- /dev/null
+++ b/acl-api/api/lib/common_setting/role_perm_base.py
@@ -0,0 +1,64 @@
+class OperationPermission(object):
+
+ def __init__(self, resource_perms):
+ for _r in resource_perms:
+ setattr(self, _r['page'], _r['page'])
+ for _p in _r['perms']:
+ setattr(self, _p, _p)
+
+
+class BaseApp(object):
+ resource_type_name = 'OperationPermission'
+ all_resource_perms = []
+
+ def __init__(self):
+ self.admin_name = None
+ self.roles = []
+ self.app_name = 'acl'
+ self.require_create_resource_type = self.resource_type_name
+ self.extra_create_resource_type_list = []
+
+ self.op = None
+
+ @staticmethod
+ def format_role(role_name, role_type, acl_rid, resource_perms, description=''):
+ return dict(
+ role_name=role_name,
+ role_type=role_type,
+ acl_rid=acl_rid,
+ description=description,
+ resource_perms=resource_perms,
+ )
+
+
+class CMDBApp(BaseApp):
+ all_resource_perms = [
+ {"page": "Big_Screen", "page_cn": "大屏", "perms": ["read"]},
+ {"page": "Dashboard", "page_cn": "仪表盘", "perms": ["read"]},
+ {"page": "Resource_Search", "page_cn": "资源搜索", "perms": ["read"]},
+ {"page": "Auto_Discovery_Pool", "page_cn": "自动发现池", "perms": ["read"]},
+ {"page": "My_Subscriptions", "page_cn": "我的订阅", "perms": ["read"]},
+ {"page": "Bulk_Import", "page_cn": "批量导入", "perms": ["read"]},
+ {"page": "Model_Configuration", "page_cn": "模型配置",
+ "perms": ["read", "create_CIType", "create_CIType_group", "update_CIType_group",
+ "delete_CIType_group", "download_CIType"]},
+ {"page": "Backend_Management", "page_cn": "后台管理", "perms": ["read"]},
+ {"page": "Customized_Dashboard", "page_cn": "定制仪表盘", "perms": ["read"]},
+ {"page": "Service_Tree_Definition", "page_cn": "服务树定义", "perms": ["read"]},
+ {"page": "Model_Relationships", "page_cn": "模型关系", "perms": ["read"]},
+ {"page": "Operation_Audit", "page_cn": "操作审计", "perms": ["read"]},
+ {"page": "Relationship_Types", "page_cn": "关系类型", "perms": ["read"]},
+ {"page": "Auto_Discovery", "page_cn": "自动发现", "perms": ["read", "create_plugin", "update_plugin", "delete_plugin"]},
+ {"page": "TopologyView", "page_cn": "拓扑视图",
+ "perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
+ "create_topology_view"],
+ },
+ ]
+
+ def __init__(self):
+ super().__init__()
+
+ self.admin_name = 'cmdb_admin'
+ self.app_name = 'cmdb'
+
+ self.op = OperationPermission(self.all_resource_perms)
diff --git a/acl-api/api/lib/common_setting/upload_file.py b/acl-api/api/lib/common_setting/upload_file.py
index d7eca29..f63bfc0 100644
--- a/acl-api/api/lib/common_setting/upload_file.py
+++ b/acl-api/api/lib/common_setting/upload_file.py
@@ -1,11 +1,18 @@
+import base64
import uuid
+import os
+from io import BytesIO
+
+from flask import abort, current_app
+import lz4.frame
from api.lib.common_setting.utils import get_cur_time_str
+from api.models.common_setting import CommonFile
+from api.lib.common_setting.resp_format import ErrFormat
def allowed_file(filename, allowed_extensions):
- return '.' in filename and \
- filename.rsplit('.', 1)[1].lower() in allowed_extensions
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
def generate_new_file_name(name):
@@ -13,4 +20,75 @@ def generate_new_file_name(name):
prev_name = ''.join(name.split(f".{ext}")[:-1])
uid = str(uuid.uuid4())
cur_str = get_cur_time_str('_')
+
return f"{prev_name}_{cur_str}_{uid}.{ext}"
+
+
+class CommonFileCRUD:
+ @staticmethod
+ def add_file(**kwargs):
+ return CommonFile.create(**kwargs)
+
+ @staticmethod
+ def get_file(file_name, to_str=False):
+ existed = CommonFile.get_by(file_name=file_name, first=True, to_dict=False)
+ if not existed:
+ abort(400, ErrFormat.file_not_found)
+
+ uncompressed_data = lz4.frame.decompress(existed.binary)
+
+ return base64.b64encode(uncompressed_data).decode('utf-8') if to_str else BytesIO(uncompressed_data)
+
+ @staticmethod
+ def sync_file_to_db():
+ for p in ['UPLOAD_DIRECTORY_FULL']:
+ upload_path = current_app.config.get(p, None)
+ if not upload_path:
+ continue
+ for root, dirs, files in os.walk(upload_path):
+ for file in files:
+ file_path = os.path.join(root, file)
+ if not os.path.isfile(file_path):
+ continue
+
+ existed = CommonFile.get_by(file_name=file, first=True, to_dict=False)
+ if existed:
+ continue
+ with open(file_path, 'rb') as f:
+ data = f.read()
+ compressed_data = lz4.frame.compress(data)
+ try:
+ CommonFileCRUD.add_file(
+ origin_name=file,
+ file_name=file,
+ binary=compressed_data
+ )
+
+ current_app.logger.info(f'sync file {file} to db')
+ except Exception as e:
+ current_app.logger.error(f'sync file {file} to db error: {e}')
+
+ def get_file_binary_str(self, file_name):
+ return self.get_file(file_name, True)
+
+ def save_str_to_file(self, file_name, str_data):
+ try:
+ self.get_file(file_name)
+ current_app.logger.info(f'file {file_name} already exists')
+ return
+ except Exception as e:
+ # file not found
+ pass
+
+ bytes_data = base64.b64decode(str_data)
+ compressed_data = lz4.frame.compress(bytes_data)
+
+ try:
+ self.add_file(
+ origin_name=file_name,
+ file_name=file_name,
+ binary=compressed_data
+ )
+ current_app.logger.info(f'save_str_to_file {file_name} success')
+ except Exception as e:
+ current_app.logger.error(f"save_str_to_file error: {e}")
diff --git a/acl-api/api/lib/common_setting/utils.py b/acl-api/api/lib/common_setting/utils.py
index 138a315..d3db5bb 100644
--- a/acl-api/api/lib/common_setting/utils.py
+++ b/acl-api/api/lib/common_setting/utils.py
@@ -1,5 +1,10 @@
# -*- coding:utf-8 -*-
from datetime import datetime
+from flask import current_app
+from sqlalchemy import inspect, text
+from sqlalchemy.dialects.mysql import ENUM
+
+from api.extensions import db
def get_cur_time_str(split_flag='-'):
@@ -23,3 +28,115 @@ def all(cls):
if not attr.startswith("_") and not callable(getattr(cls, attr))
}
return cls._ALL_
+
+
+class CheckNewColumn(object):
+
+ def __init__(self):
+ self.engine = db.get_engine()
+ self.inspector = inspect(self.engine)
+ self.table_names = self.inspector.get_table_names()
+
+ @staticmethod
+ def get_model_by_table_name(_table_name):
+ registry = getattr(db.Model, 'registry', None)
+ class_registry = getattr(registry, '_class_registry', None)
+ for _model in class_registry.values():
+ if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name:
+ return _model
+ return None
+
+ def run(self):
+ for table_name in self.table_names:
+ self.check_by_table(table_name)
+
+ def check_by_table(self, table_name):
+ existed_columns = self.inspector.get_columns(table_name)
+ enum_columns = []
+ existed_column_name_list = []
+ for c in existed_columns:
+ if isinstance(c['type'], ENUM):
+ enum_columns.append(c['name'])
+ existed_column_name_list.append(c['name'])
+
+ model = self.get_model_by_table_name(table_name)
+ if model is None:
+ return
+ model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns')
+ for column in model_columns:
+ if column.name not in existed_column_name_list:
+ add_res = self.add_new_column(table_name, column)
+ if not add_res:
+ continue
+
+ current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
+
+ if column.name in enum_columns:
+ enum_columns.remove(column.name)
+
+ self.add_new_index(table_name, column)
+
+ if len(enum_columns) > 0:
+ self.check_enum_column(enum_columns, existed_columns, model_columns, table_name)
+
+ def add_new_column(self, target_table_name, new_column):
+ try:
+ column_type = new_column.type.compile(self.engine.dialect)
+ default_value = new_column.default.arg if new_column.default else None
+
+ sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + f"`{new_column.name}`" + " " + column_type
+ if new_column.comment:
+ sql += f" comment '{new_column.comment}'"
+
+ if column_type == 'JSON':
+ pass
+ elif default_value:
+ if column_type.startswith('VAR') or column_type.startswith('Text'):
+ if default_value is None or len(default_value) == 0:
+ pass
+ else:
+ sql += f" DEFAULT {default_value}"
+
+ sql = text(sql)
+ db.session.execute(sql)
+ return True
+ except Exception as e:
+ err = f"add_new_column [{new_column.name}] to table [{target_table_name}] err: {e}"
+ current_app.logger.error(err)
+ return False
+
+ @staticmethod
+ def add_new_index(target_table_name, new_column):
+ try:
+ if new_column.index:
+ index_name = f"{target_table_name}_{new_column.name}"
+ sql = "CREATE INDEX " + f"{index_name}" + " ON " + target_table_name + " (" + new_column.name + ")"
+ db.session.execute(sql)
+ current_app.logger.info(f"add new index [{index_name}] in table [{target_table_name}] success.")
+
+ return True
+ except Exception as e:
+ err = f"add_new_index [{new_column.name}] to table [{target_table_name}] err: {e}"
+ current_app.logger.error(err)
+ return False
+
+ @staticmethod
+ def check_enum_column(enum_columns, existed_columns, model_columns, table_name):
+ for column_name in enum_columns:
+ try:
+ enum_column = list(filter(lambda x: x['name'] == column_name, existed_columns))[0]
+ old_enum_value = enum_column.get('type', {}).enums
+ target_column = list(filter(lambda x: x.name == column_name, model_columns))[0]
+ new_enum_value = target_column.type.enums
+
+ if set(old_enum_value) == set(new_enum_value):
+ continue
+
+ enum_values_str = ','.join(["'{}'".format(value) for value in new_enum_value])
+ sql = f"ALTER TABLE {table_name} MODIFY COLUMN" + f"`{column_name}`" + f" enum({enum_values_str})"
+ db.session.execute(sql)
+ current_app.logger.info(
+ f"modify column [{column_name}] ENUM: {new_enum_value} in table [{table_name}] success.")
+ except Exception as e:
+ current_app.logger.error(
+ f"modify column ENUM [{column_name}] in table [{table_name}] err: {e}")
diff --git a/acl-api/api/lib/database.py b/acl-api/api/lib/database.py
index 5145bd4..3bb7aa8 100644
--- a/acl-api/api/lib/database.py
+++ b/acl-api/api/lib/database.py
@@ -10,17 +10,21 @@
class FormatMixin(object):
def to_dict(self):
- res = dict([(k, getattr(self, k) if not isinstance(
- getattr(self, k), (datetime.datetime, datetime.date, datetime.time)) else str(
- getattr(self, k))) for k in getattr(self, "__mapper__").c.keys()])
- # FIXME: getattr(cls, "__table__").columns k.name
+ res = dict()
+ for k in getattr(self, "__mapper__").c.keys():
+ if k in {'password', '_password', 'secret', '_secret'}:
+ continue
- res.pop('password', None)
- res.pop('_password', None)
- res.pop('secret', None)
+ if k.startswith('_'):
+ k = k[1:]
+
+ if not isinstance(getattr(self, k), (datetime.datetime, datetime.date, datetime.time)):
+ res[k] = getattr(self, k)
+ else:
+ res[k] = str(getattr(self, k))
return res
-
+
@classmethod
def from_dict(cls, **kwargs):
from sqlalchemy.sql.sqltypes import Time, Date, DateTime
@@ -80,17 +84,17 @@ def delete(self, flush=False, commit=True):
db.session.rollback()
raise CommitException(str(e))
- def soft_delete(self, flush=False):
+ def soft_delete(self, flush=False, commit=True):
setattr(self, "deleted", True)
setattr(self, "deleted_at", datetime.datetime.now())
- self.save(flush=flush)
+ self.save(flush=flush, commit=commit)
@classmethod
def get_by_id(cls, _id):
if any((isinstance(_id, six.string_types) and _id.isdigit(),
isinstance(_id, (six.integer_types, float))), ):
obj = getattr(cls, "query").get(int(_id))
- if obj and not obj.deleted:
+ if obj and not getattr(obj, 'deleted', False):
return obj
@classmethod
@@ -138,8 +142,11 @@ def get_by(cls, first=False,
return result[0] if first and result else (None if first else result)
@classmethod
- def get_by_like(cls, to_dict=True, **kwargs):
+ def get_by_like(cls, to_dict=True, deleted=False, **kwargs):
query = db.session.query(cls)
+ if hasattr(cls, "deleted") and deleted is not None:
+ query = query.filter(cls.deleted.is_(deleted))
+
for k, v in kwargs.items():
query = query.filter(getattr(cls, k).ilike('%{0}%'.format(v)))
return [i.to_dict() if to_dict else i for i in query]
diff --git a/acl-api/api/lib/decorator.py b/acl-api/api/lib/decorator.py
index 94b0ce5..26247e6 100644
--- a/acl-api/api/lib/decorator.py
+++ b/acl-api/api/lib/decorator.py
@@ -4,8 +4,14 @@
from functools import wraps
from flask import abort
+from flask import current_app
from flask import request
+from sqlalchemy.exc import InvalidRequestError
+from sqlalchemy.exc import OperationalError
+from sqlalchemy.exc import PendingRollbackError
+from sqlalchemy.exc import StatementError
+from api.extensions import db
from api.lib.resp_format import CommonErrFormat
@@ -55,8 +61,8 @@ def wrapper(*args, **kwargs):
if exclude_args and arg in exclude_args:
continue
- if attr.type.python_type == str and attr.type.length and \
- len(request.values[arg] or '') > attr.type.length:
+ if attr.type.python_type == str and attr.type.length and (
+ len(request.values[arg] or '') > attr.type.length):
return abort(400, CommonErrFormat.argument_str_length_limit.format(arg, attr.type.length))
elif attr.type.python_type in (int, float) and request.values[arg]:
@@ -70,3 +76,43 @@ def wrapper(*args, **kwargs):
return wrapper
return decorate
+
+
+def reconnect_db(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except (StatementError, OperationalError, InvalidRequestError) as e:
+ error_msg = str(e)
+ if 'Lost connection' in error_msg or 'reconnect until invalid transaction' in error_msg or \
+ 'can be emitted within this transaction' in error_msg:
+ current_app.logger.info('[reconnect_db] lost connect rollback then retry')
+ db.session.rollback()
+ return func(*args, **kwargs)
+ else:
+ raise e
+ except Exception as e:
+ raise e
+
+ return wrapper
+
+
+def _flush_db():
+ try:
+ db.session.commit()
+ except (StatementError, OperationalError, InvalidRequestError, PendingRollbackError):
+ db.session.rollback()
+
+
+def flush_db(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ _flush_db()
+ return func(*args, **kwargs)
+
+ return wrapper
+
+
+def run_flush_db():
+ _flush_db()
diff --git a/acl-api/api/lib/perm/acl/acl.py b/acl-api/api/lib/perm/acl/acl.py
index 74f3011..5af60d5 100644
--- a/acl-api/api/lib/perm/acl/acl.py
+++ b/acl-api/api/lib/perm/acl/acl.py
@@ -5,8 +5,10 @@
import requests
import six
-from flask import abort, session
-from flask import current_app, request
+from flask import abort
+from flask import current_app
+from flask import request
+from flask import session
from flask_login import current_user
from api.extensions import cache
@@ -85,8 +87,8 @@ def _get_role(self, name):
if user:
return Role.get_by(name=name, uid=user.uid, first=True, to_dict=False)
- return Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or \
- Role.get_by(name=name, first=True, to_dict=False)
+ return (Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or
+ Role.get_by(name=name, first=True, to_dict=False))
def add_resource(self, name, resource_type_name=None):
resource_type = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
@@ -115,15 +117,15 @@ def grant_resource_to_role(self, name, role, resource_type_name=None, permission
if group:
PermissionCRUD.grant(role.id, permissions, group_id=group.id)
- def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
+ def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True):
resource = self._get_resource(name, resource_type_name)
if resource:
- PermissionCRUD.grant(rid, permissions, resource_id=resource.id)
+ PermissionCRUD.grant(rid, permissions, resource_id=resource.id, rebuild=rebuild)
else:
group = self._get_resource_group(name)
if group:
- PermissionCRUD.grant(rid, permissions, group_id=group.id)
+ PermissionCRUD.grant(rid, permissions, group_id=group.id, rebuild=rebuild)
def revoke_resource_from_role(self, name, role, resource_type_name=None, permissions=None):
resource = self._get_resource(name, resource_type_name)
@@ -136,26 +138,26 @@ def revoke_resource_from_role(self, name, role, resource_type_name=None, permiss
if group:
PermissionCRUD.revoke(role.id, permissions, group_id=group.id)
- def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
+ def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True):
resource = self._get_resource(name, resource_type_name)
if resource:
- PermissionCRUD.revoke(rid, permissions, resource_id=resource.id)
+ PermissionCRUD.revoke(rid, permissions, resource_id=resource.id, rebuild=rebuild)
else:
group = self._get_resource_group(name)
if group:
- PermissionCRUD.revoke(rid, permissions, group_id=group.id)
+ PermissionCRUD.revoke(rid, permissions, group_id=group.id, rebuild=rebuild)
- def del_resource(self, name, resource_type_name=None):
+ def del_resource(self, name, resource_type_name=None, rebuild=True):
resource = self._get_resource(name, resource_type_name)
if resource:
- ResourceCRUD.delete(resource.id)
+ return ResourceCRUD.delete(resource.id, rebuild=rebuild)
- def has_permission(self, resource_name, resource_type, perm, resource_id=None):
+ def has_permission(self, resource_name, resource_type, perm, resource_id=None, rid=None):
if is_app_admin(self.app_id):
return True
- role = self._get_role(current_user.username)
+ role = self._get_role(current_user.username) if rid is None else RoleCache.get(rid)
role or abort(404, ErrFormat.role_not_found.format(current_user.username))
diff --git a/acl-api/api/lib/perm/acl/app.py b/acl-api/api/lib/perm/acl/app.py
index 93772bc..cf36432 100644
--- a/acl-api/api/lib/perm/acl/app.py
+++ b/acl-api/api/lib/perm/acl/app.py
@@ -8,7 +8,9 @@
from flask import current_app
from api.extensions import db
-from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.audit import AuditOperateType
+from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.resp_format import ErrFormat
from api.models.acl import App
diff --git a/acl-api/api/lib/perm/acl/audit.py b/acl-api/api/lib/perm/acl/audit.py
index 921d619..dfa44be 100644
--- a/acl-api/api/lib/perm/acl/audit.py
+++ b/acl-api/api/lib/perm/acl/audit.py
@@ -1,16 +1,29 @@
# -*- coding:utf-8 -*-
+
+import datetime
import itertools
import json
from enum import Enum
from typing import List
-from flask import g, has_request_context, request
+from flask import has_request_context
+from flask import request
from flask_login import current_user
from sqlalchemy import func
+from api.extensions import db
from api.lib.perm.acl import AppCache
-from api.models.acl import AuditPermissionLog, AuditResourceLog, AuditRoleLog, AuditTriggerLog, Permission, Resource, \
- ResourceGroup, ResourceType, Role, RolePermission
+from api.models.acl import AuditLoginLog
+from api.models.acl import AuditPermissionLog
+from api.models.acl import AuditResourceLog
+from api.models.acl import AuditRoleLog
+from api.models.acl import AuditTriggerLog
+from api.models.acl import Permission
+from api.models.acl import Resource
+from api.models.acl import ResourceGroup
+from api.models.acl import ResourceType
+from api.models.acl import Role
+from api.models.acl import RolePermission
class AuditScope(str, Enum):
@@ -49,9 +62,7 @@ class AuditCRUD(object):
@staticmethod
def get_current_operate_uid(uid=None):
-
- user_id = uid or (hasattr(g, 'user') and getattr(current_user, 'uid', None)) \
- or getattr(current_user, 'user_id', None)
+ user_id = uid or (getattr(current_user, 'uid', None)) or getattr(current_user, 'user_id', None)
if has_request_context() and request.headers.get('X-User-Id'):
_user_id = request.headers['X-User-Id']
@@ -93,11 +104,8 @@ def search_permission(app_id, q=None, page=1, page_size=10, start=None, end=None
criterion.append(AuditPermissionLog.operate_type == v)
records = AuditPermissionLog.query.filter(
- AuditPermissionLog.deleted == 0,
- *criterion) \
- .order_by(AuditPermissionLog.id.desc()) \
- .offset((page - 1) * page_size) \
- .limit(page_size).all()
+ AuditPermissionLog.deleted == 0, *criterion).order_by(
+ AuditPermissionLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -160,10 +168,8 @@ def search_role(app_id, q=None, page=1, page_size=10, start=None, end=None):
elif k == 'operate_type':
criterion.append(AuditRoleLog.operate_type == v)
- records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion) \
- .order_by(AuditRoleLog.id.desc()) \
- .offset((page - 1) * page_size) \
- .limit(page_size).all()
+ records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion).order_by(
+ AuditRoleLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -225,11 +231,8 @@ def search_resource(app_id, q=None, page=1, page_size=10, start=None, end=None):
criterion.append(AuditResourceLog.operate_type == v)
records = AuditResourceLog.query.filter(
- AuditResourceLog.deleted == 0,
- *criterion) \
- .order_by(AuditResourceLog.id.desc()) \
- .offset((page - 1) * page_size) \
- .limit(page_size).all()
+ AuditResourceLog.deleted == 0, *criterion).order_by(
+ AuditResourceLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -259,11 +262,8 @@ def search_trigger(app_id, q=None, page=1, page_size=10, start=None, end=None):
criterion.append(AuditTriggerLog.operate_type == v)
records = AuditTriggerLog.query.filter(
- AuditTriggerLog.deleted == 0,
- *criterion) \
- .order_by(AuditTriggerLog.id.desc()) \
- .offset((page - 1) * page_size) \
- .limit(page_size).all()
+ AuditTriggerLog.deleted == 0, *criterion).order_by(
+ AuditTriggerLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -288,6 +288,27 @@ def search_trigger(app_id, q=None, page=1, page_size=10, start=None, end=None):
return data
+ @staticmethod
+ def search_login(_, q=None, page=1, page_size=10, start=None, end=None):
+ query = db.session.query(AuditLoginLog)
+
+ if start:
+ query = query.filter(AuditLoginLog.login_at >= start)
+ if end:
+ query = query.filter(AuditLoginLog.login_at <= end)
+
+ if q:
+ query = query.filter(AuditLoginLog.username == q)
+
+ records = query.order_by(
+ AuditLoginLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
+
+ data = {
+ 'data': [r.to_dict() for r in records],
+ }
+
+ return data
+
@classmethod
def add_role_log(cls, app_id, operate_type: AuditOperateType,
scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict,
@@ -353,3 +374,32 @@ def add_trigger_log(cls, app_id, trigger_id, operate_type: AuditOperateType,
AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id,
operate_type=operate_type.value,
origin=origin, current=current, extra=extra, source=source.value)
+
+ @classmethod
+ def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
+ if _id is not None:
+ existed = AuditLoginLog.get_by_id(_id)
+ if existed is not None:
+ existed.update(logout_at=logout_at)
+ return
+
+ payload = dict(username=username,
+ is_ok=is_ok,
+ description=description,
+ logout_at=logout_at,
+ ip=(ip or request.headers.get('X-Forwarded-For') or
+ request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
+ browser=browser or request.headers.get('User-Agent'),
+ channel=request.values.get('channel', 'web'),
+ )
+
+ if logout_at is None:
+ payload['login_at'] = datetime.datetime.now()
+
+ try:
+ from api.lib.common_setting.employee import EmployeeCRUD
+ EmployeeCRUD.update_last_login_by_uid(current_user.uid)
+ except:
+ pass
+
+ return AuditLoginLog.create(**payload).id
diff --git a/acl-api/api/lib/perm/acl/cache.py b/acl-api/api/lib/perm/acl/cache.py
index b490759..42acbfb 100644
--- a/acl-api/api/lib/perm/acl/cache.py
+++ b/acl-api/api/lib/perm/acl/cache.py
@@ -2,10 +2,12 @@
import msgpack
+import redis_lock
from api.extensions import cache
from api.extensions import db
-from api.lib.utils import Lock
+from api.extensions import rd
+from api.lib.decorator import flush_db
from api.models.acl import App
from api.models.acl import Permission
from api.models.acl import Resource
@@ -60,15 +62,15 @@ class UserCache(object):
@classmethod
def get(cls, key):
- user = cache.get(cls.PREFIX_ID.format(key)) or \
- cache.get(cls.PREFIX_NAME.format(key)) or \
- cache.get(cls.PREFIX_NICK.format(key)) or \
- cache.get(cls.PREFIX_WXID.format(key))
+ user = (cache.get(cls.PREFIX_ID.format(key)) or
+ cache.get(cls.PREFIX_NAME.format(key)) or
+ cache.get(cls.PREFIX_NICK.format(key)) or
+ cache.get(cls.PREFIX_WXID.format(key)))
if not user:
- user = User.query.get(key) or \
- User.query.get_by_username(key) or \
- User.query.get_by_nickname(key) or \
- User.query.get_by_wxid(key)
+ user = (User.query.get(key) or
+ User.query.get_by_username(key) or
+ User.query.get_by_nickname(key) or
+ User.query.get_by_wxid(key))
if user:
cls.set(user)
@@ -136,14 +138,14 @@ def get(cls, app_id):
@classmethod
def add(cls, rid, app_id):
- with Lock('HasResourceRoleCache'):
+ with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id)
c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod
def remove(cls, rid, app_id):
- with Lock('HasResourceRoleCache'):
+ with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id)
c.pop(rid, None)
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@@ -156,9 +158,10 @@ class RoleRelationCache(object):
PREFIX_RESOURCES2 = "RoleRelationResources2::id::{0}::AppId::{1}"
@classmethod
- def get_parent_ids(cls, rid, app_id):
+ def get_parent_ids(cls, rid, app_id, force=False):
parent_ids = cache.get(cls.PREFIX_PARENT.format(rid, app_id))
- if not parent_ids:
+ if not parent_ids or force:
+ db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD
parent_ids = RoleRelationCRUD.get_parent_ids(rid, app_id)
cache.set(cls.PREFIX_PARENT.format(rid, app_id), parent_ids, timeout=0)
@@ -166,9 +169,10 @@ def get_parent_ids(cls, rid, app_id):
return parent_ids
@classmethod
- def get_child_ids(cls, rid, app_id):
+ def get_child_ids(cls, rid, app_id, force=False):
child_ids = cache.get(cls.PREFIX_CHILDREN.format(rid, app_id))
- if not child_ids:
+ if not child_ids or force:
+ db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD
child_ids = RoleRelationCRUD.get_child_ids(rid, app_id)
cache.set(cls.PREFIX_CHILDREN.format(rid, app_id), child_ids, timeout=0)
@@ -176,14 +180,16 @@ def get_child_ids(cls, rid, app_id):
return child_ids
@classmethod
- def get_resources(cls, rid, app_id):
+ def get_resources(cls, rid, app_id, force=False):
"""
:param rid:
:param app_id:
+ :param force:
:return: {id2perms: {resource_id: [perm,]}, group2perms: {group_id: [perm, ]}}
"""
resources = cache.get(cls.PREFIX_RESOURCES.format(rid, app_id))
- if not resources:
+ if not resources or force:
+ db.session.commit()
from api.lib.perm.acl.role import RoleCRUD
resources = RoleCRUD.get_resources(rid, app_id)
if resources['id2perms'] or resources['group2perms']:
@@ -192,9 +198,10 @@ def get_resources(cls, rid, app_id):
return resources or {}
@classmethod
- def get_resources2(cls, rid, app_id):
+ def get_resources2(cls, rid, app_id, force=False):
r_g = cache.get(cls.PREFIX_RESOURCES2.format(rid, app_id))
- if not r_g:
+ if not r_g or force:
+ db.session.commit()
res = cls.get_resources(rid, app_id)
id2perms = res['id2perms']
group2perms = res['group2perms']
@@ -221,24 +228,30 @@ def get_resources2(cls, rid, app_id):
return msgpack.loads(r_g, raw=False)
@classmethod
+ @flush_db
def rebuild(cls, rid, app_id):
- cls.clean(rid, app_id)
- db.session.remove()
-
- cls.get_parent_ids(rid, app_id)
- cls.get_child_ids(rid, app_id)
- resources = cls.get_resources(rid, app_id)
- if resources.get('id2perms') or resources.get('group2perms'):
- HasResourceRoleCache.add(rid, app_id)
+ if app_id is None:
+ app_ids = [None] + [i.id for i in App.get_by(to_dict=False)]
else:
- HasResourceRoleCache.remove(rid, app_id)
- cls.get_resources2(rid, app_id)
+ app_ids = [app_id]
+
+ for _app_id in app_ids:
+ cls.clean(rid, _app_id)
+
+ cls.get_parent_ids(rid, _app_id, force=True)
+ cls.get_child_ids(rid, _app_id, force=True)
+ resources = cls.get_resources(rid, _app_id, force=True)
+ if resources.get('id2perms') or resources.get('group2perms'):
+ HasResourceRoleCache.add(rid, _app_id)
+ else:
+ HasResourceRoleCache.remove(rid, _app_id)
+ cls.get_resources2(rid, _app_id, force=True)
@classmethod
+ @flush_db
def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
- db.session.remove()
- cls.get_resources2(rid, app_id)
+ cls.get_resources2(rid, app_id, force=True)
@classmethod
def clean(cls, rid, app_id):
diff --git a/acl-api/api/lib/perm/acl/permission.py b/acl-api/api/lib/perm/acl/permission.py
index f0259cc..ed8367a 100644
--- a/acl-api/api/lib/perm/acl/permission.py
+++ b/acl-api/api/lib/perm/acl/permission.py
@@ -4,7 +4,9 @@
from flask import abort
from api.extensions import db
-from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.audit import AuditOperateSource
+from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.cache import PermissionCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import UserCache
@@ -69,7 +71,7 @@ def get_all(resource_id=None, group_id=None, need_users=True):
@classmethod
def get_all2(cls, resource_name, resource_type_name, app_id):
- rt = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
+ rt = ResourceType.get_by(name=resource_type_name, app_id=app_id, first=True, to_dict=False)
rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name))
r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False)
@@ -77,7 +79,8 @@ def get_all2(cls, resource_name, resource_type_name, app_id):
return r and cls.get_all(r.id)
@staticmethod
- def grant(rid, perms, resource_id=None, group_id=None, rebuild=True, source=AuditOperateSource.acl):
+ def grant(rid, perms, resource_id=None, group_id=None, rebuild=True,
+ source=AuditOperateSource.acl, force_update=False):
app_id = None
rt_id = None
@@ -97,15 +100,30 @@ def grant(rid, perms, resource_id=None, group_id=None, rebuild=True, source=Audi
elif group_id is not None:
from api.models.acl import ResourceGroup
- group = ResourceGroup.get_by_id(group_id) or \
- abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
+ group = ResourceGroup.get_by_id(group_id) or abort(
+ 404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
app_id = group.app_id
rt_id = group.resource_type_id
if not perms:
perms = [i.get('name') for i in ResourceTypeCRUD.get_perms(group.resource_type_id)]
- _role_permissions = []
+ if force_update:
+ revoke_role_permissions = []
+ existed_perms = RolePermission.get_by(rid=rid,
+ app_id=app_id,
+ group_id=group_id,
+ resource_id=resource_id,
+ to_dict=False)
+ for role_perm in existed_perms:
+ perm = PermissionCache.get(role_perm.perm_id, rt_id)
+ if perm and perm.name not in perms:
+ role_perm.soft_delete()
+ revoke_role_permissions.append(role_perm)
+
+ AuditCRUD.add_permission_log(app_id, AuditOperateType.revoke, rid, rt_id,
+ revoke_role_permissions, source=source)
+ _role_permissions = []
for _perm in set(perms):
perm = PermissionCache.get(_perm, rt_id)
if not perm:
@@ -206,8 +224,8 @@ def revoke(rid, perms, resource_id=None, group_id=None, rebuild=True, source=Aud
if resource_id is not None:
from api.models.acl import Resource
- resource = Resource.get_by_id(resource_id) or \
- abort(404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
+ resource = Resource.get_by_id(resource_id) or abort(
+ 404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
app_id = resource.app_id
rt_id = resource.resource_type_id
if not perms:
@@ -216,8 +234,8 @@ def revoke(rid, perms, resource_id=None, group_id=None, rebuild=True, source=Aud
elif group_id is not None:
from api.models.acl import ResourceGroup
- group = ResourceGroup.get_by_id(group_id) or \
- abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
+ group = ResourceGroup.get_by_id(group_id) or abort(
+ 404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
app_id = group.app_id
rt_id = group.resource_type_id
@@ -272,12 +290,14 @@ def batch_revoke_by_resource_names(rid, perms, resource_type_id, resource_names,
perm2resource.setdefault(_perm, []).append(resource_id)
for _perm in perm2resource:
perm = PermissionCache.get(_perm, resource_type_id)
- existeds = RolePermission.get_by(rid=rid,
- app_id=app_id,
- perm_id=perm.id,
- __func_in___key_resource_id=perm2resource[_perm],
- to_dict=False)
- for existed in existeds:
+ if perm is None:
+ continue
+ exists = RolePermission.get_by(rid=rid,
+ app_id=app_id,
+ perm_id=perm.id,
+ __func_in___key_resource_id=perm2resource[_perm],
+ to_dict=False)
+ for existed in exists:
existed.deleted = True
existed.deleted_at = datetime.datetime.now()
db.session.add(existed)
diff --git a/acl-api/api/lib/perm/acl/resource.py b/acl-api/api/lib/perm/acl/resource.py
index 3673d20..739d104 100644
--- a/acl-api/api/lib/perm/acl/resource.py
+++ b/acl-api/api/lib/perm/acl/resource.py
@@ -2,10 +2,11 @@
from flask import abort
-from flask import current_app
from api.extensions import db
-from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.audit import AuditOperateType
+from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.cache import ResourceCache
from api.lib.perm.acl.cache import ResourceGroupCache
from api.lib.perm.acl.cache import UserCache
@@ -102,8 +103,8 @@ def update(cls, rt_id, **kwargs):
@classmethod
def delete(cls, rt_id):
- rt = ResourceType.get_by_id(rt_id) or \
- abort(404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
+ rt = ResourceType.get_by_id(rt_id) or abort(
+ 404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
Resource.get_by(resource_type_id=rt_id) and abort(400, ErrFormat.resource_type_cannot_delete)
@@ -125,11 +126,18 @@ def update_perms(cls, rt_id, perms, app_id):
existed_ids = [i.id for i in existed]
current_ids = []
+ rebuild_rids = set()
for i in existed:
if i.name not in perms:
- i.soft_delete()
+ i.soft_delete(commit=False)
+ for rp in RolePermission.get_by(perm_id=i.id, to_dict=False):
+ rp.soft_delete(commit=False)
+ rebuild_rids.add((rp.app_id, rp.rid))
else:
current_ids.append(i.id)
+ db.session.commit()
+ for _app_id, _rid in rebuild_rids:
+ role_rebuild.apply_async(args=(_rid, _app_id), queue=ACL_QUEUE)
for i in perms:
if i not in existed_names:
@@ -165,8 +173,8 @@ def get_items(rg_id):
@staticmethod
def add(name, type_id, app_id, uid=None):
- ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
- abort(400, ErrFormat.resource_group_exists.format(name))
+ ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
+ 400, ErrFormat.resource_group_exists.format(name))
rg = ResourceGroup.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)
AuditCRUD.add_resource_log(app_id, AuditOperateType.create,
@@ -175,8 +183,8 @@ def add(name, type_id, app_id, uid=None):
@staticmethod
def update(rg_id, items):
- rg = ResourceGroup.get_by_id(rg_id) or \
- abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
+ rg = ResourceGroup.get_by_id(rg_id) or abort(
+ 404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
existed = ResourceGroupItems.get_by(group_id=rg_id, to_dict=False)
existed_ids = [i.resource_id for i in existed]
@@ -196,8 +204,8 @@ def update(rg_id, items):
@staticmethod
def delete(rg_id):
- rg = ResourceGroup.get_by_id(rg_id) or \
- abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
+ rg = ResourceGroup.get_by_id(rg_id) or abort(
+ 404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
origin = rg.to_dict()
rg.soft_delete()
@@ -267,14 +275,13 @@ def search(cls, q, u, app_id, resource_type_id=None, page=1, page_size=None):
def add(cls, name, type_id, app_id, uid=None):
type_id = cls._parse_resource_type_id(type_id, app_id)
- Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
- abort(400, ErrFormat.resource_exists.format(name))
+ Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
+ 400, ErrFormat.resource_exists.format(name))
r = Resource.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)
from api.tasks.acl import apply_trigger
triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid)
- current_app.logger.info(triggers)
for trigger in triggers:
# auto trigger should be no uid
apply_trigger.apply_async(args=(trigger.id,),
@@ -308,9 +315,12 @@ def update(_id, name):
return resource
@staticmethod
- def delete(_id):
+ def delete(_id, rebuild=True, app_id=None):
resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
+ if app_id is not None and resource.app_id != app_id:
+ return abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
+
origin = resource.to_dict()
resource.soft_delete()
@@ -321,12 +331,15 @@ def delete(_id):
i.soft_delete()
rebuilds.append((i.rid, i.app_id))
- for rid, app_id in set(rebuilds):
- role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
+ if rebuild:
+ for rid, app_id in set(rebuilds):
+ role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete,
AuditScope.resource, resource.id, origin, {}, {})
+ return rebuilds
+
@classmethod
def delete_by_name(cls, name, type_id, app_id):
resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort(
diff --git a/acl-api/api/lib/perm/acl/resp_format.py b/acl-api/api/lib/perm/acl/resp_format.py
index 25f6bdc..8304f87 100644
--- a/acl-api/api/lib/perm/acl/resp_format.py
+++ b/acl-api/api/lib/perm/acl/resp_format.py
@@ -1,43 +1,50 @@
# -*- coding:utf-8 -*-
+from flask_babel import lazy_gettext as _l
+
from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat):
- auth_only_with_app_token_failed = "应用 Token验证失败"
- session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
-
- resource_type_not_found = "资源类型 {} 不存在!"
- resource_type_exists = "资源类型 {} 已经存在!"
- resource_type_cannot_delete = "因为该类型下有资源的存在, 不能删除!"
-
- user_not_found = "用户 {} 不存在!"
- user_exists = "用户 {} 已经存在!"
- role_not_found = "角色 {} 不存在!"
- role_exists = "角色 {} 已经存在!"
- global_role_not_found = "全局角色 {} 不存在!"
- global_role_exists = "全局角色 {} 已经存在!"
- user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
-
- resource_no_permission = "您没有资源: {} 的 {} 权限"
- admin_required = "需要管理员权限"
- role_required = "需要角色: {}"
-
- app_is_ready_existed = "应用 {} 已经存在"
- app_not_found = "应用 {} 不存在!"
- app_secret_invalid = "应用的Secret无效"
-
- resource_not_found = "资源 {} 不存在!"
- resource_exists = "资源 {} 已经存在!"
-
- resource_group_not_found = "资源组 {} 不存在!"
- resource_group_exists = "资源组 {} 已经存在!"
-
- inheritance_dead_loop = "继承检测到了死循环"
- role_relation_not_found = "角色关系 {} 不存在!"
-
- trigger_not_found = "触发器 {} 不存在!"
- trigger_exists = "触发器 {} 已经存在!"
- trigger_disabled = "触发器 {} 已经被禁用!"
-
- invalid_password = "密码不正确!"
+ login_succeed = _l("login successful") # 登录成功
+ ldap_connection_failed = _l("Failed to connect to LDAP service") # 连接LDAP服务失败
+ invalid_password = _l("Password verification failed") # 密码验证失败
+ auth_only_with_app_token_failed = _l("Application Token verification failed") # 应用 Token验证失败
+ # 您不是应用管理员 或者 session失效(尝试一下退出重新登录)
+ session_invalid = _l(
+ "You are not the application administrator or the session has expired (try logging out and logging in again)")
+
+ resource_type_not_found = _l("Resource type {} does not exist!") # 资源类型 {} 不存在!
+ resource_type_exists = _l("Resource type {} already exists!") # 资源类型 {} 已经存在!
+ # 因为该类型下有资源的存在, 不能删除!
+ resource_type_cannot_delete = _l("Because there are resources under this type, they cannot be deleted!")
+
+ user_not_found = _l("User {} does not exist!") # 用户 {} 不存在!
+ user_exists = _l("User {} already exists!") # 用户 {} 已经存在!
+ role_not_found = _l("Role {} does not exist!") # 角色 {} 不存在!
+ role_exists = _l("Role {} already exists!") # 角色 {} 已经存在!
+ global_role_not_found = _l("Global role {} does not exist!") # 全局角色 {} 不存在!
+ global_role_exists = _l("Global role {} already exists!") # 全局角色 {} 已经存在!
+
+ resource_no_permission = _l("You do not have {} permission on resource: {}") # 您没有资源: {} 的 {} 权限
+ admin_required = _l("Requires administrator permissions") # 需要管理员权限
+ role_required = _l("Requires role: {}") # 需要角色: {}
+ # 删除用户角色, 请在 用户管理 页面操作!
+ user_role_delete_invalid = _l("To delete a user role, please operate on the User Management page!")
+
+ app_is_ready_existed = _l("Application {} already exists") # 应用 {} 已经存在
+ app_not_found = _l("Application {} does not exist!") # 应用 {} 不存在!
+ app_secret_invalid = _l("The Secret is invalid") # 应用的Secret无效
+
+ resource_not_found = _l("Resource {} does not exist!") # 资源 {} 不存在!
+ resource_exists = _l("Resource {} already exists!") # 资源 {} 已经存在!
+
+ resource_group_not_found = _l("Resource group {} does not exist!") # 资源组 {} 不存在!
+ resource_group_exists = _l("Resource group {} already exists!") # 资源组 {} 已经存在!
+
+ inheritance_dead_loop = _l("Inheritance detected infinite loop") # 继承检测到了死循环
+ role_relation_not_found = _l("Role relationship {} does not exist!") # 角色关系 {} 不存在!
+
+ trigger_not_found = _l("Trigger {} does not exist!") # 触发器 {} 不存在!
+ trigger_exists = _l("Trigger {} already exists!") # 触发器 {} 已经存在!
+ trigger_disabled = _l("Trigger {} has been disabled!") # Trigger {} has been disabled!
diff --git a/acl-api/api/lib/perm/acl/role.py b/acl-api/api/lib/perm/acl/role.py
index 0c2a28c..b9fbe63 100644
--- a/acl-api/api/lib/perm/acl/role.py
+++ b/acl-api/api/lib/perm/acl/role.py
@@ -1,14 +1,14 @@
# -*- coding:utf-8 -*-
-import time
-
+import redis_lock
import six
from flask import abort
from flask import current_app
from sqlalchemy import or_
from api.extensions import db
+from api.extensions import rd
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import AppCache
@@ -62,7 +62,9 @@ def get_parents(rids=None, uids=None, app_id=None, all_app=False):
id2parents = {}
for i in res:
- id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(RoleCache.get(i.parent_id).to_dict())
+ parent = RoleCache.get(i.parent_id)
+ if parent:
+ id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(parent.to_dict())
return id2parents
@@ -70,7 +72,7 @@ def get_parents(rids=None, uids=None, app_id=None, all_app=False):
def get_parent_ids(rid, app_id):
if app_id is not None:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \
- [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
+ [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
else:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
@@ -78,7 +80,7 @@ def get_parent_ids(rid, app_id):
def get_child_ids(rid, app_id):
if app_id is not None:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \
- [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
+ [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
else:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]
@@ -141,24 +143,27 @@ def get_users_by_rid(cls, rid, app_id, rid2obj=None, uid2obj=None):
@classmethod
def add(cls, role, parent_id, child_ids, app_id):
- result = []
- for child_id in child_ids:
- existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
- if existed:
- continue
+ with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD"):
+ db.session.commit()
+
+ result = []
+ for child_id in child_ids:
+ existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
+ if existed:
+ continue
- RoleRelationCache.clean(parent_id, app_id)
- RoleRelationCache.clean(child_id, app_id)
+ if parent_id in cls.recursive_child_ids(child_id, app_id):
+ return abort(400, ErrFormat.inheritance_dead_loop)
- if parent_id in cls.recursive_child_ids(child_id, app_id):
- return abort(400, ErrFormat.inheritance_dead_loop)
+ result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
- if app_id is None:
- for app in AppCRUD.get_all():
- if app.name != "acl":
- RoleRelationCache.clean(child_id, app.id)
+ RoleRelationCache.clean(parent_id, app_id)
+ RoleRelationCache.clean(child_id, app_id)
- result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
+ if app_id is None:
+ for app in AppCRUD.get_all():
+ if app.name != "acl":
+ RoleRelationCache.clean(child_id, app.id)
AuditCRUD.add_role_log(app_id, AuditOperateType.role_relation_add,
AuditScope.role_relation, role.id, {}, {},
@@ -372,16 +377,16 @@ def _merge(a, b):
resource_type_id = resource_type and resource_type.id
result = dict(resources=dict(), groups=dict())
- s = time.time()
+ # s = time.time()
parent_ids = RoleRelationCRUD.recursive_parent_ids(rid, app_id)
- current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
+ # current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
for parent_id in parent_ids:
_resources, _groups = cls._extend_resources(parent_id, resource_type_id, app_id)
- current_app.logger.info('middle1: {0}'.format(time.time() - s))
+ # current_app.logger.info('middle1: {0}'.format(time.time() - s))
_merge(result['resources'], _resources)
- current_app.logger.info('middle2: {0}'.format(time.time() - s))
- current_app.logger.info(len(_groups))
+ # current_app.logger.info('middle2: {0}'.format(time.time() - s))
+ # current_app.logger.info(len(_groups))
if not group_flat:
_merge(result['groups'], _groups)
else:
@@ -392,7 +397,7 @@ def _merge(a, b):
item.setdefault('permissions', [])
item['permissions'] = list(set(item['permissions'] + _groups[rg_id]['permissions']))
result['resources'][item['id']] = item
- current_app.logger.info('End: {0}'.format(time.time() - s))
+ # current_app.logger.info('End: {0}'.format(time.time() - s))
result['resources'] = list(result['resources'].values())
result['groups'] = list(result['groups'].values())
diff --git a/acl-api/api/lib/perm/acl/trigger.py b/acl-api/api/lib/perm/acl/trigger.py
index f5a5d3f..8035e4f 100644
--- a/acl-api/api/lib/perm/acl/trigger.py
+++ b/acl-api/api/lib/perm/acl/trigger.py
@@ -6,9 +6,10 @@
import re
from fnmatch import fnmatch
-from flask import abort, current_app
+from flask import abort
-from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.resp_format import ErrFormat
diff --git a/acl-api/api/lib/perm/acl/user.py b/acl-api/api/lib/perm/acl/user.py
index bd6fb18..d17af58 100644
--- a/acl-api/api/lib/perm/acl/user.py
+++ b/acl-api/api/lib/perm/acl/user.py
@@ -9,7 +9,9 @@
from flask_login import current_user
from api.extensions import db
-from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.audit import AuditOperateType
+from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from api.lib.perm.acl.role import RoleCRUD
@@ -39,6 +41,7 @@ def gen_key_secret():
@classmethod
def add(cls, **kwargs):
+ add_from = kwargs.pop('add_from', None)
existed = User.get_by(username=kwargs['username'])
existed and abort(400, ErrFormat.user_exists.format(kwargs['username']))
@@ -49,20 +52,24 @@ def add(cls, **kwargs):
kwargs['block'] = 0
kwargs['key'], kwargs['secret'] = cls.gen_key_secret()
- user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(
- User.employee_id.desc()).first()
+ user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(User.employee_id.desc()).first()
- biggest_employee_id = int(float(user_employee.employee_id)) \
- if user_employee is not None else 0
+ biggest_employee_id = int(float(user_employee.employee_id)) if user_employee is not None else 0
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
user = User.create(**kwargs)
- RoleCRUD.add_role(user.username, uid=user.uid)
+ role = RoleCRUD.add_role(user.username, uid=user.uid)
AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
)
+ if add_from != 'common':
+ from api.lib.common_setting.employee import EmployeeCRUD
+ payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
+ payload['rid'] = role.id
+ EmployeeCRUD.add_employee_from_acl_created(**payload)
+
return user
@staticmethod
diff --git a/acl-api/api/lib/perm/auth.py b/acl-api/api/lib/perm/auth.py
index 76e2481..f2c66e7 100644
--- a/acl-api/api/lib/perm/auth.py
+++ b/acl-api/api/lib/perm/auth.py
@@ -51,12 +51,12 @@ def _auth_with_key():
user, authenticated = User.query.authenticate_with_key(key, secret, req_args, path)
if user and authenticated:
login_user(user)
- reset_session(user)
+ # reset_session(user)
return True
role, authenticated = Role.query.authenticate_with_key(key, secret, req_args, path)
if role and authenticated:
- reset_session(None, role=role.name)
+ # reset_session(None, role=role.name)
return True
return False
@@ -93,6 +93,9 @@ def _auth_with_token():
def _auth_with_ip_white_list():
+ if request.url.endswith("acl/users/info"):
+ return False
+
ip = request.headers.get('X-Real-IP') or request.remote_addr
key = request.values.get('_key')
secret = request.values.get('_secret')
diff --git a/acl-api/api/lib/perm/authentication/__init__.py b/acl-api/api/lib/perm/authentication/__init__.py
new file mode 100644
index 0000000..380474e
--- /dev/null
+++ b/acl-api/api/lib/perm/authentication/__init__.py
@@ -0,0 +1 @@
+# -*- coding:utf-8 -*-
diff --git a/acl-api/api/lib/perm/authentication/cas/__init__.py b/acl-api/api/lib/perm/authentication/cas/__init__.py
new file mode 100644
index 0000000..5f0fd52
--- /dev/null
+++ b/acl-api/api/lib/perm/authentication/cas/__init__.py
@@ -0,0 +1,78 @@
+# -*- coding:utf-8 -*-
+
+"""
+flask_cas.__init__
+"""
+
+import flask
+from flask import current_app
+
+# Find the stack on which we want to store the database connection.
+# Starting with Flask 0.9, the _app_ctx_stack is the correct one,
+# before that we need to use the _request_ctx_stack.
+try:
+ from flask import _app_ctx_stack as stack
+except ImportError:
+ from flask import _request_ctx_stack as stack
+
+from . import routing
+
+
+class CAS(object):
+ """
+ Required Configs:
+
+ |Key |
+ |----------------|
+ |CAS_SERVER |
+ |CAS_AFTER_LOGIN |
+
+ Optional Configs:
+
+ |Key | Default |
+ |-------------------------|----------------|
+ |CAS_TOKEN_SESSION_KEY | _CAS_TOKEN |
+ |CAS_USERNAME_SESSION_KEY | CAS_USERNAME |
+ |CAS_LOGIN_ROUTE | '/cas' |
+ |CAS_LOGOUT_ROUTE | '/cas/logout' |
+ |CAS_VALIDATE_ROUTE | '/cas/validate'|
+ """
+
+ def __init__(self, app=None, url_prefix=None):
+ self._app = app
+ if app is not None:
+ self.init_app(app, url_prefix)
+
+ def init_app(self, app, url_prefix=None):
+ # Configuration defaults
+ app.config.setdefault('CAS_TOKEN_SESSION_KEY', '_CAS_TOKEN')
+ app.config.setdefault('CAS_USERNAME_SESSION_KEY', 'CAS_USERNAME')
+ app.config.setdefault('CAS_LOGIN_ROUTE', '/login')
+ app.config.setdefault('CAS_LOGOUT_ROUTE', '/logout')
+ app.config.setdefault('CAS_VALIDATE_ROUTE', '/serviceValidate')
+ # Register Blueprint
+ app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
+
+ # Use the newstyle teardown_appcontext if it's available,
+ # otherwise fall back to the request context
+ if hasattr(app, 'teardown_appcontext'):
+ app.teardown_appcontext(self.teardown)
+ else:
+ app.teardown_request(self.teardown)
+
+ def teardown(self, exception):
+ ctx = stack.top
+
+ @property
+ def app(self):
+ return self._app or current_app
+
+ @property
+ def username(self):
+ return flask.session.get(
+ self.app.config['CAS_USERNAME_SESSION_KEY'], None)
+
+ @property
+ def token(self):
+ return flask.session.get(
+ self.app.config['CAS_TOKEN_SESSION_KEY'], None)
\ No newline at end of file
diff --git a/acl-api/api/lib/perm/authentication/cas/cas_urls.py b/acl-api/api/lib/perm/authentication/cas/cas_urls.py
new file mode 100644
index 0000000..4cbba47
--- /dev/null
+++ b/acl-api/api/lib/perm/authentication/cas/cas_urls.py
@@ -0,0 +1,122 @@
+# -*- coding:utf-8 -*-
+
+"""
+flask_cas.cas_urls
+
+Functions for creating urls to access CAS.
+"""
+from six.moves.urllib.parse import quote
+from six.moves.urllib.parse import urlencode
+from six.moves.urllib.parse import urljoin
+
+
+def create_url(base, path=None, *query):
+ """ Create a url.
+
+ Creates a url by combining base, path, and the query's list of
+ key/value pairs. Escaping is handled automatically. Any
+ key/value pair with a value that is None is ignored.
+
+ Keyword arguments:
+ base -- The left most part of the url (ex. http://localhost:5000).
+ path -- The path after the base (ex. /foo/bar).
+ query -- A list of key value pairs (ex. [('key', 'value')]).
+
+ Example usage:
+ >>> create_url(
+ ... 'http://localhost:5000',
+ ... 'foo/bar',
+ ... ('key1', 'value'),
+ ... ('key2', None), # Will not include None
+ ... ('url', 'http://example.com'),
+ ... )
+ 'http://localhost:5000/foo/bar?key1=value&url=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttp%2Fexample.com'
+ """
+ url = base
+ # Add the path to the url if it's not None.
+ if path is not None:
+ url = urljoin(url, quote(path))
+ # Remove key/value pairs with None values.
+ query = filter(lambda pair: pair[1] is not None, query)
+ # Add the query string to the url
+ url = urljoin(url, '?{0}'.format(urlencode(list(query))))
+ return url
+
+
+def create_cas_login_url(cas_url, cas_route, service,
+ renew=None, gateway=None):
+ """ Create a CAS login URL .
+
+ Keyword arguments:
+ cas_url -- The url to the CAS (ex. http://sso.pdx.edu)
+ cas_route -- The route where the CAS lives on server (ex. /cas)
+ service -- (ex. http://localhost:5000/login)
+ renew -- "true" or "false"
+ gateway -- "true" or "false"
+
+ Example usage:
+ >>> create_cas_login_url(
+ ... 'http://sso.pdx.edu',
+ ... '/cas',
+ ... 'http://localhost:5000',
+ ... )
+ 'http://sso.pdx.edu/cas?service=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttp%2Flocalhost%3A5000'
+ """
+ return create_url(
+ cas_url,
+ cas_route,
+ ('service', service),
+ ('renew', renew),
+ ('gateway', gateway),
+ )
+
+
+def create_cas_logout_url(cas_url, cas_route, url=None):
+ """ Create a CAS logout URL.
+
+ Keyword arguments:
+ cas_url -- The url to the CAS (ex. http://sso.pdx.edu)
+ cas_route -- The route where the CAS lives on server (ex. /cas/logout)
+ url -- (ex. http://localhost:5000/login)
+
+ Example usage:
+ >>> create_cas_logout_url(
+ ... 'http://sso.pdx.edu',
+ ... '/cas/logout',
+ ... 'http://localhost:5000',
+ ... )
+ 'http://sso.pdx.edu/cas/logout?url=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttp%2Flocalhost%3A5000'
+ """
+ return create_url(
+ cas_url,
+ cas_route,
+ ('service', url),
+ )
+
+
+def create_cas_validate_url(cas_url, cas_route, service, ticket,
+ renew=None):
+ """ Create a CAS validate URL.
+
+ Keyword arguments:
+ cas_url -- The url to the CAS (ex. http://sso.pdx.edu)
+ cas_route -- The route where the CAS lives on server (ex. /cas/validate)
+ service -- (ex. http://localhost:5000/login)
+ ticket -- (ex. 'ST-58274-x839euFek492ou832Eena7ee-cas')
+ renew -- "true" or "false"
+
+ Example usage:
+ >>> create_cas_validate_url(
+ ... 'http://sso.pdx.edu',
+ ... '/cas/validate',
+ ... 'http://localhost:5000/login',
+ ... 'ST-58274-x839euFek492ou832Eena7ee-cas'
+ ... )
+ """
+ return create_url(
+ cas_url,
+ cas_route,
+ ('service', service),
+ ('ticket', ticket),
+ ('renew', renew),
+ )
diff --git a/acl-api/api/lib/perm/authentication/cas/routing.py b/acl-api/api/lib/perm/authentication/cas/routing.py
new file mode 100644
index 0000000..612ad89
--- /dev/null
+++ b/acl-api/api/lib/perm/authentication/cas/routing.py
@@ -0,0 +1,206 @@
+# -*- coding:utf-8 -*-
+import datetime
+import uuid
+
+import bs4
+from flask import Blueprint
+from flask import current_app
+from flask import redirect
+from flask import request
+from flask import session
+from flask import url_for
+from flask_login import login_user
+from flask_login import logout_user
+from six.moves.urllib.parse import urlparse
+from six.moves.urllib_request import urlopen
+
+from api.lib.common_setting.common_data import AuthenticateDataCRUD
+from api.lib.common_setting.const import AuthenticateType
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.cache import UserCache
+from api.lib.perm.acl.resp_format import ErrFormat
+from .cas_urls import create_cas_login_url
+from .cas_urls import create_cas_logout_url
+from .cas_urls import create_cas_validate_url
+
+blueprint = Blueprint('cas', __name__)
+
+
+@blueprint.route('/api/cas/login')
+@blueprint.route('/api/sso/login')
+def login():
+ """
+ This route has two purposes. First, it is used by the user
+ to login. Second, it is used by the CAS to respond with the
+ `ticket` after the user logs in successfully.
+
+ When the user accesses this url, they are redirected to the CAS
+ to login. If the login was successful, the CAS will respond to this
+ route with the ticket in the url. The ticket is then validated.
+ If validation was successful the logged in username is saved in
+ the user's session under the key `CAS_USERNAME_SESSION_KEY`.
+ """
+ config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
+
+ cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
+ if request.values.get("next"):
+ session["next"] = request.values.get("next")
+
+ # _service = url_for('cas.login', _external=True)
+ _service = "{}://{}{}".format(urlparse(request.referrer).scheme,
+ urlparse(request.referrer).netloc,
+ url_for('cas.login'))
+
+ redirect_url = create_cas_login_url(
+ config['cas_server'],
+ config['cas_login_route'],
+ _service)
+
+ if 'ticket' in request.args:
+ session[cas_token_session_key] = request.args.get('ticket')
+
+ if request.args.get('ticket'):
+
+ if validate(request.args['ticket']):
+ redirect_url = session.get("next") or config.get("cas_after_login") or "/"
+ username = session.get("CAS_USERNAME")
+ user = UserCache.get(username)
+ login_user(user)
+
+ session.permanent = True
+
+ _id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
+ session['LOGIN_ID'] = _id
+
+ else:
+ del session[cas_token_session_key]
+ redirect_url = create_cas_login_url(
+ config['cas_server'],
+ config['cas_login_route'],
+ url_for('cas.login', _external=True),
+ renew=True)
+
+ AuditCRUD.add_login_log(session.get("CAS_USERNAME"), False, ErrFormat.invalid_password)
+
+ current_app.logger.info("redirect to: {0}".format(redirect_url))
+ return redirect(redirect_url)
+
+
+@blueprint.route('/api/cas/logout')
+@blueprint.route('/api/sso/logout')
+def logout():
+ """
+ When the user accesses this route they are logged out.
+ """
+ config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
+ current_app.logger.info(config)
+
+ cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
+ cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
+
+ cas_username_session_key in session and session.pop(cas_username_session_key)
+ "acl" in session and session.pop("acl")
+ "uid" in session and session.pop("uid")
+ cas_token_session_key in session and session.pop(cas_token_session_key)
+ "next" in session and session.pop("next")
+
+ redirect_url = create_cas_logout_url(
+ config['cas_server'],
+ config['cas_logout_route'],
+ url_for('cas.login', _external=True, next=request.referrer))
+
+ logout_user()
+
+ AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
+
+ current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
+
+ return redirect(redirect_url)
+
+
+def validate(ticket):
+ """
+ Will attempt to validate the ticket. If validation fails, then False
+ is returned. If validation is successful, then True is returned
+ and the validated username is saved in the session under the
+ key `CAS_USERNAME_SESSION_KEY`.
+ """
+ config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
+
+ cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
+
+ current_app.logger.debug("validating token {0}".format(ticket))
+
+ cas_validate_url = create_cas_validate_url(
+ config['cas_validate_server'],
+ config['cas_validate_route'],
+ url_for('cas.login', _external=True),
+ ticket)
+
+ current_app.logger.debug("Making GET request to {0}".format(cas_validate_url))
+
+ try:
+ response = urlopen(cas_validate_url).read()
+ ticket_id = _parse_tag(response, "cas:user")
+ strs = [s.strip() for s in ticket_id.split('|') if s.strip()]
+ username, is_valid = None, False
+ if len(strs) == 1:
+ username = strs[0]
+ is_valid = True
+ except ValueError:
+ current_app.logger.error("CAS returned unexpected result")
+ is_valid = False
+ return is_valid
+
+ if is_valid:
+ current_app.logger.debug("{}: {}".format(cas_username_session_key, username))
+ session[cas_username_session_key] = username
+ user = UserCache.get(username)
+ if user is None:
+ current_app.logger.info("create user: {}".format(username))
+ from api.lib.perm.acl.user import UserCRUD
+ soup = bs4.BeautifulSoup(response)
+ cas_user_map = config.get('cas_user_map')
+ user_dict = dict()
+ for k in cas_user_map:
+ v = soup.find(cas_user_map[k]['tag'], cas_user_map[k].get('attrs', {}))
+ user_dict[k] = v and v.text or None
+ user_dict['password'] = uuid.uuid4().hex
+ if "email" not in user_dict:
+ user_dict['email'] = username
+
+ UserCRUD.add(**user_dict)
+
+ from api.lib.perm.acl.acl import ACLManager
+ user_info = ACLManager.get_user_info(username)
+
+ session["acl"] = dict(uid=user_info.get("uid"),
+ avatar=user.avatar if user else user_info.get("avatar"),
+ userId=user_info.get("uid"),
+ rid=user_info.get("rid"),
+ userName=user_info.get("username"),
+ nickName=user_info.get("nickname"),
+ parentRoles=user_info.get("parents"),
+ childRoles=user_info.get("children"),
+ roleName=user_info.get("role"))
+ session["uid"] = user_info.get("uid")
+ current_app.logger.debug(session)
+ current_app.logger.debug(request.url)
+ else:
+ current_app.logger.debug("invalid")
+
+ return is_valid
+
+
+def _parse_tag(string, tag):
+ """
+ Used for parsing xml. Search string for the first occurrence of
+ ..... and return text (stripped of leading and tailing
+ whitespace) between tags. Return "" if tag not found.
+ """
+ soup = bs4.BeautifulSoup(string)
+
+ if soup.find(tag) is None:
+ return ''
+
+ return soup.find(tag).string.strip()
diff --git a/acl-api/api/lib/perm/authentication/ldap.py b/acl-api/api/lib/perm/authentication/ldap.py
new file mode 100644
index 0000000..64e3239
--- /dev/null
+++ b/acl-api/api/lib/perm/authentication/ldap.py
@@ -0,0 +1,67 @@
+# -*- coding:utf-8 -*-
+
+import uuid
+
+from flask import abort
+from flask import current_app
+from flask import session
+from ldap3 import ALL
+from ldap3 import AUTO_BIND_NO_TLS
+from ldap3 import Connection
+from ldap3 import Server
+from ldap3.core.exceptions import LDAPBindError
+from ldap3.core.exceptions import LDAPCertificateError
+from ldap3.core.exceptions import LDAPSocketOpenError
+
+from api.lib.common_setting.common_data import AuthenticateDataCRUD
+from api.lib.common_setting.const import AuthenticateType
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.resp_format import ErrFormat
+from api.models.acl import User
+
+
+def authenticate_with_ldap(username, password):
+ config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
+
+ server = Server(config.get('ldap_server'), get_info=ALL, connect_timeout=3)
+ if '@' in username:
+ email = username
+ who = config.get('ldap_user_dn').format(username.split('@')[0])
+ else:
+ who = config.get('ldap_user_dn').format(username)
+ email = "{}@{}".format(who, config.get('ldap_domain'))
+
+ username = username.split('@')[0]
+ user = User.query.get_by_username(username)
+ try:
+ if not password:
+ raise LDAPCertificateError
+
+ try:
+ conn = Connection(server, user=who, password=password, auto_bind=AUTO_BIND_NO_TLS)
+ except LDAPBindError:
+ conn = Connection(server,
+ user=f"{username}@{config.get('ldap_domain')}",
+ password=password,
+ auto_bind=AUTO_BIND_NO_TLS)
+
+ if conn.result['result'] != 0:
+ AuditCRUD.add_login_log(username, False, ErrFormat.invalid_password)
+ raise LDAPBindError
+ else:
+ _id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
+ session['LOGIN_ID'] = _id
+
+ if not user:
+ from api.lib.perm.acl.user import UserCRUD
+ user = UserCRUD.add(username=username, email=email, password=uuid.uuid4().hex)
+
+ return user, True
+
+ except LDAPBindError as e:
+ current_app.logger.info(e)
+ return user, False
+
+ except LDAPSocketOpenError as e:
+ current_app.logger.info(e)
+ return abort(403, ErrFormat.ldap_connection_failed)
diff --git a/acl-api/api/lib/perm/authentication/oauth2/__init__.py b/acl-api/api/lib/perm/authentication/oauth2/__init__.py
new file mode 100644
index 0000000..1b7d02e
--- /dev/null
+++ b/acl-api/api/lib/perm/authentication/oauth2/__init__.py
@@ -0,0 +1,30 @@
+# -*- coding:utf-8 -*-
+
+from flask import current_app
+
+from . import routing
+
+
+class OAuth2(object):
+ def __init__(self, app=None, url_prefix=None):
+ self._app = app
+ if app is not None:
+ self.init_app(app, url_prefix)
+
+ @staticmethod
+ def init_app(app, url_prefix=None):
+ # Configuration defaults
+ app.config.setdefault('OAUTH2_GRANT_TYPE', 'authorization_code')
+ app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
+ app.config.setdefault('OAUTH2_AFTER_LOGIN', '/')
+
+ app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code')
+ app.config.setdefault('OIDC_RESPONSE_TYPE', 'code')
+ app.config.setdefault('OIDC_AFTER_LOGIN', '/')
+
+ # Register Blueprint
+ app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
+
+ @property
+ def app(self):
+ return self._app or current_app
diff --git a/acl-api/api/lib/perm/authentication/oauth2/routing.py b/acl-api/api/lib/perm/authentication/oauth2/routing.py
new file mode 100644
index 0000000..dfc42d8
--- /dev/null
+++ b/acl-api/api/lib/perm/authentication/oauth2/routing.py
@@ -0,0 +1,139 @@
+# -*- coding:utf-8 -*-
+
+import datetime
+import secrets
+import uuid
+
+import requests
+from flask import Blueprint
+from flask import abort
+from flask import current_app
+from flask import redirect
+from flask import request
+from flask import session
+from flask import url_for
+from flask_login import login_user
+from flask_login import logout_user
+from six.moves.urllib.parse import urlencode
+from six.moves.urllib.parse import urlparse
+
+from api.lib.common_setting.common_data import AuthenticateDataCRUD
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.cache import UserCache
+from api.lib.perm.acl.resp_format import ErrFormat
+
+blueprint = Blueprint('oauth2', __name__)
+
+
+@blueprint.route('/api//login')
+def login(auth_type):
+ config = AuthenticateDataCRUD(auth_type.upper()).get()
+
+ if request.values.get("next"):
+ session["next"] = request.values.get("next")
+
+ session[f'{auth_type}_state'] = secrets.token_urlsafe(16)
+
+ auth_type = auth_type.upper()
+
+ redirect_uri = "{}://{}{}".format(urlparse(request.referrer).scheme,
+ urlparse(request.referrer).netloc,
+ url_for('oauth2.callback', auth_type=auth_type.lower()))
+ qs = urlencode({
+ 'client_id': config['client_id'],
+ 'redirect_uri': redirect_uri,
+ 'response_type': current_app.config[f'{auth_type}_RESPONSE_TYPE'],
+ 'scope': ' '.join(config['scopes'] or []),
+ 'state': session[f'{auth_type.lower()}_state'],
+ })
+
+ return redirect("{}?{}".format(config['authorize_url'].split('?')[0], qs))
+
+
+@blueprint.route('/api//callback')
+def callback(auth_type):
+ auth_type = auth_type.upper()
+ config = AuthenticateDataCRUD(auth_type).get()
+
+ redirect_url = session.get("next") or config.get('after_login') or '/'
+
+ if request.values['state'] != session.get(f'{auth_type.lower()}_state'):
+ return abort(401, "state is invalid")
+
+ if 'code' not in request.values:
+ return abort(401, 'code is invalid')
+
+ response = requests.post(config['token_url'], data={
+ 'client_id': config['client_id'],
+ 'client_secret': config['client_secret'],
+ 'code': request.values['code'],
+ 'grant_type': current_app.config[f'{auth_type}_GRANT_TYPE'],
+ 'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True),
+ }, headers={'Accept': 'application/json'})
+ if response.status_code != 200:
+ current_app.logger.error(response.text)
+ return abort(401)
+ access_token = response.json().get('access_token')
+ if not access_token:
+ return abort(401)
+
+ response = requests.get(config['user_info']['url'], headers={
+ 'Authorization': 'Bearer {}'.format(access_token),
+ 'Accept': 'application/json',
+ })
+ if response.status_code != 200:
+ return abort(401)
+
+ res = response.json()
+ email = res.get(config['user_info']['email'])
+ username = res.get(config['user_info']['username'])
+ avatar = res.get(config['user_info'].get('avatar'))
+ user = UserCache.get(username)
+ if user is None:
+ current_app.logger.info("create user: {}".format(username))
+ from api.lib.perm.acl.user import UserCRUD
+
+ user_dict = dict(username=username, email=email, avatar=avatar)
+ user_dict['password'] = uuid.uuid4().hex
+
+ user = UserCRUD.add(**user_dict)
+
+ # log the user in
+ login_user(user)
+
+ from api.lib.perm.acl.acl import ACLManager
+ user_info = ACLManager.get_user_info(username)
+
+ session["acl"] = dict(uid=user_info.get("uid"),
+ avatar=user.avatar if user else user_info.get("avatar"),
+ userId=user_info.get("uid"),
+ rid=user_info.get("rid"),
+ userName=user_info.get("username"),
+ nickName=user_info.get("nickname") or user_info.get("username"),
+ parentRoles=user_info.get("parents"),
+ childRoles=user_info.get("children"),
+ roleName=user_info.get("role"))
+ session["uid"] = user_info.get("uid")
+
+ _id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
+ session['LOGIN_ID'] = _id
+
+ return redirect(redirect_url)
+
+
+@blueprint.route('/api//logout')
+def logout(auth_type):
+ "acl" in session and session.pop("acl")
+ "uid" in session and session.pop("uid")
+ f'{auth_type}_state' in session and session.pop(f'{auth_type}_state')
+ "next" in session and session.pop("next")
+
+ redirect_url = url_for('oauth2.login', auth_type=auth_type, _external=True, next=request.referrer)
+
+ logout_user()
+
+ current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
+
+ AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
+
+ return redirect(redirect_url)
diff --git a/acl-api/api/lib/resp_format.py b/acl-api/api/lib/resp_format.py
index e3cf4da..18ff0c6 100644
--- a/acl-api/api/lib/resp_format.py
+++ b/acl-api/api/lib/resp_format.py
@@ -1,27 +1,34 @@
# -*- coding:utf-8 -*-
+from flask_babel import lazy_gettext as _l
+
+
class CommonErrFormat(object):
- unauthorized = "未认证"
- unknown_error = "未知错误"
+ unauthorized = _l("unauthorized") # 未认证
+ unknown_error = _l("unknown error") # 未知错误
+
+ invalid_request = _l("Illegal request") # 不合法的请求
+ invalid_operation = _l("Invalid operation") # 无效的操作
- invalid_request = "不合法的请求"
- invalid_operation = "无效的操作"
+ not_found = _l("does not exist") # 不存在
- not_found = "不存在"
+ circular_dependency_error = _l("There is a circular dependency!") # 存在循环依赖!
- unknown_search_error = "未知搜索错误"
+ unknown_search_error = _l("Unknown search error") # 未知搜索错误
- invalid_json = "json格式似乎不正确了, 请仔细确认一下!"
+ # json格式似乎不正确了, 请仔细确认一下!
+ invalid_json = _l("The json format seems to be incorrect, please confirm carefully!")
- datetime_argument_invalid = "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS"
+ # 参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS
+ datetime_argument_invalid = _l("The format of parameter {} is incorrect, the format must be: yyyy-mm-dd HH:MM:SS")
- argument_value_required = "参数 {} 的值不能为空!"
- argument_required = "请求缺少参数 {}"
- argument_invalid = "参数 {} 的值无效"
- argument_str_length_limit = "参数 {} 的长度必须 <= {}"
+ argument_value_required = _l("The value of parameter {} cannot be empty!") # 参数 {} 的值不能为空!
+ argument_required = _l("The request is missing parameters {}") # 请求缺少参数 {}
+ argument_invalid = _l("Invalid value for parameter {}") # 参数 {} 的值无效
+ argument_str_length_limit = _l("The length of parameter {} must be <= {}") # 参数 {} 的长度必须 <= {}
- role_required = "角色 {} 才能操作!"
- user_not_found = "用户 {} 不存在"
- no_permission = "您没有资源: {} 的{}权限!"
- no_permission2 = "您没有操作权限!"
- no_permission_only_owner = "只有创建人或者管理员才有权限!"
+ role_required = _l("Role {} can only operate!") # 角色 {} 才能操作!
+ user_not_found = _l("User {} does not exist") # 用户 {} 不存在
+ no_permission = _l("For resource: {}, you do not have {} permission!") # 您没有资源: {} 的{}权限!
+ no_permission2 = _l("You do not have permission to operate!") # 您没有操作权限!
+ no_permission_only_owner = _l("Only the creator or administrator has permission!") # 只有创建人或者管理员才有权限!
diff --git a/acl-api/api/lib/utils.py b/acl-api/api/lib/utils.py
index 85d541a..a8482a8 100644
--- a/acl-api/api/lib/utils.py
+++ b/acl-api/api/lib/utils.py
@@ -1,11 +1,13 @@
# -*- coding:utf-8 -*-
-import sys
-import time
+import base64
from typing import Set
import redis
import six
+import sys
+import time
+from Crypto.Cipher import AES
from flask import current_app
@@ -108,7 +110,7 @@ def delete(self, key_id, prefix):
try:
ret = self.r.hdel(prefix, key_id)
if not ret:
- current_app.logger.warn("[{0}] is not in redis".format(key_id))
+ current_app.logger.warning("[{0}] is not in redis".format(key_id))
except Exception as e:
current_app.logger.error("delete redis key error, {0}".format(str(e)))
@@ -157,3 +159,35 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
if self.need_lock:
self.release()
+
+
+class AESCrypto(object):
+ BLOCK_SIZE = 16 # Bytes
+ pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *
+ chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE))
+ unpad = lambda s: s[:-ord(s[len(s) - 1:])]
+
+ iv = '0102030405060708'
+
+ @staticmethod
+ def key():
+ key = current_app.config.get("SECRET_KEY")[:16]
+ if len(key) < 16:
+ key = "{}{}".format(key, (16 - len(key)) * "x")
+
+ return key.encode('utf8')
+
+ @classmethod
+ def encrypt(cls, data):
+ data = cls.pad(data)
+ cipher = AES.new(cls.key(), AES.MODE_CBC, cls.iv.encode('utf8'))
+
+ return base64.b64encode(cipher.encrypt(data.encode('utf8'))).decode('utf8')
+
+ @classmethod
+ def decrypt(cls, data):
+ encode_bytes = base64.decodebytes(data.encode('utf8'))
+ cipher = AES.new(cls.key(), AES.MODE_CBC, cls.iv.encode('utf8'))
+ text_decrypted = cipher.decrypt(encode_bytes)
+
+ return cls.unpad(text_decrypted).decode('utf8')
diff --git a/acl-api/api/models/acl.py b/acl-api/api/models/acl.py
index ebf02ff..730a012 100644
--- a/acl-api/api/models/acl.py
+++ b/acl-api/api/models/acl.py
@@ -5,16 +5,18 @@
import hashlib
from datetime import datetime
-import ldap
from flask import current_app
+from flask import session
from flask_sqlalchemy import BaseQuery
from api.extensions import db
from api.lib.database import CRUDModel
from api.lib.database import Model
+from api.lib.database import Model2
from api.lib.database import SoftDeleteMixin
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.const import OperateType
+from api.lib.perm.acl.resp_format import ErrFormat
class App(Model):
@@ -27,21 +29,26 @@ class App(Model):
class UserQuery(BaseQuery):
- def _join(self, *args, **kwargs):
- super(UserQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password):
+ from api.lib.perm.acl.audit import AuditCRUD
+
user = self.filter(db.or_(User.username == login,
User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first()
if user:
- current_app.logger.info(user)
authenticated = user.check_password(password)
if authenticated:
- from api.tasks.acl import op_record
- op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
+ _id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed)
+ session['LOGIN_ID'] = _id
+ else:
+ AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password)
else:
authenticated = False
+ AuditCRUD.add_login_log(login, False, ErrFormat.user_not_found.format(login))
+
+ current_app.logger.info(("login", login, user, authenticated))
+
return user, authenticated
def authenticate_with_key(self, key, secret, args, path):
@@ -56,37 +63,6 @@ def authenticate_with_key(self, key, secret, args, path):
return user, authenticated
- def authenticate_with_ldap(self, username, password):
- ldap_conn = ldap.initialize(current_app.config.get('LDAP_SERVER'))
- ldap_conn.protocol_version = 3
- ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
- if '@' in username:
- email = username
- who = '{0}@{1}'.format(username.split('@')[0], current_app.config.get('LDAP_DOMAIN'))
- else:
- who = '{0}@{1}'.format(username, current_app.config.get('LDAP_DOMAIN'))
- email = who
-
- username = username.split('@')[0]
- user = self.get_by_username(username)
- try:
-
- if not password:
- raise ldap.INVALID_CREDENTIALS
-
- ldap_conn.simple_bind_s(who, password)
-
- if not user:
- from api.lib.perm.acl.user import UserCRUD
- user = UserCRUD.add(username=username, email=email)
-
- from api.tasks.acl import op_record
- op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
-
- return user, True
- except ldap.INVALID_CREDENTIALS:
- return user, False
-
def search(self, key):
query = self.filter(db.or_(User.email == key,
User.nickname.ilike('%' + key + '%'),
@@ -136,6 +112,7 @@ class User(CRUDModel, SoftDeleteMixin):
wx_id = db.Column(db.String(32))
employee_id = db.Column(db.String(16), index=True)
avatar = db.Column(db.String(128))
+
# apps = db.Column(db.JSON)
def __str__(self):
@@ -166,11 +143,9 @@ def check_password(self, password):
class RoleQuery(BaseQuery):
- def _join(self, *args, **kwargs):
- super(RoleQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password):
- role = self.filter(Role.name == login).first()
+ role = self.filter(Role.name == login).filter(Role.deleted.is_(False)).first()
if role:
authenticated = role.check_password(password)
@@ -375,3 +350,16 @@ class AuditTriggerLog(Model):
current = db.Column(db.JSON, default=dict(), comment='当前数据')
extra = db.Column(db.JSON, default=dict(), comment='权限名')
source = db.Column(db.String(16), default='', comment='来源')
+
+
+class AuditLoginLog(Model2):
+ __tablename__ = "acl_audit_login_logs"
+
+ username = db.Column(db.String(64), index=True)
+ channel = db.Column(db.Enum('web', 'api', 'ssh'), default="web")
+ ip = db.Column(db.String(15))
+ browser = db.Column(db.String(256))
+ description = db.Column(db.String(128))
+ is_ok = db.Column(db.Boolean)
+ login_at = db.Column(db.DateTime)
+ logout_at = db.Column(db.DateTime)
diff --git a/acl-api/api/models/common_setting.py b/acl-api/api/models/common_setting.py
index 741f664..f1f5404 100644
--- a/acl-api/api/models/common_setting.py
+++ b/acl-api/api/models/common_setting.py
@@ -13,40 +13,41 @@ class Department(ModelWithoutPK):
__tablename__ = 'common_department'
department_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
- department_name = db.Column(db.VARCHAR(255), default='', comment='部门名称')
+ department_name = db.Column(db.VARCHAR(255), default='')
department_director_id = db.Column(
- db.Integer, default=0, comment='部门负责人ID')
- department_parent_id = db.Column(db.Integer, default=1, comment='上级部门ID')
+ db.Integer, default=0)
+ department_parent_id = db.Column(db.Integer, default=1)
- sort_value = db.Column(db.Integer, default=0, comment='排序值')
+ sort_value = db.Column(db.Integer, default=0)
- acl_rid = db.Column(db.Integer, comment='ACL中rid', default=0)
+ acl_rid = db.Column(db.Integer, default=0)
class Employee(ModelWithoutPK):
__tablename__ = 'common_employee'
employee_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
- email = db.Column(db.VARCHAR(255), default='', comment='邮箱')
- username = db.Column(db.VARCHAR(255), default='', comment='用户名')
- nickname = db.Column(db.VARCHAR(255), default='', comment='姓名')
- sex = db.Column(db.VARCHAR(64), default='', comment='性别')
- position_name = db.Column(db.VARCHAR(255), default='', comment='职位名称')
- mobile = db.Column(db.VARCHAR(255), default='', comment='电话号码')
- avatar = db.Column(db.VARCHAR(255), default='', comment='头像')
+ email = db.Column(db.VARCHAR(255), default='')
+ username = db.Column(db.VARCHAR(255), default='')
+ nickname = db.Column(db.VARCHAR(255), default='')
+ sex = db.Column(db.VARCHAR(64), default='')
+ position_name = db.Column(db.VARCHAR(255), default='')
+ mobile = db.Column(db.VARCHAR(255), default='')
+ avatar = db.Column(db.VARCHAR(255), default='')
- direct_supervisor_id = db.Column(db.Integer, default=0, comment='直接上级ID')
+ direct_supervisor_id = db.Column(db.Integer, default=0)
department_id = db.Column(db.Integer,
- db.ForeignKey('common_department.department_id'),
- comment='部门ID',
+ db.ForeignKey('common_department.department_id')
)
- acl_uid = db.Column(db.Integer, comment='ACL中uid', default=0)
- acl_rid = db.Column(db.Integer, comment='ACL中rid', default=0)
- acl_virtual_rid = db.Column(db.Integer, comment='ACL中虚拟角色rid', default=0)
- last_login = db.Column(db.TIMESTAMP, nullable=True, comment='上次登录时间')
- block = db.Column(db.Integer, comment='锁定状态', default=0)
+ acl_uid = db.Column(db.Integer, default=0)
+ acl_rid = db.Column(db.Integer, default=0)
+ acl_virtual_rid = db.Column(db.Integer, default=0)
+ last_login = db.Column(db.TIMESTAMP, nullable=True)
+ block = db.Column(db.Integer, default=0)
+
+ notice_info = db.Column(db.JSON, default={})
_department = db.relationship(
'Department', backref='common_employee.department_id',
@@ -55,14 +56,11 @@ class Employee(ModelWithoutPK):
class EmployeeInfo(Model):
- """
- 员工信息
- """
__tablename__ = 'common_employee_info'
- info = db.Column(db.JSON, default={}, comment='员工信息')
+ info = db.Column(db.JSON, default={})
employee_id = db.Column(db.Integer, db.ForeignKey(
- 'common_employee.employee_id'), comment='员工ID')
+ 'common_employee.employee_id'))
employee = db.relationship(
'Employee', backref='common_employee.employee_id', lazy='joined')
@@ -74,16 +72,35 @@ class CompanyInfo(Model):
class InternalMessage(Model):
- """
- 内部消息
- """
__tablename__ = "common_internal_message"
- title = db.Column(db.VARCHAR(255), nullable=True, comment='标题')
- content = db.Column(db.TEXT, nullable=True, comment='内容')
- path = db.Column(db.VARCHAR(255), nullable=True, comment='跳转路径')
- is_read = db.Column(db.Boolean, default=False, comment='是否已读')
- app_name = db.Column(db.VARCHAR(128), nullable=False, comment='应用名称')
- category = db.Column(db.VARCHAR(128), nullable=False, comment='分类')
- message_data = db.Column(db.JSON, nullable=True, comment='数据')
+ title = db.Column(db.VARCHAR(255), nullable=True)
+ content = db.Column(db.TEXT, nullable=True)
+ path = db.Column(db.VARCHAR(255), nullable=True)
+ is_read = db.Column(db.Boolean, default=False)
+ app_name = db.Column(db.VARCHAR(128), nullable=False)
+ category = db.Column(db.VARCHAR(128), nullable=False)
+ message_data = db.Column(db.JSON, nullable=True)
employee_id = db.Column(db.Integer, db.ForeignKey('common_employee.employee_id'), comment='ID')
+
+
+class CommonData(Model):
+ __table_name__ = 'common_data'
+
+ data_type = db.Column(db.VARCHAR(255), default='')
+ data = db.Column(db.JSON)
+
+
+class NoticeConfig(Model):
+ __tablename__ = "common_notice_config"
+
+ platform = db.Column(db.VARCHAR(255), nullable=False)
+ info = db.Column(db.JSON)
+
+
+class CommonFile(Model):
+ __tablename__ = 'common_file'
+
+ file_name = db.Column(db.VARCHAR(512), nullable=False, index=True)
+ origin_name = db.Column(db.VARCHAR(512), nullable=False)
+ binary = db.Column(db.LargeBinary(16777216), nullable=False)
diff --git a/acl-api/api/tasks/acl.py b/acl-api/api/tasks/acl.py
index e083ffe..94f37b6 100644
--- a/acl-api/api/tasks/acl.py
+++ b/acl-api/api/tasks/acl.py
@@ -3,37 +3,43 @@
import json
import re
-from celery_once import QueueOnce
+import redis_lock
from flask import current_app
-from werkzeug.exceptions import BadRequest, NotFound
+from werkzeug.exceptions import BadRequest
+from werkzeug.exceptions import NotFound
from api.extensions import celery
-from api.extensions import db
+from api.extensions import rd
+from api.lib.decorator import flush_db
+from api.lib.decorator import reconnect_db
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.audit import AuditOperateSource
+from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import RoleRelationCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.record import OperateRecordCRUD
-from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
from api.models.acl import Resource
from api.models.acl import Role
from api.models.acl import Trigger
-@celery.task(base=QueueOnce,
- name="acl.role_rebuild",
- queue=ACL_QUEUE,
- once={"graceful": True, "unlock_before_run": True})
+@celery.task(name="acl.role_rebuild", queue=ACL_QUEUE, )
+@flush_db
+@reconnect_db
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
- RoleRelationCache.rebuild(rid, app_id)
+ with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id)):
+ RoleRelationCache.rebuild(rid, app_id)
current_app.logger.info("Role {0} App {1} rebuild..........".format(rids, app_id))
@celery.task(name="acl.update_resource_to_build_role", queue=ACL_QUEUE)
+@reconnect_db
def update_resource_to_build_role(resource_id, app_id, group_id=None):
rids = [i.id for i in Role.get_by(__func_isnot__key_uid=None, fl='id', to_dict=False)]
rids += [i.id for i in Role.get_by(app_id=app_id, fl='id', to_dict=False)]
@@ -49,9 +55,9 @@ def update_resource_to_build_role(resource_id, app_id, group_id=None):
@celery.task(name="acl.apply_trigger", queue=ACL_QUEUE)
+@flush_db
+@reconnect_db
def apply_trigger(_id, resource_id=None, operator_uid=None):
- db.session.remove()
-
from api.lib.perm.acl.permission import PermissionCRUD
trigger = Trigger.get_by_id(_id)
@@ -115,9 +121,9 @@ def apply_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE)
+@flush_db
+@reconnect_db
def cancel_trigger(_id, resource_id=None, operator_uid=None):
- db.session.remove()
-
from api.lib.perm.acl.permission import PermissionCRUD
trigger = Trigger.get_by_id(_id)
@@ -183,18 +189,19 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
-def op_record(app, rolename, operate_type, obj):
+@reconnect_db
+def op_record(app, role_name, operate_type, obj):
if isinstance(app, int):
app = AppCache.get(app)
app = app and app.name
- if isinstance(rolename, int):
- u = UserCache.get(rolename)
+ if isinstance(role_name, int):
+ u = UserCache.get(role_name)
if u:
- rolename = u.username
+ role_name = u.username
if not u:
- r = RoleCache.get(rolename)
+ r = RoleCache.get(role_name)
if r:
- rolename = r.name
+ role_name = r.name
- OperateRecordCRUD.add(app, rolename, operate_type, obj)
+ OperateRecordCRUD.add(app, role_name, operate_type, obj)
diff --git a/acl-api/api/tasks/common_setting.py b/acl-api/api/tasks/common_setting.py
index ca0f669..053e70f 100644
--- a/acl-api/api/tasks/common_setting.py
+++ b/acl-api/api/tasks/common_setting.py
@@ -1,24 +1,24 @@
# -*- coding:utf-8 -*-
-import requests
from flask import current_app
from api.extensions import celery
-from api.extensions import db
from api.lib.common_setting.acl import ACLManager
-from api.lib.common_setting.const import COMMON_SETTING_QUEUE
+from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
-from api.models.common_setting import Department
+from api.models.common_setting import Department, Employee
+from api.lib.decorator import flush_db
+from api.lib.decorator import reconnect_db
-@celery.task(name="common_setting.edit_employee_department_in_acl", queue=COMMON_SETTING_QUEUE)
+@celery.task(name="common_setting.edit_employee_department_in_acl", queue=ACL_QUEUE)
+@flush_db
+@reconnect_db
def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
"""
:param e_list:{acl_rid: 11, department_id: 22}
:param new_d_id
:param op_uid
"""
- db.session.remove()
-
result = []
new_department = Department.get_by(
first=True, department_id=new_d_id, to_dict=False)
@@ -49,21 +49,20 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
continue
old_d_rid_in_acl = role_map.get(old_department.department_name, 0)
- if old_d_rid_in_acl == 0:
- return
- if old_d_rid_in_acl != old_department.acl_rid:
- old_department.update(
- acl_rid=old_d_rid_in_acl
- )
- d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
- payload = {
- 'app_id': 'acl',
- 'parent_id': d_acl_rid,
- }
- try:
- acl.remove_user_from_role(employee_acl_rid, payload)
- except Exception as e:
- result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
+ if old_d_rid_in_acl > 0:
+ if old_d_rid_in_acl != old_department.acl_rid:
+ old_department.update(
+ acl_rid=old_d_rid_in_acl
+ )
+ d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
+ payload = {
+ 'app_id': 'acl',
+ 'parent_id': d_acl_rid,
+ }
+ try:
+ acl.remove_user_from_role(employee_acl_rid, payload)
+ except Exception as e:
+ result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
payload = {
'app_id': 'acl',
@@ -75,3 +74,57 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
result.append(ErrFormat.acl_add_user_to_role_failed.format(str(e)))
return result
+
+
+@celery.task(name="common_setting.refresh_employee_acl_info", queue=ACL_QUEUE)
+@flush_db
+@reconnect_db
+def refresh_employee_acl_info(current_employee_id=None):
+ acl = ACLManager('acl')
+ role_map = {role['name']: role for role in acl.get_all_roles()}
+
+ criterion = [
+ Employee.deleted == 0
+ ]
+ query = Employee.query.filter(*criterion).order_by(
+ Employee.created_at.desc()
+ )
+ current_employee_rid = 0
+
+ for em in query.all():
+ if current_employee_id and em.employee_id == current_employee_id:
+ current_employee_rid = em.acl_rid if em.acl_rid else 0
+
+ if em.acl_uid and em.acl_rid:
+ continue
+ role = role_map.get(em.username, None)
+ if not role:
+ continue
+
+ params = dict()
+ if not em.acl_uid:
+ params['acl_uid'] = role.get('uid', 0)
+
+ if not em.acl_rid:
+ params['acl_rid'] = role.get('id', 0)
+
+ if current_employee_id and em.employee_id == current_employee_id:
+ current_employee_rid = params['acl_rid'] if params.get('acl_rid', 0) else 0
+
+ try:
+ em.update(**params)
+ current_app.logger.info(
+ f"refresh_employee_acl_info success, employee_id: {em.employee_id}, uid: {em.acl_uid}, "
+ f"rid: {em.acl_rid}")
+ except Exception as e:
+ current_app.logger.error(str(e))
+ continue
+
+ if current_employee_rid and current_employee_rid > 0:
+ try:
+ from api.lib.common_setting.employee import GrantEmployeeACLPerm
+
+ GrantEmployeeACLPerm().grant_by_rid(current_employee_rid, False)
+ current_app.logger.info(f"GrantEmployeeACLPerm success, current_employee_rid: {current_employee_rid}")
+ except Exception as e:
+ current_app.logger.error(str(e))
diff --git a/acl-api/api/translations/zh/LC_MESSAGES/messages.mo b/acl-api/api/translations/zh/LC_MESSAGES/messages.mo
new file mode 100644
index 0000000..aba4214
Binary files /dev/null and b/acl-api/api/translations/zh/LC_MESSAGES/messages.mo differ
diff --git a/acl-api/api/translations/zh/LC_MESSAGES/messages.po b/acl-api/api/translations/zh/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..741e9fd
--- /dev/null
+++ b/acl-api/api/translations/zh/LC_MESSAGES/messages.po
@@ -0,0 +1,525 @@
+# Chinese translations for PROJECT.
+# Copyright (C) 2024 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2024.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2025-08-16 23:41+0800\n"
+"PO-Revision-Date: 2024-01-25 17:19+0800\n"
+"Last-Translator: FULL NAME \n"
+"Language: zh\n"
+"Language-Team: zh \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.17.0\n"
+
+#: api/lib/resp_format.py:7
+msgid "unauthorized"
+msgstr "未认证"
+
+#: api/lib/resp_format.py:8
+msgid "unknown error"
+msgstr "未知错误"
+
+#: api/lib/resp_format.py:10
+msgid "Illegal request"
+msgstr "不合法的请求"
+
+#: api/lib/resp_format.py:11
+msgid "Invalid operation"
+msgstr "无效的操作"
+
+#: api/lib/resp_format.py:13
+msgid "does not exist"
+msgstr "不存在"
+
+#: api/lib/resp_format.py:15
+msgid "There is a circular dependency!"
+msgstr "存在循环依赖!"
+
+#: api/lib/resp_format.py:17
+msgid "Unknown search error"
+msgstr "未知搜索错误"
+
+#: api/lib/resp_format.py:20
+msgid "The json format seems to be incorrect, please confirm carefully!"
+msgstr "# json格式似乎不正确了, 请仔细确认一下!"
+
+#: api/lib/resp_format.py:23
+msgid ""
+"The format of parameter {} is incorrect, the format must be: yyyy-mm-dd "
+"HH:MM:SS"
+msgstr "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS"
+
+#: api/lib/resp_format.py:25
+msgid "The value of parameter {} cannot be empty!"
+msgstr "参数 {} 的值不能为空!"
+
+#: api/lib/resp_format.py:26
+msgid "The request is missing parameters {}"
+msgstr "请求缺少参数 {}"
+
+#: api/lib/resp_format.py:27
+msgid "Invalid value for parameter {}"
+msgstr "参数 {} 的值无效"
+
+#: api/lib/resp_format.py:28
+msgid "The length of parameter {} must be <= {}"
+msgstr "参数 {} 的长度必须 <= {}"
+
+#: api/lib/resp_format.py:30
+msgid "Role {} can only operate!"
+msgstr "角色 {} 才能操作!"
+
+#: api/lib/resp_format.py:31
+msgid "User {} does not exist"
+msgstr "用户 {} 不存在"
+
+#: api/lib/resp_format.py:32
+msgid "For resource: {}, you do not have {} permission!"
+msgstr "您没有资源: {} 的{}权限!"
+
+#: api/lib/resp_format.py:33
+msgid "You do not have permission to operate!"
+msgstr "您没有操作权限!"
+
+#: api/lib/resp_format.py:34
+msgid "Only the creator or administrator has permission!"
+msgstr "只有创建人或者管理员才有权限!"
+
+#: api/lib/common_setting/resp_format.py:8
+msgid "Company info already existed"
+msgstr "公司信息已存在,无法创建!"
+
+#: api/lib/common_setting/resp_format.py:10
+msgid "No file part"
+msgstr "没有文件部分"
+
+#: api/lib/common_setting/resp_format.py:11
+msgid "File is required"
+msgstr "文件是必须的"
+
+#: api/lib/common_setting/resp_format.py:12
+msgid "File not found"
+msgstr "文件不存在!"
+
+#: api/lib/common_setting/resp_format.py:13
+msgid "File type not allowed"
+msgstr "文件类型不允许!"
+
+#: api/lib/common_setting/resp_format.py:14
+#, python-brace-format
+msgid "Upload failed: {}"
+msgstr "上传失败: {}"
+
+#: api/lib/common_setting/resp_format.py:16
+msgid "Direct supervisor is not self"
+msgstr "直属上级不能是自己"
+
+#: api/lib/common_setting/resp_format.py:17
+msgid "Parent department is not self"
+msgstr "上级部门不能是自己"
+
+#: api/lib/common_setting/resp_format.py:18
+msgid "Employee list is empty"
+msgstr "员工列表为空"
+
+#: api/lib/common_setting/resp_format.py:20
+msgid "Column name not support"
+msgstr "不支持的列名"
+
+#: api/lib/common_setting/resp_format.py:21
+msgid "Password is required"
+msgstr "密码是必须的"
+
+#: api/lib/common_setting/resp_format.py:22
+msgid "Employee acl rid is zero"
+msgstr "员工ACL角色ID不能为0"
+
+#: api/lib/common_setting/resp_format.py:24
+#, python-brace-format
+msgid "Generate excel failed: {}"
+msgstr "生成excel失败: {}"
+
+#: api/lib/common_setting/resp_format.py:25
+#, python-brace-format
+msgid "Rename columns failed: {}"
+msgstr "重命名字段失败: {}"
+
+#: api/lib/common_setting/resp_format.py:27
+msgid "Cannot block this employee is other direct supervisor"
+msgstr "该员工是其他员工的直属上级, 不能禁用"
+
+#: api/lib/common_setting/resp_format.py:29
+msgid "Cannot block this employee is department manager"
+msgstr "该员工是部门负责人, 不能禁用"
+
+#: api/lib/common_setting/resp_format.py:30
+#, python-brace-format
+msgid "Employee id [{}] not found"
+msgstr "员工ID [{}] 不存在!"
+
+#: api/lib/common_setting/resp_format.py:31
+msgid "Value is required"
+msgstr "值是必须的"
+
+#: api/lib/common_setting/resp_format.py:32
+msgid "Email already exists"
+msgstr "邮箱已存在!"
+
+#: api/lib/common_setting/resp_format.py:33
+#, python-brace-format
+msgid "Query {} none keep value empty"
+msgstr "查询 {} 空值时请保持value为空"
+
+#: api/lib/common_setting/resp_format.py:34
+#, python-brace-format
+msgid "Not support operator: {}"
+msgstr "不支持的操作符: {}"
+
+#: api/lib/common_setting/resp_format.py:35
+#, python-brace-format
+msgid "Not support relation: {}"
+msgstr "不支持的关系: {}"
+
+#: api/lib/common_setting/resp_format.py:36
+msgid "Conditions field missing"
+msgstr " conditions内元素字段缺失,请检查!"
+
+#: api/lib/common_setting/resp_format.py:37
+#, python-brace-format
+msgid "Datetime format error: {}"
+msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
+
+#: api/lib/common_setting/resp_format.py:38
+msgid "Department level relation error"
+msgstr "部门层级关系不正确"
+
+#: api/lib/common_setting/resp_format.py:39
+msgid "Delete reserved department name"
+msgstr "保留部门,无法删除!"
+
+#: api/lib/common_setting/resp_format.py:40
+msgid "Department id is required"
+msgstr "部门ID是必须的"
+
+#: api/lib/common_setting/resp_format.py:41
+msgid "Department list is required"
+msgstr "部门列表是必须的"
+
+#: api/lib/common_setting/resp_format.py:42
+#, python-brace-format
+msgid "{} Cannot to be parent department"
+msgstr "{} 不能设置为上级部门"
+
+#: api/lib/common_setting/resp_format.py:43
+#, python-brace-format
+msgid "Department id [{}] not found"
+msgstr "部门ID [{}] 不存在"
+
+#: api/lib/common_setting/resp_format.py:44
+msgid "Parent department id must more than zero"
+msgstr "上级部门ID必须大于0"
+
+#: api/lib/common_setting/resp_format.py:45
+#, python-brace-format
+msgid "Department name [{}] already exists"
+msgstr "部门名称 [{}] 已存在"
+
+#: api/lib/common_setting/resp_format.py:46
+msgid "New department is none"
+msgstr "新部门是空的"
+
+#: api/lib/common_setting/resp_format.py:48
+#, python-brace-format
+msgid "ACL edit user failed: {}"
+msgstr "ACL 修改用户失败: {}"
+
+#: api/lib/common_setting/resp_format.py:49
+#, python-brace-format
+msgid "ACL uid not found: {}"
+msgstr "ACL 用户UID [{}] 不存在"
+
+#: api/lib/common_setting/resp_format.py:50
+#, python-brace-format
+msgid "ACL add user failed: {}"
+msgstr "ACL 添加用户失败: {}"
+
+#: api/lib/common_setting/resp_format.py:51
+#, python-brace-format
+msgid "ACL add role failed: {}"
+msgstr "ACL 添加角色失败: {}"
+
+#: api/lib/common_setting/resp_format.py:52
+#, python-brace-format
+msgid "ACL update role failed: {}"
+msgstr "ACL 更新角色失败: {}"
+
+#: api/lib/common_setting/resp_format.py:53
+#, python-brace-format
+msgid "ACL get all users failed: {}"
+msgstr "ACL 获取所有用户失败: {}"
+
+#: api/lib/common_setting/resp_format.py:54
+#, python-brace-format
+msgid "ACL remove user from role failed: {}"
+msgstr "ACL 从角色中移除用户失败: {}"
+
+#: api/lib/common_setting/resp_format.py:55
+#, python-brace-format
+msgid "ACL add user to role failed: {}"
+msgstr "ACL 添加用户到角色失败: {}"
+
+#: api/lib/common_setting/resp_format.py:56
+#, python-brace-format
+msgid "ACL import user failed: {}"
+msgstr "ACL 导入用户失败: {}"
+
+#: api/lib/common_setting/resp_format.py:58
+msgid "Nickname is required"
+msgstr "昵称不能为空"
+
+#: api/lib/common_setting/resp_format.py:59
+msgid "Username is required"
+msgstr "用户名不能为空"
+
+#: api/lib/common_setting/resp_format.py:60
+msgid "Email is required"
+msgstr "邮箱不能为空"
+
+#: api/lib/common_setting/resp_format.py:61
+msgid "Email format error"
+msgstr "邮箱格式错误"
+
+#: api/lib/common_setting/resp_format.py:62
+msgid "Email send timeout"
+msgstr "邮件发送超时"
+
+#: api/lib/common_setting/resp_format.py:64
+#, python-brace-format
+msgid "Common data not found {} "
+msgstr "ID {} 找不到记录"
+
+#: api/lib/common_setting/resp_format.py:65
+#, python-brace-format
+msgid "Common data {} already existed"
+msgstr "{} 已经存在"
+
+#: api/lib/common_setting/resp_format.py:66
+#, python-brace-format
+msgid "Notice platform {} existed"
+msgstr "{} 已经存在"
+
+#: api/lib/common_setting/resp_format.py:67
+#, python-brace-format
+msgid "Notice {} not existed"
+msgstr "{} 配置项不存在"
+
+#: api/lib/common_setting/resp_format.py:68
+msgid "Notice please config messenger first"
+msgstr "请先配置messenger URL"
+
+#: api/lib/common_setting/resp_format.py:69
+msgid "Notice bind err with empty mobile"
+msgstr "绑定错误,手机号为空"
+
+#: api/lib/common_setting/resp_format.py:70
+#, python-brace-format
+msgid "Notice bind failed: {}"
+msgstr "绑定失败: {}"
+
+#: api/lib/common_setting/resp_format.py:71
+msgid "Notice bind success"
+msgstr "绑定成功"
+
+#: api/lib/common_setting/resp_format.py:72
+msgid "Notice remove bind success"
+msgstr "解绑成功"
+
+#: api/lib/common_setting/resp_format.py:74
+#, python-brace-format
+msgid "Not support test type: {}"
+msgstr "不支持的测试类型: {}"
+
+#: api/lib/common_setting/resp_format.py:75
+#, python-brace-format
+msgid "Not support auth type: {}"
+msgstr "不支持的认证类型: {}"
+
+#: api/lib/common_setting/resp_format.py:76
+msgid "LDAP server connect timeout"
+msgstr "LDAP服务器连接超时"
+
+#: api/lib/common_setting/resp_format.py:77
+msgid "LDAP server connect not available"
+msgstr "LDAP服务器连接不可用"
+
+#: api/lib/common_setting/resp_format.py:78
+#, python-brace-format
+msgid "LDAP test unknown error: {}"
+msgstr "LDAP测试未知错误: {}"
+
+#: api/lib/common_setting/resp_format.py:79
+#, python-brace-format
+msgid "Common data not support auth type: {}"
+msgstr "通用数据不支持auth类型: {}"
+
+#: api/lib/common_setting/resp_format.py:80
+msgid "LDAP test username required"
+msgstr "LDAP测试用户名必填"
+
+#: api/lib/common_setting/resp_format.py:82
+msgid "Company wide"
+msgstr "全公司"
+
+#: api/lib/common_setting/resp_format.py:84
+msgid "No permission to access resource {}, perm {} "
+msgstr "您没有资源: {} 的 {} 权限"
+
+#: api/lib/perm/acl/resp_format.py:9
+msgid "login successful"
+msgstr "登录成功"
+
+#: api/lib/perm/acl/resp_format.py:10
+msgid "Failed to connect to LDAP service"
+msgstr "连接LDAP服务失败"
+
+#: api/lib/perm/acl/resp_format.py:11
+msgid "Password verification failed"
+msgstr "密码验证失败"
+
+#: api/lib/perm/acl/resp_format.py:12
+msgid "Application Token verification failed"
+msgstr "应用 Token验证失败"
+
+#: api/lib/perm/acl/resp_format.py:15
+msgid ""
+"You are not the application administrator or the session has expired (try"
+" logging out and logging in again)"
+msgstr "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
+
+#: api/lib/perm/acl/resp_format.py:17
+#, python-brace-format
+msgid "Resource type {} does not exist!"
+msgstr "资源类型 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:18
+#, python-brace-format
+msgid "Resource type {} already exists!"
+msgstr "资源类型 {} 已经存在!"
+
+#: api/lib/perm/acl/resp_format.py:20
+msgid "Because there are resources under this type, they cannot be deleted!"
+msgstr "因为该类型下有资源的存在, 不能删除!"
+
+#: api/lib/perm/acl/resp_format.py:22
+#, python-brace-format
+msgid "User {} does not exist!"
+msgstr "用户 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:23
+#, python-brace-format
+msgid "User {} already exists!"
+msgstr "用户 {} 已经存在!"
+
+#: api/lib/perm/acl/resp_format.py:24
+#, python-brace-format
+msgid "Role {} does not exist!"
+msgstr "角色 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:25
+#, python-brace-format
+msgid "Role {} already exists!"
+msgstr "角色 {} 已经存在!"
+
+#: api/lib/perm/acl/resp_format.py:26
+#, python-brace-format
+msgid "Global role {} does not exist!"
+msgstr "全局角色 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:27
+#, python-brace-format
+msgid "Global role {} already exists!"
+msgstr "全局角色 {} 已经存在!"
+
+#: api/lib/perm/acl/resp_format.py:29
+#, python-brace-format
+msgid "You do not have {} permission on resource: {}"
+msgstr "您没有资源: {} 的 {} 权限"
+
+#: api/lib/perm/acl/resp_format.py:30
+msgid "Requires administrator permissions"
+msgstr "需要管理员权限"
+
+#: api/lib/perm/acl/resp_format.py:31
+#, python-brace-format
+msgid "Requires role: {}"
+msgstr "需要角色: {}"
+
+#: api/lib/perm/acl/resp_format.py:33
+msgid "To delete a user role, please operate on the User Management page!"
+msgstr "删除用户角色, 请在 用户管理 页面操作!"
+
+#: api/lib/perm/acl/resp_format.py:35
+#, python-brace-format
+msgid "Application {} already exists"
+msgstr "应用 {} 已经存在"
+
+#: api/lib/perm/acl/resp_format.py:36
+#, python-brace-format
+msgid "Application {} does not exist!"
+msgstr "应用 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:37
+msgid "The Secret is invalid"
+msgstr "应用的Secret无效"
+
+#: api/lib/perm/acl/resp_format.py:39
+#, python-brace-format
+msgid "Resource {} does not exist!"
+msgstr "资源 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:40
+#, python-brace-format
+msgid "Resource {} already exists!"
+msgstr "资源 {} 已经存在!"
+
+#: api/lib/perm/acl/resp_format.py:42
+#, python-brace-format
+msgid "Resource group {} does not exist!"
+msgstr "资源组 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:43
+#, python-brace-format
+msgid "Resource group {} already exists!"
+msgstr "资源组 {} 已经存在!"
+
+#: api/lib/perm/acl/resp_format.py:45
+msgid "Inheritance detected infinite loop"
+msgstr "继承检测到了死循环"
+
+#: api/lib/perm/acl/resp_format.py:46
+#, python-brace-format
+msgid "Role relationship {} does not exist!"
+msgstr "角色关系 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:48
+#, python-brace-format
+msgid "Trigger {} does not exist!"
+msgstr "触发器 {} 不存在!"
+
+#: api/lib/perm/acl/resp_format.py:49
+#, python-brace-format
+msgid "Trigger {} already exists!"
+msgstr "触发器 {} 已经存在!"
+
+#: api/lib/perm/acl/resp_format.py:50
+#, python-brace-format
+msgid "Trigger {} has been disabled!"
+msgstr "Trigger {} has been disabled!"
+
diff --git a/acl-api/api/views/acl/audit.py b/acl-api/api/views/acl/audit.py
index ae4c20e..9826bb9 100644
--- a/acl-api/api/views/acl/audit.py
+++ b/acl-api/api/views/acl/audit.py
@@ -24,6 +24,7 @@ def get(self, name):
'role': AuditCRUD.search_role,
'trigger': AuditCRUD.search_trigger,
'resource': AuditCRUD.search_resource,
+ 'login': AuditCRUD.search_login,
}
if name not in func_map:
abort(400, f'wrong {name}, please use {func_map.keys()}')
diff --git a/acl-api/api/views/acl/login.py b/acl-api/api/views/acl/login.py
index 09ee89a..d1669ce 100644
--- a/acl-api/api/views/acl/login.py
+++ b/acl-api/api/views/acl/login.py
@@ -1,22 +1,27 @@
# -*- coding:utf-8 -*-
import datetime
-
import jwt
import six
from flask import abort
from flask import current_app
from flask import request
from flask import session
-from flask_login import login_user, logout_user
+from flask_login import login_user
+from flask_login import logout_user
+from api.lib.common_setting.common_data import AuthenticateDataCRUD
+from api.lib.common_setting.const import AuthenticateType
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
+from api.lib.perm.acl.audit import AuditCRUD
+from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import User
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
+from api.lib.perm.acl.role import RoleRelationCRUD
from api.lib.perm.auth import auth_abandoned
from api.lib.perm.auth import auth_with_app_token
from api.models.acl import Role
@@ -34,8 +39,11 @@ def post(self):
username = request.values.get("username") or request.values.get("email")
password = request.values.get("password")
_role = None
- if current_app.config.get('AUTH_WITH_LDAP'):
- user, authenticated = User.query.authenticate_with_ldap(username, password)
+ auth_with_ldap = request.values.get('auth_with_ldap', True)
+ config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
+ if (config.get('enabled') or config.get('enable')) and auth_with_ldap:
+ from api.lib.perm.authentication.ldap import authenticate_with_ldap
+ user, authenticated = authenticate_with_ldap(username, password)
else:
user, authenticated = User.query.authenticate(username, password)
if not user:
@@ -117,10 +125,17 @@ def post(self):
if not user.get('username'):
user['username'] = user.get('name')
- return self.jsonify(user=user,
- authenticated=authenticated,
- rid=role and role.id,
- can_proxy=can_proxy)
+ result = dict(user=user,
+ authenticated=authenticated,
+ rid=role and role.id,
+ can_proxy=can_proxy)
+
+ if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
+ app_id = AppCache.get(request.values.get('app_id'))
+ parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
+ result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
+
+ return self.jsonify(result)
class AuthWithTokenView(APIView):
@@ -176,4 +191,9 @@ class LogoutView(APIView):
@auth_abandoned
def post(self):
logout_user()
+
+ AuditCRUD.add_login_log(None, None, None,
+ _id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
+ logout_at=datetime.datetime.now())
+
self.jsonify(code=200)
diff --git a/acl-api/api/views/acl/resources.py b/acl-api/api/views/acl/resources.py
index df080b8..4a15a0b 100644
--- a/acl-api/api/views/acl/resources.py
+++ b/acl-api/api/views/acl/resources.py
@@ -1,6 +1,5 @@
# -*- coding:utf-8 -*-
-from flask import g
from flask import request
from flask_login import current_user
@@ -104,7 +103,7 @@ def post(self):
type_id = request.values.get('type_id')
app_id = request.values.get('app_id')
uid = request.values.get('uid')
- if not uid and hasattr(g, "user") and hasattr(current_user, "uid"):
+ if not uid and hasattr(current_user, "uid"):
uid = current_user.uid
resource = ResourceCRUD.add(name, type_id, app_id, uid)
diff --git a/acl-api/api/views/acl/user.py b/acl-api/api/views/acl/user.py
index d5e0d86..51bcca5 100644
--- a/acl-api/api/views/acl/user.py
+++ b/acl-api/api/views/acl/user.py
@@ -4,7 +4,6 @@
import requests
from flask import abort
from flask import current_app
-from flask import g
from flask import request
from flask import session
from flask_login import current_user
@@ -12,6 +11,7 @@
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
+from api.lib.perm.acl.acl import AuditCRUD
from api.lib.perm.acl.acl import role_required
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
@@ -49,6 +49,13 @@ def get(self):
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar'))
+ if request.values.get('channel'):
+ _id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
+ ip=request.values.get('ip'),
+ browser=request.values.get('browser'))
+ session['LOGIN_ID'] = _id
+ result['LOGIN_ID'] = _id
+
current_app.logger.info("get user info for3: {}".format(result))
return self.jsonify(result=result)
@@ -161,7 +168,7 @@ def post(self):
if app.name not in ('cas-server', 'acl'):
return abort(403, ErrFormat.invalid_request)
- elif hasattr(g, 'user'):
+ elif hasattr(current_user, 'username'):
if current_user.username != request.values['username']:
return abort(403, ErrFormat.invalid_request)
diff --git a/acl-api/api/views/common_setting/auth_config.py b/acl-api/api/views/common_setting/auth_config.py
new file mode 100644
index 0000000..5ec2840
--- /dev/null
+++ b/acl-api/api/views/common_setting/auth_config.py
@@ -0,0 +1,88 @@
+from flask import abort, request
+
+from api.lib.common_setting.common_data import AuthenticateDataCRUD
+from api.lib.common_setting.const import TestType
+from api.lib.common_setting.resp_format import ErrFormat
+from api.lib.perm.acl.acl import role_required
+from api.resource import APIView
+
+prefix = '/auth_config'
+
+
+class AuthConfigView(APIView):
+ url_prefix = (f'{prefix}/',)
+
+ @role_required("acl_admin")
+ def get(self, auth_type):
+ cli = AuthenticateDataCRUD(auth_type)
+
+ if auth_type not in cli.get_support_type_list():
+ abort(400, ErrFormat.not_support_auth_type.format(auth_type))
+
+ if auth_type in cli.common_type_list:
+ data = cli.get_record(True)
+ else:
+ data = cli.get_record_with_decrypt()
+ return self.jsonify(data)
+
+ @role_required("acl_admin")
+ def post(self, auth_type):
+ cli = AuthenticateDataCRUD(auth_type)
+
+ if auth_type not in cli.get_support_type_list():
+ abort(400, ErrFormat.not_support_auth_type.format(auth_type))
+
+ params = request.json
+ data = params.get('data', {})
+ if auth_type in cli.common_type_list:
+ data['encrypt'] = False
+ cli.create(data)
+
+ return self.jsonify(params)
+
+
+class AuthConfigViewWithId(APIView):
+ url_prefix = (f'{prefix}//',)
+
+ @role_required("acl_admin")
+ def put(self, auth_type, _id):
+ cli = AuthenticateDataCRUD(auth_type)
+
+ if auth_type not in cli.get_support_type_list():
+ abort(400, ErrFormat.not_support_auth_type.format(auth_type))
+
+ params = request.json
+ data = params.get('data', {})
+ if auth_type in cli.common_type_list:
+ data['encrypt'] = False
+
+ res = cli.update(_id, data)
+
+ return self.jsonify(res.to_dict())
+
+ @role_required("acl_admin")
+ def delete(self, auth_type, _id):
+ cli = AuthenticateDataCRUD(auth_type)
+
+ if auth_type not in cli.get_support_type_list():
+ abort(400, ErrFormat.not_support_auth_type.format(auth_type))
+ cli.delete(_id)
+ return self.jsonify({})
+
+
+class AuthEnableListView(APIView):
+ url_prefix = (f'{prefix}/enable_list',)
+
+ method_decorators = []
+
+ def get(self):
+ return self.jsonify(AuthenticateDataCRUD.get_enable_list())
+
+
+class AuthConfigTestView(APIView):
+ url_prefix = (f'{prefix}//test',)
+
+ def post(self, auth_type):
+ test_type = request.values.get('test_type', TestType.Connect)
+ params = request.json
+ return self.jsonify(AuthenticateDataCRUD(auth_type).test(test_type, params.get('data')))
diff --git a/acl-api/api/views/common_setting/common_data.py b/acl-api/api/views/common_setting/common_data.py
new file mode 100644
index 0000000..6d44ba1
--- /dev/null
+++ b/acl-api/api/views/common_setting/common_data.py
@@ -0,0 +1,35 @@
+from flask import request
+
+from api.lib.common_setting.common_data import CommonDataCRUD
+from api.resource import APIView
+
+prefix = '/data'
+
+
+class DataView(APIView):
+ url_prefix = (f'{prefix}/',)
+
+ def get(self, data_type):
+ data_list = CommonDataCRUD.get_data_by_type(data_type)
+
+ return self.jsonify(data_list)
+
+ def post(self, data_type):
+ params = request.json
+ CommonDataCRUD.create_new_data(data_type, **params)
+
+ return self.jsonify(params)
+
+
+class DataViewWithId(APIView):
+ url_prefix = (f'{prefix}//',)
+
+ def put(self, data_type, _id):
+ params = request.json
+ res = CommonDataCRUD.update_data(_id, **params)
+
+ return self.jsonify(res.to_dict())
+
+ def delete(self, data_type, _id):
+ CommonDataCRUD.delete(_id)
+ return self.jsonify({})
diff --git a/acl-api/api/views/common_setting/company_info.py b/acl-api/api/views/common_setting/company_info.py
index d2aca2a..4298cfd 100644
--- a/acl-api/api/views/common_setting/company_info.py
+++ b/acl-api/api/views/common_setting/company_info.py
@@ -1,9 +1,7 @@
# -*- coding:utf-8 -*-
-from flask import abort
from flask import request
from api.lib.common_setting.company_info import CompanyInfoCRUD
-from api.lib.common_setting.resp_format import ErrFormat
from api.resource import APIView
prefix = '/company'
@@ -16,15 +14,16 @@ def get(self):
return self.jsonify(CompanyInfoCRUD.get())
def post(self):
- info = CompanyInfoCRUD.get()
- if info:
- abort(400, ErrFormat.company_info_is_already_existed)
data = {
'info': {
**request.values
}
}
- d = CompanyInfoCRUD.create(**data)
+ info = CompanyInfoCRUD.get()
+ if info:
+ d = CompanyInfoCRUD.update(info.get('id'), **data)
+ else:
+ d = CompanyInfoCRUD.create(**data)
res = d.to_dict()
return self.jsonify(res)
diff --git a/acl-api/api/views/common_setting/department.py b/acl-api/api/views/common_setting/department.py
index 9a8dd4a..ba4091a 100644
--- a/acl-api/api/views/common_setting/department.py
+++ b/acl-api/api/views/common_setting/department.py
@@ -62,7 +62,7 @@ def post(self):
class DepartmentIDView(APIView):
url_prefix = (f'{prefix}/',)
- def get(self, _id):
+ def put(self, _id):
form = DepartmentForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg))
diff --git a/acl-api/api/views/common_setting/employee.py b/acl-api/api/views/common_setting/employee.py
index 56adf8b..173dc13 100644
--- a/acl-api/api/views/common_setting/employee.py
+++ b/acl-api/api/views/common_setting/employee.py
@@ -1,7 +1,5 @@
# -*- coding:utf-8 -*-
-import os
-
-from flask import abort, current_app, send_from_directory
+from flask import abort
from flask import request
from werkzeug.datastructures import MultiDict
@@ -145,3 +143,26 @@ def get(self):
result = EmployeeCRUD.get_all_position()
return self.jsonify(result)
+
+class GetEmployeeNoticeByIds(APIView):
+ url_prefix = (f'{prefix}/get_notice_by_ids',)
+
+ def post(self):
+ employee_ids = request.json.get('employee_ids', [])
+ if not employee_ids:
+ result = []
+ else:
+ result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids)
+ return self.jsonify(result)
+
+
+class EmployeeBindNoticeWithACLID(APIView):
+ url_prefix = (f'{prefix}/by_uid/bind_notice//',)
+
+ def put(self, platform, _uid):
+ data = EmployeeCRUD.bind_notice_by_uid(platform, _uid)
+ return self.jsonify(info=data)
+
+ def delete(self, platform, _uid):
+ data = EmployeeCRUD.remove_bind_notice_by_uid(platform, _uid)
+ return self.jsonify(info=data)
diff --git a/acl-api/api/views/common_setting/file_manage.py b/acl-api/api/views/common_setting/file_manage.py
index 23e0047..fb304df 100644
--- a/acl-api/api/views/common_setting/file_manage.py
+++ b/acl-api/api/views/common_setting/file_manage.py
@@ -1,17 +1,18 @@
# -*- coding:utf-8 -*-
-import os
-
-from flask import request, abort, current_app, send_from_directory
+from flask import request, abort, current_app
from werkzeug.utils import secure_filename
+import lz4.frame
+import magic
+from api.lib.common_setting.const import MIMEExtMap
from api.lib.common_setting.resp_format import ErrFormat
-from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name
+from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name, CommonFileCRUD
from api.resource import APIView
prefix = '/file'
ALLOWED_EXTENSIONS = {
- 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv'
+ 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg'
}
@@ -28,7 +29,8 @@ class GetFileView(APIView):
url_prefix = (f'{prefix}/',)
def get(self, _filename):
- return send_from_directory(current_app.config['UPLOAD_DIRECTORY_FULL'], _filename, as_attachment=True)
+ file_stream = CommonFileCRUD.get_file(_filename)
+ return self.send_file(file_stream, as_attachment=True, download_name=_filename)
class PostFileView(APIView):
@@ -43,21 +45,35 @@ def post(self):
if not file:
abort(400, ErrFormat.file_is_required)
- extension = file.mimetype.split('/')[-1]
- if file.filename == '':
- filename = f'.{extension}'
- else:
- if extension not in file.filename:
- filename = file.filename + f".{extension}"
- else:
- filename = file.filename
-
- if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)):
- filename = generate_new_file_name(filename)
- filename = secure_filename(filename)
- file.save(os.path.join(
- current_app.config['UPLOAD_DIRECTORY_FULL'], filename))
-
- return self.jsonify(file_name=filename)
-
- abort(400, 'Extension not allow')
+
+ m_type = magic.from_buffer(file.read(2048), mime=True)
+ file.seek(0)
+
+ if m_type == 'application/octet-stream':
+ m_type = file.mimetype
+ elif m_type == 'text/plain':
+ # https://github.com/ahupp/python-magic/issues/193
+ m_type = m_type if file.mimetype == m_type else file.mimetype
+
+ extension = MIMEExtMap.get(m_type, None)
+
+ if extension is None:
+ abort(400, f"不支持的文件类型: {m_type}")
+
+ filename = file.filename if file.filename and file.filename.endswith(extension) else file.filename + extension
+
+ new_filename = generate_new_file_name(filename)
+ new_filename = secure_filename(new_filename)
+ file_content = file.read()
+ compressed_data = lz4.frame.compress(file_content)
+ try:
+ CommonFileCRUD.add_file(
+ origin_name=filename,
+ file_name=new_filename,
+ binary=compressed_data,
+ )
+
+ return self.jsonify(file_name=new_filename)
+ except Exception as e:
+ current_app.logger.error(e)
+ abort(400, ErrFormat.upload_failed.format(e))
diff --git a/acl-api/api/views/common_setting/notice_config.py b/acl-api/api/views/common_setting/notice_config.py
new file mode 100644
index 0000000..e5dea63
--- /dev/null
+++ b/acl-api/api/views/common_setting/notice_config.py
@@ -0,0 +1,79 @@
+from flask import request, abort, current_app
+from werkzeug.datastructures import MultiDict
+
+from api.lib.perm.auth import auth_with_app_token
+from api.models.common_setting import NoticeConfig
+from api.resource import APIView
+from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD
+from api.lib.decorator import args_required
+from api.lib.common_setting.resp_format import ErrFormat
+
+prefix = '/notice_config'
+
+
+class NoticeConfigView(APIView):
+ url_prefix = (f'{prefix}',)
+
+ @args_required('platform')
+ @auth_with_app_token
+ def get(self):
+ platform = request.args.get('platform')
+ res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {}
+ return self.jsonify(res)
+
+ def post(self):
+ form = NoticeConfigForm(MultiDict(request.json))
+ if not form.validate():
+ abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
+
+ data = NoticeConfigCRUD.add_notice_config(**form.data)
+ return self.jsonify(data.to_dict())
+
+
+class NoticeConfigUpdateView(APIView):
+ url_prefix = (f'{prefix}/',)
+
+ def put(self, _id):
+ form = NoticeConfigUpdateForm(MultiDict(request.json))
+ if not form.validate():
+ abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
+
+ data = NoticeConfigCRUD.edit_notice_config(_id, **form.data)
+ return self.jsonify(data.to_dict())
+
+
+class CheckEmailServer(APIView):
+ url_prefix = (f'{prefix}/send_test_email',)
+
+ def post(self):
+ receive_address = request.args.get('receive_address')
+ info = request.values.get('info', {})
+
+ try:
+
+ result = NoticeConfigCRUD.test_send_email(receive_address, **info)
+ return self.jsonify(result=result)
+ except Exception as e:
+ current_app.logger.error('test_send_email err:')
+ current_app.logger.error(e)
+ if 'Timed Out' in str(e):
+ abort(400, ErrFormat.email_send_timeout)
+ abort(400, f"{str(e)}")
+
+
+class NoticeConfigGetView(APIView):
+ method_decorators = []
+ url_prefix = (f'{prefix}/all',)
+
+ @auth_with_app_token
+ def get(self):
+ res = NoticeConfigCRUD.get_all()
+ return self.jsonify(res)
+
+
+class NoticeAppBotView(APIView):
+ url_prefix = (f'{prefix}/app_bot',)
+
+ def get(self):
+ res = NoticeConfigCRUD.get_app_bot()
+ return self.jsonify(res)
diff --git a/acl-api/api/views/common_setting/system_language.py b/acl-api/api/views/common_setting/system_language.py
new file mode 100644
index 0000000..d9defc3
--- /dev/null
+++ b/acl-api/api/views/common_setting/system_language.py
@@ -0,0 +1,37 @@
+import os
+
+from api.resource import APIView
+from api.lib.perm.auth import auth_abandoned
+
+prefix = "/system"
+
+
+class SystemLanguageView(APIView):
+ url_prefix = (f"{prefix}/language",)
+
+ method_decorators = []
+
+ @auth_abandoned
+ def get(self):
+ """Get system default language
+ Read from environment variable SYSTEM_DEFAULT_LANGUAGE, default to Chinese if not set
+ """
+ default_language = os.environ.get("SYSTEM_DEFAULT_LANGUAGE", "")
+
+ return self.jsonify(
+ {
+ "language": default_language,
+ "language_name": self._get_language_name(default_language),
+ }
+ )
+
+ def _get_language_name(self, language_code):
+ """Return language name based on language code"""
+ language_mapping = {
+ "zh-CN": "中文(简体)",
+ "zh-TW": "中文(繁体)",
+ "en-US": "English",
+ "ja-JP": "日本語",
+ "ko-KR": "한국어",
+ }
+ return language_mapping.get(language_code, "")
diff --git a/acl-api/babel.cfg b/acl-api/babel.cfg
new file mode 100644
index 0000000..991e57e
--- /dev/null
+++ b/acl-api/babel.cfg
@@ -0,0 +1 @@
+[python: api/**.py]
diff --git a/acl-api/requirements.txt b/acl-api/requirements.txt
index 31b6886..8fac696 100644
--- a/acl-api/requirements.txt
+++ b/acl-api/requirements.txt
@@ -78,3 +78,5 @@ vine==1.3.0
Werkzeug==0.15.5
WTForms==3.0.0
zipp==3.16.0
+python-magic==0.4.27
+python-redis-lock==4.0.0
diff --git a/acl-api/settings.example.py b/acl-api/settings.example.py
index 3db205d..606408a 100644
--- a/acl-api/settings.example.py
+++ b/acl-api/settings.example.py
@@ -66,26 +66,76 @@
}
}
-# # SSO
-CAS_SERVER = "http://sso.xxx.com"
-CAS_VALIDATE_SERVER = "http://sso.xxx.com"
-CAS_LOGIN_ROUTE = "/cas/login"
-CAS_LOGOUT_ROUTE = "/cas/logout"
-CAS_VALIDATE_ROUTE = "/cas/serviceValidate"
-CAS_AFTER_LOGIN = "/"
-DEFAULT_SERVICE = "http://127.0.0.1:8000"
-
-# # ldap
-AUTH_WITH_LDAP = False
-LDAP_SERVER = ''
-LDAP_DOMAIN = ''
+# =============================== Authentication ===========================================================
+
+# # CAS
+CAS = dict(
+ enabled=False,
+ cas_server='https://{your-CASServer-hostname}',
+ cas_validate_server='https://{your-CASServer-hostname}',
+ cas_login_route='/cas/built-in/cas/login',
+ cas_logout_route='/cas/built-in/cas/logout',
+ cas_validate_route='/cas/built-in/cas/serviceValidate',
+ cas_after_login='/',
+ cas_user_map={
+ 'username': {'tag': 'cas:user'},
+ 'nickname': {'tag': 'cas:attribute', 'attrs': {'name': 'displayName'}},
+ 'email': {'tag': 'cas:attribute', 'attrs': {'name': 'email'}},
+ 'mobile': {'tag': 'cas:attribute', 'attrs': {'name': 'phone'}},
+ 'avatar': {'tag': 'cas:attribute', 'attrs': {'name': 'avatar'}},
+ }
+)
+
+# # OAuth2.0
+OAUTH2 = dict(
+ enabled=False,
+ client_id='',
+ client_secret='',
+ authorize_url='https://{your-OAuth2Server-hostname}/login/oauth/authorize',
+ token_url='https://{your-OAuth2Server-hostname}/api/login/oauth/access_token',
+ scopes=['profile', 'email'],
+ user_info={
+ 'url': 'https://{your-OAuth2Server-hostname}/api/userinfo',
+ 'email': 'email',
+ 'username': 'name',
+ 'avatar': 'picture'
+ },
+ after_login='/'
+)
+
+# # OIDC
+OIDC = dict(
+ enabled=False,
+ client_id='',
+ client_secret='',
+ authorize_url='https://{your-OIDCServer-hostname}/login/oauth/authorize',
+ token_url='https://{your-OIDCServer-hostname}/api/login/oauth/access_token',
+ scopes=['openid', 'profile', 'email'],
+ user_info={
+ 'url': 'https://{your-OIDCServer-hostname}/api/userinfo',
+ 'email': 'email',
+ 'username': 'name',
+ 'avatar': 'picture'
+ },
+ after_login='/'
+)
+
+# # LDAP
+LDAP = dict(
+ enabled=False,
+ ldap_server='',
+ ldap_domain='',
+ ldap_user_dn='cn={},ou=users,dc=xxx,dc=com'
+)
+# ==========================================================================================================
+
# # pagination
DEFAULT_PAGE_COUNT = 50
# # permission
WHITE_LIST = ["127.0.0.1"]
-USE_ACL = False
+USE_ACL = True
# # elastic search
ES_HOST = '127.0.0.1'
diff --git a/acl-ui/src/assets/icons/ops-move-icon.svg b/acl-ui/src/assets/icons/ops-move-icon.svg
new file mode 100644
index 0000000..c10fc54
--- /dev/null
+++ b/acl-ui/src/assets/icons/ops-move-icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/acl-ui/src/config/setting.js b/acl-ui/src/config/setting.js
index 4b7b85b..47a7f8b 100644
--- a/acl-ui/src/config/setting.js
+++ b/acl-ui/src/config/setting.js
@@ -15,7 +15,7 @@
*/
export default {
- useSSO: false,
+ useSSO: true,
ssoLoginUrl: '/api/sso/login',
primaryColor: '#1890ff', // primary color of ant design
navTheme: 'dark', // theme for nav menu
diff --git a/acl-ui/src/core/icons.js b/acl-ui/src/core/icons.js
index 76bf5d8..cbea4d7 100644
--- a/acl-ui/src/core/icons.js
+++ b/acl-ui/src/core/icons.js
@@ -6,6 +6,7 @@
* 自定义图标加载表
* 所有图标均从这里加载,方便管理
*/
-
+import ops_move_icon from '@/assets/icons/ops-move-icon.svg?inline'
export {
+ ops_move_icon
}
diff --git a/acl-ui/src/modules/acl/views/resources.vue b/acl-ui/src/modules/acl/views/resources.vue
index f888088..818ca14 100644
--- a/acl-ui/src/modules/acl/views/resources.vue
+++ b/acl-ui/src/modules/acl/views/resources.vue
@@ -36,7 +36,7 @@
$refs.resourceBatchPerm.open(currentType.id)
}
"
- >便捷授权便捷授权
-
+
成员
diff --git a/acl-ui/src/views/setting/companyStructure/BatchUpload.vue b/acl-ui/src/views/setting/companyStructure/BatchUpload.vue
index 7582e22..5060fe6 100644
--- a/acl-ui/src/views/setting/companyStructure/BatchUpload.vue
+++ b/acl-ui/src/views/setting/companyStructure/BatchUpload.vue
@@ -53,27 +53,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ row.err }}
diff --git a/acl-ui/src/views/setting/companyStructure/EmployeeModal.vue b/acl-ui/src/views/setting/companyStructure/EmployeeModal.vue
index 1c65ac3..b25fc0b 100644
--- a/acl-ui/src/views/setting/companyStructure/EmployeeModal.vue
+++ b/acl-ui/src/views/setting/companyStructure/EmployeeModal.vue
@@ -382,9 +382,9 @@
:style="{ display: 'inline-block', width: '98%', margin: '0 7px 24px' }"
v-if="
attributes.findIndex((v) => v == 'bank_card_number') !== -1 ||
- attributes.findIndex((v) => v == 'bank_card_name') !== -1 ||
- attributes.findIndex((v) => v == 'opening_bank') !== -1 ||
- attributes.findIndex((v) => v == 'account_opening_location') !== -1
+ attributes.findIndex((v) => v == 'bank_card_name') !== -1 ||
+ attributes.findIndex((v) => v == 'opening_bank') !== -1 ||
+ attributes.findIndex((v) => v == 'account_opening_location') !== -1
"
>
diff --git a/acl-ui/src/views/setting/companyStructure/index.vue b/acl-ui/src/views/setting/companyStructure/index.vue
index 7620d25..ba0c85d 100644
--- a/acl-ui/src/views/setting/companyStructure/index.vue
+++ b/acl-ui/src/views/setting/companyStructure/index.vue
@@ -25,9 +25,7 @@