Code Monkey home page Code Monkey logo

longguikeji / arkid Goto Github PK

View Code? Open in Web Editor NEW
1.5K 97.0 281.0 33.95 MB

一账通是一款开源的统一身份认证授权管理解决方案,支持多种标准协议(LDAP, OAuth2, SAML, OpenID),细粒度权限控制,完整的WEB管理功能,钉钉、企业微信集成等,QQ group: 167885406

Home Page: https://longguikeji.github.io/arkid/

License: GNU Affero General Public License v3.0

Dockerfile 0.06% Python 98.61% HTML 1.30% Shell 0.03%
openid saml sso ldap idp identity-provider idaas iam oidc cas identity oauth

arkid's Introduction

title
首页

ArkID : 企业级IDaaS/IAM平台系统

  • 丰富的插件,快速构建专属IDaaS/IAM平台
  • 轻松集成您的所有应用
  • 统一的身份,认证,权限管理系统

https://arkid.cc

https://idaas.arkid.cc


功能特点

插件化

  • 基于Plug-in插件化可扩展的底层应用架构
  • 可在不改动主体程序的情况下灵活极速的给主体程序增加新功能

统一目录

  • 实现企业组织架构和海量人员身份信息的集中安全存储
  • 多维度建立对应关系,安全集成企业身份数据源
  • 实现在一个平台对企业人员、组织架构、应用信息的高效统一管理

单点登录

  • 通过一套用户名和密码即可访问所有工作系统
  • 支持如OIDC,OAuth2,CAS,SAML2等单点登录协议
  • 通过插件定向支持各类自有协议或非标准协议
  • 通过浏览器插件支持账密代填的方式

账号生命周期

  • 实现人员入离调转/组织架构变动等身份信息在不同应用系统中的自动化流转
  • 有效提升账户配置效率,管理时间缩短90%
  • 增加审计效率

多因素认证

  • 支持手机,邮箱,人脸,指纹,扫码等各类认证因素
  • 支持图形验证码,动态验证码等各类二次认证
  • 支持微信,钉钉,企业微信等第三方认证
  • 根据客户不同业务场景及安全需要智能唤起不同组合形式的认证规则

集中授权

  • 支持RBAC,ABAC的权限管理
  • 支持权限数据统一录入,存储和分配
  • 统一权限验证,支持第三方权限系统集成

集中审批

  • 支持自定义审批动作
  • 支持对接第三方审批系统

数据同步

  • 支持SCIM协议,跨系统同步用户与组织架构数据
  • 支持包括用户,组织架构,认证,权限等各类数据同步
  • 支持对多个三方系统以各种组合方式进行同步,如将HR系统数据同步至AD或LDAP服务中

安全审计

  • 实时记录用户和管理员的所有请求行为
  • 支持日志读取至第三方数据分析或安全分析系统中
  • 支持在系统中直接展示第三方数据分析系统的图表
  • 支持安全分析系统对认证规则和认证结果做出干预,实现安全的智能控制

客户端

  • 自带WEB页面
  • 支持各类小程序,手机客户端
  • 插件无需编写前端代码即可展示页面

云原生

  • 支持Docker开发
  • 支持Docker,K8S容器化部署

低代码开发框架

  • 对OpenAPI进行扩展,使前端页面由后端配置驱动
  • 快速构建页面原型
  • 支持自定义CSS主题

插件与应用商店

  • 支持开发者共享出售插件
  • 支持SaaS应用伪本地化
  • 支持代理商代理与分成

arkid's People

Contributors

dependabot[bot] avatar devops-longguikeji avatar fanhe-lg avatar guancyxx avatar hanbinloop avatar jinji-hanbin avatar longgui-penglei avatar luolu-lg avatar meihailin avatar meixinyue avatar notevery avatar oo-rr-oo avatar rockli avatar sanguomao avatar shiyuelongguikeji avatar skoogi avatar welylongguikeji avatar xiaowei0919 avatar zenkimax avatar zhiyuchen-aqumon avatar zichoolelonggui avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

arkid's Issues

同步钉钉账号失败

image
接口返回信息如下:

{
    "result": "{\"exc_type\": \"MaybeEncodingError\", \"exc_message\": [\"'PicklingError(\\\"Can\\\\'t pickle <class \\\\'thirdparty_data_sdk.dingding.dingsdk.error_utils.APICallError\\\\'>: it\\\\'s not the same object as thirdparty_data_sdk.dingding.dingsdk.error_utils.APICallError\\\",)'\", \"'(1, <ExceptionInfo: APICallError()>, None)'\"], \"exc_module\": \"billiard.pool\"}",
    "status": 3,
    "status_raw": "FAILURE"
}

不知道为什么,第一次导入的时候只能导入顶级部门的成员,所有子部门账号均导入失败。

文档报错, 怎么配置?

image

本地文档报错, 是哪里要配置吗?
等在这个view上加了serializer_class修复后,还有很其它的view?

第三方登录的需求(包含讨论)

openEuler社区提的gitee登录需求:

登录时,点击gitee按钮后,完成重定向功能以后,打开绑定表单,自动填充用户名,无需设置密码,完成注册。

这里的问题是,按照原本的逻辑,应该是gitee登录后要求绑定用户。
原本是,gitee登录后,前端获取到gitee用户信息,再要求用户完成arkid的登录

使用钉钉账号同步的时候报错

使用的是docker-compose一键拉起的,保存配置是500错误,response如下:

RequestException at /siteapi/oneid/config/
[38f6aa1c-52df-11ea-ab4d-0242ac120005]request sign failed. No PEM start marker "b'-----BEGIN RSA PRIVATE KEY-----'" found

Request Method: PATCH
Request URL: http://10.10.10.76/siteapi/oneid/config/
Django Version: 2.0.13
Python Executable: /usr/local/bin/uwsgi
Python Version: 3.6.9
Python Path: ['.', '', '/usr/local/lib/python36.zip', '/usr/local/lib/python3.6', '/usr/local/lib/python3.6/lib-dynload', '/usr/local/lib/python3.6/site-packages', '/var/oneid']
Server time: 星期三, 19 二月 2020 14:15:27 +0800
Installed Applications:
['django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django_celery_results',
'django_celery_beat',
'corsheaders',
'rest_framework',
'rest_framework.authtoken',
'drf_expiring_authtoken',
'coreapi',
'tasksapp',
'siteadmin',
'siteapi',
'oneid_meta',
'oauth2_provider',
'infrastructure',
'captcha',
'ldap.sql_backend']
Installed Middleware:
['corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'common.django.middleware.CrequestMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware']

Traceback:

File "/usr/local/lib/python3.6/site-packages/alipay/aop/api/DefaultAlipayClient.py" in __prepare_request_params
125. sign = sign_with_rsa2(self.__config.app_private_key, sign_content, self.__config.charset)

File "/usr/local/lib/python3.6/site-packages/alipay/aop/api/util/SignatureUtils.py" in sign_with_rsa2
49. signature = rsa.sign(sign_content, rsa.PrivateKey.load_pkcs1(private_key, format='PEM'), 'SHA-256')

File "/usr/local/lib/python3.6/site-packages/rsa/key.py" in load_pkcs1
118. return method(keyfile)

File "/usr/local/lib/python3.6/site-packages/rsa/key.py" in _load_pkcs1_pem
559. der = rsa.pem.load_pem(keyfile, b'RSA PRIVATE KEY')

File "/usr/local/lib/python3.6/site-packages/rsa/pem.py" in load_pem
92. raise ValueError('No PEM start marker "%s" found' % pem_start)

During handling of the above exception (No PEM start marker "b'-----BEGIN RSA PRIVATE KEY-----'" found), another exception occurred:

File "/usr/local/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
35. response = get_response(request)

File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
128. response = self.process_exception_by_middleware(e, request)

File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
126. response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/usr/local/lib/python3.6/site-packages/django/views/decorators/csrf.py" in wrapped_view
54. return view_func(*args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/django/views/generic/base.py" in view
69. return self.dispatch(request, *args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in dispatch
497. response = self.handle_exception(exc)

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in handle_exception
457. self.raise_uncaught_exception(exc)

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in raise_uncaught_exception
468. raise exc

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in dispatch
494. response = handler(request, *args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework/generics.py" in patch
258. return self.partial_update(request, *args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework/mixins.py" in partial_update
82. return self.update(request, *args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework/mixins.py" in update
68. self.perform_update(serializer)

File "./siteapi/v1/views/config.py" in perform_update
45. super().perform_update(serializer) # pylint: disable=no-member

File "/usr/local/lib/python3.6/site-packages/rest_framework/mixins.py" in perform_update
78. serializer.save()

File "/usr/local/lib/python3.6/site-packages/rest_framework/serializers.py" in save
208. self.instance = self.update(self.instance, validated_data)

File "/usr/local/lib/python3.6/contextlib.py" in inner
52. return func(*args, **kwds)

File "./siteapi/v1/serializers/config.py" in update
278. serializer.save()

File "/usr/local/lib/python3.6/site-packages/rest_framework/serializers.py" in save
208. self.instance = self.update(self.instance, validated_data)

File "./siteapi/v1/serializers/qr_app_config.py" in update
195. instance.qr_app_valid = self.validate_qr_app_config(instance)

File "./siteapi/v1/serializers/qr_app_config.py" in validate_qr_app_config
207. is_valid = instance.check_valid()

File "./oneid_meta/models/config.py" in check_valid
373. alipay_public_key=self.alipay_public_key).check_config_valid()

File "./thirdparty_data_sdk/alipay_api/alipay_oauth_manager.py" in check_config_valid
38. alipay_public_key=self.alipay_public_key)

File "./thirdparty_data_sdk/alipay_api/alipay_user_id_sdk.py" in check_valid
98. alipay_public_key=alipay_public_key)

File "./thirdparty_data_sdk/alipay_api/alipay_user_id_sdk.py" in get_alipay_oauth_token_response
82. response_content = client.execute(request)

File "/usr/local/lib/python3.6/site-packages/alipay/aop/api/DefaultAlipayClient.py" in execute
212. query_string, params = self.__prepare_request(request)

File "/usr/local/lib/python3.6/site-packages/alipay/aop/api/DefaultAlipayClient.py" in __prepare_request
89. common_params, params = self.__prepare_request_params(request)

File "/usr/local/lib/python3.6/site-packages/alipay/aop/api/DefaultAlipayClient.py" in __prepare_request_params
129. raise RequestException('[' + THREAD_LOCAL.uuid + ']request sign failed. ' + str(e))

Exception Type: RequestException at /siteapi/oneid/config/
Exception Value: [38f6aa1c-52df-11ea-ab4d-0242ac120005]request sign failed. No PEM start marker "b'-----BEGIN RSA PRIVATE KEY-----'" found
Request information:
USER: User: admin(admin)

GET: No GET data

POST: No POST data

FILES: No FILES data

COOKIES: No cookie data

META:
CONTENT_LENGTH = '590'
CONTENT_TYPE = 'application/json;charset=UTF-8'
HTTP_ACCEPT = 'application/json, text/plain, /'
HTTP_ACCEPT_ENCODING = 'gzip, deflate'
HTTP_ACCEPT_LANGUAGE = 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7'
HTTP_ARKER = 'portal'
HTTP_AUTHORIZATION = 'token 0e347bc0481a68f02a5904a9d84269523e93d6b6'
HTTP_CACHE_CONTROL = 'no-cache'
HTTP_CONNECTION = 'close'
HTTP_DNT = '1'
HTTP_HOST = '10.10.10.76'
HTTP_ORIGIN = 'http://10.10.10.76'
HTTP_PRAGMA = 'no-cache'
HTTP_REFERER = 'http://10.10.10.76/'
HTTP_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
HTTP_X_FORWARDED_FOR = '192.168.111.58'
HTTP_X_REAL_IP = '192.168.111.58'
HTTP_X_REQUESTED_WITH = 'XMLHttpRequest'
PATH_INFO = '/siteapi/oneid/config/'
QUERY_STRING = ''
REMOTE_ADDR = '172.18.0.6'
REMOTE_PORT = '4787'
REQUEST_METHOD = 'PATCH'
REQUEST_URI = '/siteapi/oneid/config/'
SCRIPT_NAME = ''
SERVER_NAME = '2d6a6ee0ba35'
SERVER_PORT = '80'
SERVER_PROTOCOL = 'HTTP/1.0'
UWSGI_ROUTER = 'http'
uwsgi.node = b'2d6a6ee0ba35'
uwsgi.version = b'2.0.18'
wsgi.errors = <_io.TextIOWrapper name=2 mode='w' encoding='UTF-8'>
wsgi.file_wrapper = ''
wsgi.input = <uwsgi._Input object at 0x7fde1ff69f90>
wsgi.multiprocess = True
wsgi.multithread = False
wsgi.run_once = False
wsgi.url_scheme = 'http'
wsgi.version = '(1, 0)'

Settings:
Using settings module oneid.settings
ABSOLUTE_URL_OVERRIDES = {}
ACTIVE_USER_DATA_LIFEDAY = 30
ACTIVE_USER_REDIS_KEY_PREFIX = ''
ADMINS = []
ALLOWED_HOSTS = ['*']
APPEND_SLASH = True
AUTHENTICATION_BACKENDS = "('rules.permissions.ObjectPermissionBackend', 'django.contrib.auth.backends.ModelBackend', 'oneid.auth_backend.OneIDBasicAuthBackend')"
AUTH_PASSWORD_VALIDATORS = '
'
AUTH_USER_MODEL = 'auth.User'
BASE_DIR = '/var/oneid'
BASE_URL = 'http://arkid.mockuai.net'
CACHES = {'default': {'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://arkid-redis:6379/0', 'TIMEOUT': 259200, 'OPTIONS': {'MAX_ENTRIES': None, 'CLIENT_CLASS': 'django_redis.client.DefaultClient'}}}
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_KEY_PREFIX = ''
CACHE_MIDDLEWARE_SECONDS = 600
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
CELERY_BROKER_URL = 'redis://arkid-redis:6379/0'
CELERY_RESULT_BACKEND = 'django-db'
CELERY_TASK_DEFAULT_QUEUE = 'default'
CELERY_TASK_QUEUES = [<unbound Queue default -> <unbound Exchange default(direct)> -> default>, <unbound Queue perm -> <unbound Exchange perm(direct)> -> perm>, <unbound Queue dept -> <unbound Exchange dept(direct)> -> dept>, <unbound Queue group -> <unbound Exchange group(direct)> -> group>, <unbound Queue sql_ldap -> <unbound Exchange sql_ldap(direct)> -> sql_ldap>]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = "('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')"
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = "('*',)"
CREDIBLE_ARKERS = ['oneid_broker', 'arkbe_broker', 'noah', 'wfe', 'msghub', 'oauth']
CSRF_COOKIE_AGE = 31449600
CSRF_COOKIE_DOMAIN = None
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_PATH = '/'
CSRF_COOKIE_SECURE = False
CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure'
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_TRUSTED_ORIGINS = []
CSRF_USE_SESSIONS = False
DATABASES = {'default': {'ENGINE': 'django.db.backends.mysql', 'NAME': 'arkid', 'USER': 'root', 'PASSWORD': '
', 'HOST': 'arkid-db', 'PORT': '3306', 'OPTIONS': {'autocommit': True, 'init_command': 'SET default_storage_engine=MyISAM'}, 'ATOMIC_REQUESTS': False, 'AUTOCOMMIT': True, 'CONN_MAX_AGE': 0, 'TIME_ZONE': None, 'TEST': {'CHARSET': None, 'COLLATION': None, 'NAME': None, 'MIRROR': None}}}
DATABASE_ROUTERS = []
DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
DATETIME_FORMAT = 'N j, Y, P'
DATETIME_INPUT_FORMATS = ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M', '%Y-%m-%d', '%m/%d/%Y %H:%M:%S', '%m/%d/%Y %H:%M:%S.%f', '%m/%d/%Y %H:%M', '%m/%d/%Y', '%m/%d/%y %H:%M:%S', '%m/%d/%y %H:%M:%S.%f', '%m/%d/%y %H:%M', '%m/%d/%y']
DATE_FORMAT = 'N j, Y'
DATE_INPUT_FORMATS = ['%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', '%b %d %Y', '%b %d, %Y', '%d %b %Y', '%d %b, %Y', '%B %d %Y', '%B %d, %Y', '%d %B %Y', '%d %B, %Y']
DEBUG = True
DEBUG_PROPAGATE_EXCEPTIONS = False
DECIMAL_SEPARATOR = '.'
DEFAULT_CHARSET = 'utf-8'
DEFAULT_CONTENT_TYPE = 'text/html'
DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFilter'
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
DEFAULT_FROM_EMAIL = 'webmaster@localhost'
DEFAULT_INDEX_TABLESPACE = ''
DEFAULT_TABLESPACE = ''
DISALLOWED_USER_AGENTS = []
DOWNLOAD_URL = '/static/download'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'localhost'
EMAIL_HOST_PASSWORD = ''
EMAIL_HOST_USER = ''
EMAIL_PORT = 25
EMAIL_SSL_CERTFILE = None
EMAIL_SSL_KEYFILE = '
'
EMAIL_SUBJECT_PREFIX = '[Django] '
EMAIL_TIMEOUT = None
EMAIL_USE_LOCALTIME = False
EMAIL_USE_SSL = False
EMAIL_USE_TLS = False
EXECUTERS = ['executer.RDB.RDBExecuter', 'executer.log.rdb.RDBLogExecuter', 'executer.cache.default.CacheExecuter']
EXECUTER_WIP = False
FE_EMAIL_ACTIVATE_USER_URL = '/oneid#/oneid/activate'
FE_EMAIL_REGISTER_URL = '/oneid#/oneid/signup'
FE_EMAIL_RESET_PWD_URL = '/oneid#/oneid/password'
FE_EMAIL_UPDATE_EMAIL_URL = '/oneid/#/reset_email_callback'
FE_TOKEN_URL = ''
FILE_CHARSET = 'utf-8'
FILE_UPLOAD_DIRECTORY_PERMISSIONS = None
FILE_UPLOAD_HANDLERS = ['django.core.files.uploadhandler.MemoryFileUploadHandler', 'django.core.files.uploadhandler.TemporaryFileUploadHandler']
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440
FILE_UPLOAD_PERMISSIONS = None
FILE_UPLOAD_TEMP_DIR = None
FIRST_DAY_OF_WEEK = 0
FIXTURE_DIRS = []
FORCE_SCRIPT_NAME = None
FORMAT_MODULE_PATH = None
FORM_RENDERER = 'django.forms.renderers.DjangoTemplates'
IGNORABLE_404_URLS = []
INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', 'django_celery_results', 'django_celery_beat', 'corsheaders', 'rest_framework', 'rest_framework.authtoken', 'drf_expiring_authtoken', 'coreapi', 'tasksapp', 'siteadmin', 'siteapi', 'oneid_meta', 'oauth2_provider', 'infrastructure', 'captcha', 'ldap.sql_backend']
INTERNAL_IPS = []
LANGUAGES = "(('en', 'English'), ('zh-hans', '中文简体'), ('zh-hant', '中文繁體'))"
LANGUAGES_BIDI = ['he', 'ar', 'fa', 'ur']
LANGUAGE_CODE = 'zh_hans'
LANGUAGE_COOKIE_AGE = None
LANGUAGE_COOKIE_DOMAIN = None
LANGUAGE_COOKIE_NAME = 'django_language'
LANGUAGE_COOKIE_PATH = '/'
LDAP_BASE = 'dc=example,dc=org'
LDAP_CLUSTER_ADDR = ''
LDAP_DEPT_BASE = 'ou=dept,dc=example,dc=org'
LDAP_GROUP_BASE = 'cn=intra,ou=group,dc=example,dc=org'
LDAP_PASSWORD = '
'
LDAP_SERVER = 'ldap://localhost'
LDAP_USER = 'cn=admin,dc=example,dc=org'
LDAP_USER_BASE = 'ou=people,dc=example,dc=org'
LOCALE_PATHS = ['/var/oneid/locale']
LOGGING = {'version': 1, 'disable_existing_loggers': False, 'handlers': {'console': {'class': 'logging.StreamHandler'}}, 'loggers': {'django': {'handlers': ['console'], 'level': 'INFO'}}}
LOGGING_CONFIG = 'logging.config.dictConfig'
LOGIN_REDIRECT_URL = '/accounts/profile/'
LOGIN_URL = '/#/oneid/login'
LOGOUT_REDIRECT_URL = None
MANAGERS = []
MEDIA_ROOT = ''
MEDIA_URL = ''
MESSAGE_STORAGE = 'django.contrib.messages.storage.fallback.FallbackStorage'
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'common.django.middleware.CrequestMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware']
MIGRATION_MODULES = {}
MINIO_ACCESS_KEY = ''
MINIO_BUCKET = 'oneid'
MINIO_ENDPOINT = 'localhost:9000'
MINIO_LOCATION = 'us-east-1'
MINIO_SECRET_KEY = '
'
MINIO_SECURE = True
MONTH_DAY_FORMAT = 'F j'
NUMBER_GROUPING = 0
PASSWORD_ENCRYPTION = ''
PASSWORD_HASHERS = '
'
PASSWORD_RESET_TIMEOUT_DAYS = ''
PREPEND_WWW = False
PRIVATE_IP = '127.0.0.1'
PUBLIC_IP = ''
REDIS_CONFIG = {'HOST': 'arkid-redis', 'PORT': 6379, 'DB': 0, 'PASSWORD': '
'}
REDIS_URL = 'redis://arkid-redis:6379/0'
REST_FRAMEWORK = {'DEFAULT_AUTHENTICATION_CLASSES': ('oneid.authentication.CustomExpiringTokenAuthentication', 'oneid.authentication.HeaderArkerBaseAuthentication'), 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated', 'oneid.permissions.IsAdminUser')}
ROOT_URLCONF = 'oneid.urls'
SECRET_KEY = '********************'
SECURE_BROWSER_XSS_FILTER = False
SECURE_CONTENT_TYPE_NOSNIFF = False
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
SECURE_HSTS_PRELOAD = False
SECURE_HSTS_SECONDS = 0
SECURE_PROXY_SSL_HEADER = None
SECURE_REDIRECT_EXEMPT = []
SECURE_SSL_HOST = None
SECURE_SSL_REDIRECT = False
SERVER_EMAIL = 'root@localhost'
SESSION_CACHE_ALIAS = 'default'
SESSION_COOKIE_AGE = 1209600
SESSION_COOKIE_DOMAIN = None
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_NAME = 'sessionid'
SESSION_COOKIE_PATH = '/'
SESSION_COOKIE_SECURE = False
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_FILE_PATH = None
SESSION_SAVE_EVERY_REQUEST = False
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
SETTINGS_MODULE = 'oneid.settings'
SHORT_DATETIME_FORMAT = 'm/d/Y P'
SHORT_DATE_FORMAT = 'm/d/Y'
SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
SILENCED_SYSTEM_CHECKS = []
SITE_ID = 1
SITE_META = 'native'
SMS_LIFESPAN = datetime.timedelta(0, 120)
STATICFILES_DIRS = ['/var/oneid/static']
STATICFILES_FINDERS = ['django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder']
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
STATIC_ROOT = '/var/oneid/staticfiles'
STATIC_URL = '/static/'
TEMPLATES = [{'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['/var/oneid/infrastructure/templates'], 'APP_DIRS': True, 'OPTIONS': {'context_processors': ['django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages']}}]
TEMPLATE_CONTEXT_PROCESSORS = ['django.core.context_processors.i18n']
TESTING = False
TEST_NON_SERIALIZED_APPS = []
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
THOUSAND_SEPARATOR = ','
TIME_FORMAT = 'P'
TIME_INPUT_FORMATS = ['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
TIME_ZONE = 'Asia/Shanghai'
UPLOADFILES_PATH = '/var/oneid/upload/'
UPLOAD_DIR = '/var/oneid/static/upload'
USE_ETAGS = False
USE_I18N = True
USE_L10N = True
USE_THOUSAND_SEPARATOR = False
USE_TZ = True
USE_X_FORWARDED_HOST = False
USE_X_FORWARDED_PORT = False
WSGI_APPLICATION = 'oneid.wsgi.application'
X_FRAME_OPTIONS = 'SAMEORIGIN'
YEAR_MONTH_FORMAT = 'F Y'

You're seeing this error because you have DEBUG = True in your
Django settings file. Change that to False, and Django will
display a standard page generated by the handler for this status code.

点击开始同步,返回的result是:

{"result":"{"exc_type": "ImportError", "exc_message": ["cannot import name 'DINGDING_APP_VERSION'"], "exc_module": "builtins"}","status":3,"status_raw":"FAILURE"}

希望能得到解答,谢谢

配置邮箱失败

配置邮箱提示失败
控制台显示...看参数就安全不对呀.
request

{
access_key: "[email protected]"
access_secret: "xxxx"
host: "smtp.qiye.aliyun.com"
port: 465
}

response

{"email":["invalid"]}

屏幕快照 2019-09-25 19.08.09.png

来自QQ群,经典对白的问题

root@company:/data/k8s/office/arkid-charts/chart# helm install . --generate-name
Error: unable to build kubernetes objects from release manifest: unable to recognize "": no matches for kind "Certificate" in version "certmanager.k8s.io/v1alpha1"

请问 这种是什么原因 我看values里边没有生命类似的资源

image

[Proposal] V2

Goal

TBD

Feature List

Zero-trusted Network

参考文档
  1. https://storage.googleapis.com/pub-tools-public-publication-data/pdf/43231.pdf
  2. https://storage.googleapis.com/pub-tools-public-publication-data/pdf/44860.pdf
  3. https://storage.googleapis.com/pub-tools-public-publication-data/pdf/45728.pdf
  4. https://storage.googleapis.com/pub-tools-public-publication-data/pdf/f29b3e764b1122d508b7b53544a3bbadd6cd1101.pdf
  5. https://storage.googleapis.com/pub-tools-public-publication-data/pdf/c8da594124dab1f91e6750995e2b7805403b19f1.pdf
  6. https://storage.googleapis.com/pub-tools-public-publication-data/pdf/b9b4a09a913e410b7c45f3fbacec4d350e38146f.pdf

New Implementation of the Permission Controlling Model

  • ACL
  • (H)RBAC
    image
Real-time permissions calculation

实时计算权限接口完全取代定时刷新权限异步任务,权限存储结构需重构
更换算法及数据结构,提高性能的同时降低数据记录的增长速度

New Operation Mode: Proxy Mode

典型场景,如期望的用户数据存储在其他系统中,如微软的本地AD或者Azure AD或者自有的LDAP中,
仅仅将ArkID以Proxy的模式启用,以便使用ArkID的更多特性,如WEB Based Management Panel, OAuth2 Server等等, 如下图所示:

Client <-> ArkID(in Proxy Mode) <-> Upstream idP

OpenAPI

使用openapi-generator生成arkid的多语言SDK
https://github.com/OpenAPITools/openapi-generator

Social Login

  • Google
  • Github
  • 钉钉
  • 微信
  • 企业微信
  • Twitter
  • ...

User Inventory Provider

支持桥接各种用户数据源,如关系型数据库等。典型的场景为企业拥有自己的账号数据源,但是很难迁移到ArkID的用户模型中,用户可以实现自己的Service Provider接入ArkID,而不破坏原有的企业内部IT架构。

注意与Proxy Mode的区别。

plugin mode

允许arkid以app的模式加入到django项目中,同时保证与arkid有关的中间件如redis(cache)的启停可配置。
保证插件模式下的arkid无需配置即可使用

MFA/2FA

支持二步验证: 短信、邮箱,支持TOTP协议(RFC6238)

组件拆分

为了更好的服务各种使用场景,需要将组件按功能做进一步合理拆分。
TBD

Protocol支持列表

  • LDAP
  • OAuth2
  • OpenID Connect
  • SAML2
  • CAS Web
  • Radius?

Authentication authorization extension

提高arkid的可扩展性,用户有各种认证授权需求的,例如wifi认证接入等
用户可将自开发的认证授权流程接入arkid扩展模块,使arkid能够灵活支持用户自定义的授权协议

Flow Hooks

支持常见的Hook扩展定义能力,如: user.login.after: xxx, user.logout.after: xxx and etc.

数据校验、数据绑定节点

TBD

Impersonate

TBD

Statistics

基于日志系统(log4j),支持统计分析,如应用级别,接口级别,用户级别等

Design Documents

  1. V2 API

钉钉导入时, 如果员工名字过长, 会导入失败

版本 : any

在执行钉钉同步时, 如果钉钉用户名长度超过 16 位时, 会导入失败.
经排查, 发现 siteapi/v1/serializers/utils.py 中的 username_valid(username)校验的 regex 最多位数为 16 位.
建议提高

Stack Overflow 上面有对英文名字长度相关的讨论 link , 可以拿来做个参考

[建议]管理员添加用户的流程

目前管理员添加用户没有设置密码的选项,添加完账号还不能使用,必须让用户邀请激活,前提还必须配置好邮箱/手机进行验证。
感觉这个流程太复杂了吧,我只想简单的使用,并不想配置邮箱(邮箱服务器尝试配置一直提示失败)和电话..
建议管理员添加用户,直接设置密码,用户就可以直接进行登录了

迁移的时候报错

v1.3.9 版本下手工部署:进行迁移,说是找不到 system.Users 模型类,报错如下:

Operations to perform:
Apply all migrations: admin, auth, authtoken, captcha, contenttypes, corsheaders, django_celery_beat, django_celery_results, drf_expiring_authtoken, oauth2_provider, oneid_meta, sessions, siteadmin, sites, tasksapp
Running migrations:
Applying authtoken.0003_auto_20201019_1524...Traceback (most recent call last):
File "manage.py", line 13, in
execute_from_command_line(sys.argv)
File "/usr/local/lib64/python3.6/site-packages/django/core/management/init.py", line 381, in execute_from_command_line
utility.execute()
File "/usr/local/lib64/python3.6/site-packages/django/core/management/init.py", line 375, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib64/python3.6/site-packages/django/core/management/base.py", line 323, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib64/python3.6/site-packages/django/core/management/base.py", line 364, in execute
output = self.handle(*args, **options)
File "/usr/local/lib64/python3.6/site-packages/django/core/management/base.py", line 83, in wrapped
res = handle_func(*args, **kwargs)
File "/usr/local/lib64/python3.6/site-packages/django/core/management/commands/migrate.py", line 234, in handle
fake_initial=fake_initial,
File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/executor.py", line 117, in migrate
state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/executor.py", line 147, in _migrate_all_forwards
state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/executor.py", line 245, in apply_migration
state = migration.apply(state, schema_editor)
File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/migration.py", line 124, in apply
operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/operations/fields.py", line 249, in database_forwards
schema_editor.alter_field(from_model, from_field, to_field)
File "/usr/local/lib64/python3.6/site-packages/django/db/backends/sqlite3/schema.py", line 137, in alter_field
super().alter_field(model, old_field, new_field, strict=strict)
File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/schema.py", line 507, in alter_field
new_db_params = new_field.db_parameters(connection=self.connection)
File "/usr/local/lib64/python3.6/site-packages/django/db/models/fields/related.py", line 966, in db_parameters
return {"type": self.db_type(connection), "check": self.db_check(connection)}
File "/usr/local/lib64/python3.6/site-packages/django/db/models/fields/related.py", line 963, in db_type
return self.target_field.rel_db_type(connection=connection)
File "/usr/local/lib64/python3.6/site-packages/django/db/models/fields/related.py", line 878, in target_field
return self.foreign_related_fields[0]
File "/usr/local/lib64/python3.6/site-packages/django/db/models/fields/related.py", line 632, in foreign_related_fields
return tuple(rhs_field for lhs_field, rhs_field in self.related_fields if rhs_field)
File "/usr/local/lib64/python3.6/site-packages/django/db/models/fields/related.py", line 619, in related_fields
self._related_fields = self.resolve_related_fields()
File "/usr/local/lib64/python3.6/site-packages/django/db/models/fields/related.py", line 604, in resolve_related_fields
raise ValueError('Related model %r cannot be resolved' % self.remote_field.model)
ValueError: Related model 'system.Users' cannot be resolved

数据迁移脚本(v1 -> v2)

v2是一个完全重写的版本,数据结构与旧版本不兼容

需要额外提供一个脚本支持将数据从老的版本迁移到v2上

同步钉钉账号功能无法保存配置

账号管理-->账号同步-->钉钉账号,填入4个配置项后,点“保存配置”
/siteapi/oneid/config/ 400 Bad Request
请问是姿势不对,还是有bug?

注册账号类型设置 无法关闭

已经关闭了开放注册,为什么还要设置注册账号类型设置,如果没有邮箱和短信配置,就无法保存钉钉扫码登录,这个逻辑是不对的呀.

安装时踩过的坑

Helm 安装

  1. PV 需要自行准备, chart里面只建立了PVC
  2. 有 web-server 的那个 pod, 在启动的时候会运行python manage.py migrate , 然而很容易初始化失败. 失败的同学可以将 pod 里面的3306端口映射到本机, 重置arkid数据库, 通过本地开发部署的方式将 pod 里面的数据库初始化 (修改oneid.settings)

本地开发部署

  1. python版本要求为3.6. 3.7会有smtplib的bug, 导致无法设置邮箱注册功能

    python 版本的要求我是在 文档-> Django 插件 -> 要求 才看到的. 为啥放这么后面啊 !

  2. settings_local.py 文件是放在项目的根目录下的

  3. 启动 celery -A celery_app worker -Q default,perm -l debug 的时候报错,

    1. 报错信息中有关于ldap.sql_backend字样的
      这时候将oneid.settings里面的INSTALLED_APPS里, 最后被注释掉的ldap.sql_backend 去掉注释

    2. 再次执行, 报 localhost:6379 端口无法连接的错.
      这个时候本地起一个redis就行

配置时的坑

  1. 钉钉扫码登录问题
    配置钉钉扫码登陆时, 要输入 appid 和 appsecret. 然而这里不能填我们创建微应用时候的 id 和 secret. 我们需要在 钉钉开发者平台 -> 移动接入应用 -> 登录来创建一个新的应用授权

  2. (Fe v1.3.2)管理后台 -> 账户管理 -> 账号配置 中, 就算选择没有开放注册, 也需要至少配置好一个注册方式才能保存当前页的配置

  3. (BE v1.3.7) 1.3.7 增强了对国际号码的支持, 但是首次使用时需要管理员调用接口添加地区号码规则, 参考文档国际号码接入部分

  4. 国际手机号码用户无法接收验证码问题
    在配置短信服务商的时候, UI 界面的短信模板短线落款都是国内短信的, 国际短信模板和落款需要手动调接口补充

调试时的坑

  1. API 文档里展示的请求例子里面的路径是不正确的. 如 https://private-anon-3e4c044fcb-oneid1.apiary-mock.com/config/i18n_mobile/. 本地调试的时候请加上/siteapi/v1/, 如127.0.0.1:8000/siteapi/v1/config/i18n_mobile/
  2. 大部分请求是需要带 token 的. 如果使用 postman 调试的话, 请不要用 postmanauthorization. Postman 中的bearer token生成的 headerAuthorization: bearer xxxxxxxxxxxxx. 请另外按照要求手填 header

前后端部署完后,登录报错

arkid-core\siteapi\v1\serializers\app.py

saml_app = SAMLAPPSerializer(many=False, required=False)
改为
saml_app = SAMLAPPSerializer(many=False, required=False, allow_null=True)
解决 saml_app 为空报错问题

LDAP配置界面不显示,该怎么处理

LDAP配置界面不显示,该怎么处理,找不到文档

# 容器状态
    Name                  Command               State                Ports             
---------------------------------------------------------------------------------------
arkid-be       /bin/sh -c python manage.p ...   Up      80/tcp                         
arkid-db       docker-entrypoint.sh --cha ...   Up      3306/tcp, 33060/tcp            
arkid-fe       /docker-entrypoint.sh /bin ...   Up      80/tcp                         
arkid-ldap     /bin/sh -c start                 Up      0.0.0.0:10389->389/tcp, 636/tcp
arkid-portal   /docker-entrypoint.sh ngin ...   Up      0.0.0.0:8989->80/tcp           
arkid-redis    docker-entrypoint.sh redis ...   Up      6379/tcp
# OpenLDAP日志
Starting OpenLDAP
+ ulimit -Sn 10000
+ echo 'Starting OpenLDAP'
+ exec slapd -h 'ldaps:/// ldap:/// ldapi:///' -d -256 -f /etc/openldap/slapd.conf -u ldap -g ldap
600fac35 @(#) $OpenLDAP: slapd 2.4.44 (Jan 29 2019 17:42:45) $
	[email protected]:/builddir/build/BUILD/openldap-2.4.44/openldap-2.4.44/servers/slapd
TLSMC: MozNSS compatibility interception begins.
tlsmc_convert: INFO: cannot open the NSS DB, expecting PEM configuration is present.
tlsmc_intercept_initialization: INFO: successfully intercepted TLS initialization. Continuing with OpenSSL only.
TLSMC: MozNSS compatibility interception ends.
600fac35 slapd starting

前端不显示LDAP配置项
image

docker部署arkid后,项目成功启动,其他功能没发现问题,但是无法保存登录界面设置

亲爱的arkid开发者们,你们好,近日我使用docker部署了arkid项目,项目成功启动,并可在前端完成基本功能。但是我发现我无法保存登陆界面设置,即使设置后,右边的预览图片也会改变,但是点击保存后保存按钮动画播放,完毕,没有出现toast,切换标签页时会丢失配置。我观察了后端打印,发现是有接收到命令的,打印如下:
image
无论是图片,标题还是其他,都无法保存

同步钉钉数据失败

无论是使用docker-compose还是手动部署,都无法同步钉钉数据,在请求账号同步-钉钉账号-保存配置接口时就会报400错误,后端日志如下:

Bad Request: /siteapi/oneid/config/
"PATCH /siteapi/oneid/config/ HTTP/1.1" 400 37

login接口的token没返回过期时间

现在arkid在返回token的时候,没有携带token的有效期,感觉用http直接请求login接口,会导致在之后的鉴权活动要反复请求arkid来验证token是否有效,这个效率是不是不行?是不是应该在返回token的时候附带上有效期,在第三方应用自己的API就可以用这个有效期去验证token是否有效就好了?

建议增加base_dn可以自定义

当前base_dn为example.org,不能更改,所以公司原有系统已用ldap的话,很难保留原有用户的同时切换至一账通的ldap。所以建议增加base_dn可以自定义。

面向性能优化的代码重构

代码审计和提案

1 原理

一次后端调用从端口经过网络服务到后端处理程序到返回响应结果,完成服务。目前django框架通过在部署过程中采用轻量级异步协议的方式实现了类异步服务,比较好的解决并发性能方面的问题。

通常一个后端服务至少有一个以上的持久层。无论web框架采用何种架构,最终存储会通过持久层进入数据库。由于数据库的io特性和事务特性,需要采用队列方式将数据排队存取,并且保持对所有连接的一致性。持久化方案往往成为性能重点瓶颈和设计的焦点。

这里我们的优化方案主要基于测量接口调用的时间和io关键指标,即以调用一个接口的完整周期和数据库因素作为测度标准,来衡量我们应该采用何种技术和方案,解决性能问题,和衡量我们大概能在什么样的程度上解决问题。

另外我们会基于可复用性来分析微服务方案。

2 测试技术

操作计数

对一次调用中的数据库操作进行计数可以简单明了的说明这次调用进行了多少次数据库操作。通常我们希望操作越少越好。其它涉及io操作的代码也可以采用这种方式,不过这里我们主要就是计量数据库。

profile

gdb为开发者提供调试和性能工具,获取代码在运行时的度量和截面状态, python 同样提供了pdb, cProfile, pstats 等系列统计和调试工具。通过中间件技术和配置,我们可以在框架中插入profiling功能,来为开发提供更加精确的参考。

集成测试(Integration Test)

对接口做集成测试,通过一些自动化脚本调用每个接口,跟profile结合,可以获得服务的整体性能。

3 组织结构

完全同构

在业务升级过程中必要保持向下兼容和依赖性时,通常会采用完全同构方法,对代码结构和数据表保持一致,在代码层优化或者重新构成。

django orm 数据表关系和用法

class UserPerm(OwnerPerm):
    owner = models.ForeignKey('oneid_meta.User', on_delete=models.CASCADE)

这条语句作为外键,给UserPerm创建一个owner属性映射,同时会为User对象创建一个默认反向引用。

u = User.objects.get(pk=pk)
u.userperm_set.all()

可以省掉这样的写法。

    @property
    def perms(self):
        '''
        所有权限
        '''
        return UserPerm.valid_objects.filter(owner=self)

也可以在ForeignKey里显式给予命名。

class UserPerm(OwnerPerm):
    owner = models.ForeignKey('oneid_meta.User', related_name='perms', on_delete=models.CASCADE)

...
# 可以通过related_name 获取对反向关系的引用。
u = User.objects.get(pk=pk)
perms = u.perms.all()
...

每一个引用Field(OneToOne, ForeignKey, ManyToMany) 都有一个相应的反引用字段。

相同接口

保持相同接口通常也是一种,成本较小的方案,这种方案无需重写前端。

异构重构

如果业务目标调整或者重新升级业务,会从流程上重新梳理,产生新的架构。

代码复用和微服务

微服务在**上被认为是先进的。它为我们提供了可复用性和灵活性。

arkid数据表基本运作

在arkid中引用关系最多的两张表 UserSite
User 表和它关联的权限组是arkid 服务的中心。绝大部分的查询和更改都集中在这几个表的条目。
整体而言,这几个表关系要表达的关系是用户有分组属性和部门属性,这两个属性连同用户本身都有一系列权限,这些权限是可以被继承到用户的。用户权限和其所属的组以及部门权限决定了对于应用和配置的作用。所以,用户、分组、部门和权限的高效查询和分配作为一个核心,是整个server的起点,可以作为一个基础微服务进行考虑。

DeptMember 和 GroupMember 作为多对多关系的一种显式建表方式,这里予以省略。

User -> UserPerm

-> Group -> GroupPerm
-> Dept -> DeptPerm

为了达到高效查询目的,实际上这里应当简化成4张表。将UserPerm, GroupPerm, DeptPerm 统一压缩到Perm表

User -> Perm

-> Group -> Perm
-> Dept -> Perm

class Perm(models.Model):
    name = models.CharField()

class Group(models.Model):
    label = models.CharField()
    group_perms = models.ManyToManyField(Perm, related_name='groups')

class Dept(models.Model):
    label = models.CharField()
    dept_perms = models.ManyToManyField(Perm, related_name='depts')

class User(models.Model):
    user_perms = models.ManyToManyField(Perm, related_name='users')
    groups = models.ManyToManyField(Group, related_name='users')
    depts = models.ManyToManyField(Dept, related_name='users')

    @property
    def all_perms(self):
        query = Q(users__in=[self]) | Q(groups__in=self.groups.all()) | Q(depts__in=self.depts.all())
        return Perm.objects.filter(query)   

系统原有设计依然不失为一种可选方案,当多对多关系存储量较大时,创建中间表可以避免读取基础数据时额外查询外键的开销。

class Perm(models.Model):
    name = models.CharField()

class Group(models.Model):
   label = models.CharField()

class User(models.Model):
    username = models.CharField()
    
    @property
    def all_perms(self):
        query = Q(user_relation__user=self) | Q(group_relation__group_id__in=self.group_relation.values_list('group', flat=True))
        return Perm.objects.filter(query)

class UserGroup(models.Model):
    user = models.ForeignKey(User, related_name='group_relation')
    group = models.ForeignKey(Group, related_name='user_relation')


class UserPerm(models.Model):
    perm = models.ForeignKey(Perm, related_name='user_relation')
    user = models.ForeignKey(User, related_name='perm_relation')

class GroupPerm(models.Model):
    perm = models.ForeignKey(Perm, related_name='group_relation')
    group = models.ForeignKey(Group, related_name='perm_relation')

可以一次性获取该用户所有权限,分组重新分配以后,结果也会跟着变化。实际上,在数据库查询中进行函数式编程考量依然是重要的。从查询的角度消除副作用,关注输入和输出的一致性将会显著的影响服务性能。

表字段、查询关系和应用算法设计

对于下面的特性,也要设法将其设计到表字段中,让权限可以通过一次组合查询获取到全部结果。配合数据库的缓存机制,可以做到非常高效。在更新表的时候增加字段复杂性和维护字段统一性的难度增加开销很小。在每次查询获取结果的过程当中迭代,开销将会大到难以忍受。

        (1, '所有人可见'),
        (2, '节点成员可见'),
        (3, '节点成员及其下属节点均可见'),
        (4, '只对指定人、节点可见'),
        (5, '所有人不可见'),

arkid 数据表关系概要

其他微服务按照具体应用分解为:

表依赖 Model\Prefix Github Ding Wechat Alipay Minio QQ
Site {pref}Config
{pref}App
User {pref}User
微服务化之后的主要app列表
  • oneid (系统)
  • base (基本用户和权限)
  • oauth2_provider
  • github
  • qq
  • wechat
  • alipay
  • ding
    ...
    后面的子app大致都共同依赖于 base。除此以外,各app之间相互基本独立,可装卸。

每个应用大致的构成是相同的: models, serializer, views, urls, 以及相关的utils 和 tasks。当我们分配好了应用的全部要素以后。每个子app 可以被挂载或者卸载。

4 方法

通常后端service在开发早期的性能问题是由于查询代码性能导致的,这时一般是单库单server。通过db优化和流程优化和代码优化可以将单服务器和数据库性能优化到最佳性能。性能合适的数据库和配置可以帮助服务支撑用户和数据快速增长。随着用户增长到服务性能极限,可以通过提升配置来大幅提升服务的响应能力。现在云服务一般会给应用商提供简化的saas,通过一些简单架构方法可以提供进一步性能支撑。这里我们聚焦于早期优化。

基于查询计数

重复操作和不当操作是优化的首要目标。

批量操作

案例:

        for plugin_path in plugin_paths:
            CrontabPlugin.valid_objects.create(
                name=plugin_path,
                import_path=plugin_path,
                is_active=False,
            )

这里的核心优化点在创建语句。假设plugin_paths有10个元素,create语句将导致10次写数据库。io过多将导致数据库锁死。

  1. 将单条创建转化成批量创建语句,显著降低io。
  2. 将循环转换成列表处理,可以少许改进速度。

解决方案:

plugins = [CrontabPlugin(name=plugin_path, import_path=plugin_path, is_active=False) for plugin_path in plugin_paths ]
queryset = CrontabPlugin.valid_objects.bulk_create(plugins)

通过bulk_create将写数据库从10次减少为1次。
由于python的动态语言特性,使用for语句时每次迭代会动态创建一个Pyobject,列表语句中的迭代是内嵌在cpython里面的,所以效率会高。

案例:

def check_perm_exists():
    '''
    为每个对象:user,gruop,dept创建perm判定结果
    '''
    for perm in Perm.valid_objects.all():
        for user in User.valid_objects.all():
            if not UserPerm.valid_objects.filter(owner=user, perm=perm).exists():
                UserPerm.valid_objects.create(
                    owner=user,
                    perm=perm,
                    dept_perm_value=False,
                    group_perm_value=False,
                    status=0,
                    value=perm.default_value,
                )
        for group in Group.valid_objects.all():
            if not GroupPerm.valid_objects.filter(owner=group, perm=perm).exists():
                GroupPerm.valid_objects.create(
                    owner=group,
                    perm=perm,
                    status=0,
                    value=perm.default_value,
                )
        for dept in Dept.valid_objects.all():
            if not DeptPerm.valid_objects.filter(owner=dept, perm=perm).exists():
                DeptPerm.valid_objects.create(
                    owner=dept,
                    perm=perm,
                    status=0,
                    value=perm.default_value,
                )

这里涉及到Perm(权限),User(用户),UserPerm(用户权限),Dept(部门),DeptPerm(部门权限),Group(分组), GroupPerm(分组权限)7张表的查询和更新。
从同构的角度来进行优化的话,应设法优化掉两次迭代带来的 O _n2 复杂度的数据库写操作。

由于unique_together属性存在,实际上我们并不需要逐个检测是否存在,可以批量创建。这里valid_manager 会过滤掉is_del=False的条目,所以不应采用。
解决方案:

perms = Perm.objects.values('id', 'default_value')

users = User.objects.values_list('id', flat=True)
groups = Group.objects.values_list('id', flat=True)
departments = Dept.objects.values_list('id', flat=True)

user_perms = [ 
    UserPerm(owner_id=user, perm_id=perm['id'], status=0, value=perm['default_value'])
    for user in users for perm in perms
]
UserPerm.objects.bulk_create(user_perms,  ignore_conflicts=True)
...

这样我们可以将流程优化为七次数据库操作。

这里通常权限,分组和部门是常量级的表,所以一次全量查询带来的开销是有限的,可以看作是分页级别的开销。
考虑到用户的线性增长,假设存在10种权限。当用户增长到1000时。一次User查询将返回1000条数据,UserPerm写操作会产生10,000条写入。这里的效率依然是不可接受的(不过作为定时任务,避开波峰时期进行这样的操作,也可以说得过去)。严谨而论,有必要在流程上进一步分析,进行某种更加优化的设计。忽略无需更新的条目,或者以空间来换取时间,将全量操作优化为查询和少量更新,达到高效操作的目的。

基于流程

通用操作拆分

注意到LOG_CLI在server中的广泛存在,目前用户审计和操作日志在系统中扮演着重要的角色。对于权限检验,操作日志这样多个接口中存在的低耦合非事务性代码,可以通过拆分的方式将其进行改造,从而在整体上使server性能获得良好的提升。

  1. 分表分库。 作为一个可选项,日志模块可以设置为单独的数据库。
class RequestAccessLog(models.Model):
    '''
    请求基本信息
    '''
    ip = models.CharField(max_length=64, blank=True, null=True, default='', verbose_name='IP地址')
    agent = models.CharField(max_length=512, blank=True, null=True, default='', verbose_name='HTTP_USER_AGENT')
    url = models.TextField(blank=True, null=True, default='', verbose_name='完整请求路径')
    method = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name='REQUEST_METHOD')

class RequestDataClientLog(models.Model):
    '''
    请求内容,定期删除
    '''
    content = models.TextField(blank=True, null=True, default='', verbose_name='请求内容')
    content_type = models.TextField(blank=True, null=True, default='', verbose_name='内容类型')

class Log(models.Model):
    '''
    操作日志,会呈现给用户(管理员) 永久保存
    '''
    uuid = models.UUIDField(default=uuid.uuid4, editable=True, unique=True)
    created = models.DateTimeField(auto_now_add=True, blank=True, null=True, verbose_name='创建时间')
    user_pk = models.IntegerField( blank=True, null=True, verbose_name='操作者')
    username = models.CharField(max_length=128, blank=True, null=True, verbose_name='操作者名称')
    subject = models.CharField(max_length=128, default='', blank=False, null=False, verbose_name='类型')
    summary = models.CharField(max_length=512, default='', blank=False, null=False, verbose_name='事件信息')
    access = models.ForeignKey(RequestAccessLog, blank=True, null=True, on_delete=models.SET_NULL)
    data = models.ForeignKey(RequestDataClientLog, blank=True, null=True, on_delete=models.SET_NULL)

解除外键不仅可以使代码具有更好的独立性和可隔离性,还可以避免在orm中由于引用外键属性带来额外查询问题。

  1. 将日志异步化。
    tasksapp.tasks.py 定义
@shared_task
def log(self, subject, summary, user_pk, username):
    Log.objects.create(    # pylint: disable=no-member
            user_pk=user_pk,
            username=username,
            subject=subject,
            summary=summary,
            access=self.access_log,
            data=self.data_log,
        )

RDBLogExecuter

class RDBLogExecuter(Executer):
    ...
    def create_user(self, user_info):
        '''
        创建用户
        :param dict user_info:
        '''
        subject = 'user_create'
        summary = f'{self.cli.user.log_name} 创建新用户 {self.cli.res.log_name}'

        if self.cli.request:
            url_name = resolve(self.cli.request.path_info).url_name
            if url_name == 'user_register':
                subject = 'ucenter_register'
                summary = f'用户注册: {self.cli.user.log_name}'
        user_pk = self.cli.user.id
        username = self.cli.user.username
        log.apply_async(subject, summary, user_pk, username)

实际执行代码

    def perform_create(self, serializer):
        user = serializer.save()
        LOG_CLI(serializer.instance).create_user()
        return user

我们将两个调用数据库多次的函数拆解成了异步。由于无需等待LOG_CLI数据库操作返回结果,响应速度得到提升。配合步骤1,对一个数据库的两次操作分解为对不同数据库的操作。

基于代码运行

基于集群组织

代码复用和微服务 中我们提供了服务内聚的解决方案。

通过kubernates和docker这样的集群服务我们可以将server层并行部署。

层次 节点
web应用 浏览器
server api服务 和 静态页面服务
db 数据库

由于各微服务对UserSite外键的引用,数据库是单库形式存在的。在server中,各服务依赖于base,所以也是聚合存在的。尽管可以并行部署多server节点,本质上还是单库单服务。

所以将各服务与user和site解耦,是分离多数据库和多服务的基础。各个服务独有的perm权限也可以拆分到这个服务的内部。这样在部署的时候,可以在服务层次上实现垂直聚合。

层次 节点\应用服务 web base github
server 服务器 vue静态页面 base github
db 数据库 base github

server就可以拆分为多套代码单独部署,每一个server配置专有数据库。对于base server和base库,由于它承载各服务的查询,这样负载最重的服务可以选用高配置,其他负载低的服务可以弹性增长或者按量配置。由于服务性能特点不同,对数据库和服务器的性能要求也不同。这种方案的话可以实现灵活和动态均衡。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.