├── .env ├── .gitignore ├── LICENSE ├── README.md ├── README.rst ├── examples ├── Pipfile ├── demo │ ├── __init__.py │ ├── asgi.py │ ├── settings.mysql.py │ ├── settings.postgres.py │ ├── settings.py │ ├── settings.sqlite3.py │ ├── urls.py │ └── wsgi.py ├── info │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20210909_2310.py │ │ └── __init__.py │ ├── models.py │ ├── serilizer.py │ ├── tests.py │ ├── urls.py │ └── views.py └── manage.py ├── multi_tenant ├── __init__.py ├── const.py ├── local.py ├── proxy │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── rest │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── patch │ │ ├── __init__.py │ │ └── request.py │ └── permissions │ │ └── __init__.py └── tenant │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── backend.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── createtenant.py │ │ └── multimigrate.py │ ├── middleware │ ├── __init__.py │ └── authentication.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models │ ├── __init__.py │ ├── tenant.py │ └── user.py │ ├── patch │ ├── __init__.py │ ├── connection.py │ ├── contenttype.py │ ├── permission.py │ └── user.py │ ├── signal.py │ ├── tests.py │ ├── utils │ ├── __init__.py │ ├── db.py │ └── pycrypt.py │ └── views.py └── setup.py /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/.env -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | .idea/ 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | workspace.xml 81 | # SageMath parsed files 82 | *.sage.py 83 | .idea/workspace.xml 84 | # Environments 85 | # .env 86 | # .venv 87 | # env/ 88 | # venv/ 89 | # ENV/ 90 | # env.bak/ 91 | # venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 独行侠 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DjangoMultiTenant 2 | 3 | django 多租户方案实现方案,本插件的是在数据库层对租户数据进行了隔离,保证每个租户只能访问自己的数据库信息,完整兼容django所有功能 4 | 5 | ## 安装 6 | 7 | ```shell 8 | pip install django-multi-tenancy 9 | ``` 10 | 11 | ## 兼容性 12 | 13 | - django >= 3.2 14 | 15 | 其他django版本未测试,不保证兼容性 16 | 17 | ## 配置 18 | 19 | ```python 20 | 21 | INSTALLED_APPS = [ 22 | 'multi_tenant.tenant', 23 | 'django.contrib.admin', 24 | 'django.contrib.auth', 25 | 'django.contrib.contenttypes', 26 | 'django.contrib.sessions', 27 | 'django.contrib.messages', 28 | 'django.contrib.staticfiles', 29 | ... 30 | 'multi_tenant.proxy', 31 | # rest_framework 需要加载rest app 32 | 'multi_tenant.rest' 33 | ] 34 | 35 | MIDDLEWARE = [ 36 | 'django.middleware.security.SecurityMiddleware', 37 | 'django.contrib.sessions.middleware.SessionMiddleware', 38 | 'corsheaders.middleware.CorsMiddleware', 39 | 'django.middleware.common.CommonMiddleware', 40 | 'django.middleware.csrf.CsrfViewMiddleware', 41 | # 需要注释官方自带的AuthenticationMiddleware,采用插件的MultTenantAuthenticationMiddleware 42 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 43 | 'multi_tenant.tenant.middleware.authentication.MultTenantAuthenticationMiddleware', 44 | 'django.contrib.messages.middleware.MessageMiddleware', 45 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 46 | ] 47 | 48 | # AUTH_USER_MODEL 全局用户模型,必须指定 49 | AUTH_USER_MODEL = 'tenant.GlobalUser' 50 | 51 | # 租户用户模型,不指定默认为:'auth.User' 52 | AUTH_TENANT_USER_MODEL = 'info.User' 53 | 54 | # 租户模型,不指定默认为:'tenant.Tenant' 55 | DEFAULT_TENANT_MODEL = 'tenant.Tenant' 56 | 57 | ## 数据库映射,这里只需要定义共用的app 58 | DATABASE_APPS_MAPPING = { 59 | 'tenant': 'default', 60 | 'admin': 'default', 61 | 'sessions': 'default' 62 | } 63 | 64 | ## 数据库映射路由 65 | DATABASE_ROUTERS = ['multi_tenant.tenant.utils.db.MultTenantDBRouter'] 66 | 67 | ``` 68 | 69 | ## 主要模块说明以及使用 70 | 71 | ### 数据库模块 72 | 73 | 1. 默认`default`数据库为主数据库,只保存公共模块数据,租户数据库可以动态创建,创建一个租户,会自动在数据库中创建了一个对应租户的数据库,所有租户的数据库结构相同 74 | 75 | 2. 可以在`DATABASE_APPS_MAPPING`中指定某个`app`属于公共`app`,还是租户`app`,公共`app`默认数据库链接为`default` 76 | 77 | ### `multi_tenant.tenant` 多租户模块 78 | 79 | 80 | 1. `Tenant`为租户模型,当然你可以继承`AbstractTenant`来自定义自己的租户模块,并在`settings`中指定`DEFAULT_TENANT_MODEL`常量来指定租户模型 81 | 82 | 2. `GlobalUser` 为全局用户,不分数据哪个租户,这里用`GlobalUser`替代了`django.contrib.auth.models.User`模块,因此`django.contrib.auth.get_user_model` 获取的是`GlobalUser`对象,相应的`request.user`也是`GlobalUser`对象,用户可以被加入租户,也可以选择不加入租户,加入租户的用户只能访问相应租户数据,不加入租户的用户如果是超级管理员可以访问`全局用户`和`租户信息` 83 | 84 | 3. 租户用户表默认采用`django.contrib.auth.models.User`,当然你可以选择继承`django.contrib.auth.models.AbstractUser`来自定义自己的租户用户模块,并在settings中指定`AUTH_TENANT_USER_MODEL`常量来指定租户用户,用户可以在租户层面完整的使用`django.contrib.auth`所有功能,包括`User`、`Group`、`Permission`、`Admin` 85 | 86 | 4. 可以登录Admin 后台创建租户,也可以使用`createtenant`命令行来创建租户 87 | 88 | 89 | ### `multi_tenant.proxy` 代理模块 90 | 91 | `ProxyContentType`contentType代理,因为在多租户模型中,主数据库和租户数据库数据模型不一样,在不断的迭代更新中,新的租户和老的租户模型`ContentType`数据信息也不一样,django默认自带的`ContentType`模型默认自带缓存,`ProxyContentType`模型无缓存,每次的数据访问都是直接访问数据库,这样避免了`ContentType`信息不一致导致的异常 92 | 93 | 94 | ### `multi_tenant.rest` rest_framework适配模块 95 | 96 | 1. 对`rest_framework`进行了适配,保证租户只能访问自己的租户的数据 97 | 2. 提供了一个`IsTanenatUser`权限类,判断是不是租户用户 98 | 3. 适配了`rest_framework`的内置权限`IsAdminUser`、`DjangoModelPermissions`、`DjangoModelPermissionsOrAnonReadOnly`、`DjangoObjectPermissions` 99 | 100 | 101 | 102 | ### `migrate` 模块 103 | 104 | 1. 迁移租户数据库,请给`migrate` 指定`--database`参数值, `--database` 105 | 2. 也可以使用`multimigrate`,必须指定`--database`参数值,或者直接使用`--all`,来迁移所有租户表结构 106 | 107 | 108 | 109 | ## 支持的数据库 110 | 111 | 适配了支持`django`所有支持的数据库(`SQLite3`、`MySQL`、`Posgres`、`Oracle`) 112 | 113 | 114 | ## 例子 115 | 116 | 可以参考`examples`的使用 117 | 118 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DjangoMultiTenant 2 | ================= 3 | 4 | django 5 | 多租户方案实现方案,本插件的是在数据库层对租户数据进行了隔离,保证每个租户只能访问自己的数据库信息,完整兼容django所有功能 6 | 7 | 安装 8 | ---- 9 | 10 | .. code:: shell 11 | 12 | pip install django-multi-tenancy 13 | 14 | 兼容性 15 | ------ 16 | 17 | - django >= 3.2 18 | 19 | 其他django版本未测试,不保证兼容性 20 | 21 | 配置 22 | ---- 23 | 24 | .. code:: python 25 | 26 | 27 | INSTALLED_APPS = [ 28 | 'multi_tenant.tenant', 29 | 'django.contrib.admin', 30 | 'django.contrib.auth', 31 | 'django.contrib.contenttypes', 32 | 'django.contrib.sessions', 33 | 'django.contrib.messages', 34 | 'django.contrib.staticfiles', 35 | ... 36 | 'multi_tenant.proxy', 37 | # rest_framework 需要加载rest app 38 | 'multi_tenant.rest' 39 | ] 40 | 41 | MIDDLEWARE = [ 42 | 'django.middleware.security.SecurityMiddleware', 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'corsheaders.middleware.CorsMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | # 需要注释官方自带的AuthenticationMiddleware,采用插件的MultTenantAuthenticationMiddleware 48 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'multi_tenant.tenant.middleware.authentication.MultTenantAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | # AUTH_USER_MODEL 全局用户模型,必须指定 55 | AUTH_USER_MODEL = 'tenant.GlobalUser' 56 | 57 | # 租户用户模型,不指定默认为:'auth.User' 58 | AUTH_TENANT_USER_MODEL = 'info.User' 59 | 60 | # 租户模型,不指定默认为:'tenant.Tenant' 61 | DEFAULT_TENANT_MODEL = 'tenant.Tenant' 62 | 63 | ## 数据库映射,这里只需要定义共用的app 64 | DATABASE_APPS_MAPPING = { 65 | 'tenant': 'default', 66 | 'admin': 'default', 67 | 'sessions': 'default' 68 | } 69 | 70 | ## 数据库映射路由 71 | DATABASE_ROUTERS = ['multi_tenant.tenant.utils.db.MultTenantDBRouter'] 72 | 73 | 主要模块说明以及使用 74 | -------------------- 75 | 76 | 数据库模块 77 | ~~~~~~~~~~ 78 | 79 | 1. 默认\ ``default``\ 数据库为主数据库,只保存公共模块数据,租户数据库可以动态创建,创建一个租户,会自动在数据库中创建了一个对应租户的数据库,所有租户的数据库结构相同 80 | 81 | 2. 可以在\ ``DATABASE_APPS_MAPPING``\ 中指定某个\ ``app``\ 属于公共\ ``app``,还是租户\ ``app``,公共\ ``app``\ 默认数据库链接为\ ``default`` 82 | 83 | ``multi_tenant.tenant`` 多租户模块 84 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | 86 | 1. ``Tenant``\ 为租户模型,当然你可以继承\ ``AbstractTenant``\ 来自定义自己的租户模块,并在\ ``settings``\ 中指定\ ``DEFAULT_TENANT_MODEL``\ 常量来指定租户模型 87 | 88 | 2. ``GlobalUser`` 89 | 为全局用户,不分数据哪个租户,这里用\ ``GlobalUser``\ 替代了\ ``django.contrib.auth.models.User``\ 模块,因此\ ``django.contrib.auth.get_user_model`` 90 | 获取的是\ ``GlobalUser``\ 对象,相应的\ ``request.user``\ 也是\ ``GlobalUser``\ 对象,用户可以被加入租户,也可以选择不加入租户,加入租户的用户只能访问相应租户数据,不加入租户的用户如果是超级管理员可以访问\ ``全局用户``\ 和\ ``租户信息`` 91 | 92 | 3. 租户用户表默认采用\ ``django.contrib.auth.models.User``,当然你可以选择继承\ ``django.contrib.auth.models.AbstractUser``\ 来自定义自己的租户用户模块,并在settings中指定\ ``AUTH_TENANT_USER_MODEL``\ 常量来指定租户用户,用户可以在租户层面完整的使用\ ``django.contrib.auth``\ 所有功能,包括\ ``User``\ 、\ ``Group``\ 、\ ``Permission``\ 、\ ``Admin`` 93 | 94 | 4. 可以登录Admin 95 | 后台创建租户,也可以使用\ ``createtenant``\ 命令行来创建租户 96 | 97 | ``multi_tenant.proxy`` 代理模块 98 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 99 | 100 | ``ProxyContentType``\ contentType代理,因为在多租户模型中,主数据库和租户数据库数据模型不一样,在不断的迭代更新中,新的租户和老的租户模型\ ``ContentType``\ 数据信息也不一样,django默认自带的\ ``ContentType``\ 模型默认自带缓存,\ ``ProxyContentType``\ 模型无缓存,每次的数据访问都是直接访问数据库,这样避免了\ ``ContentType``\ 信息不一致导致的异常 101 | 102 | ``multi_tenant.rest`` rest\_framework适配模块 103 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 104 | 105 | 1. 对\ ``rest_framework``\ 进行了适配,保证租户只能访问自己的租户的数据 106 | 2. 提供了一个\ ``IsTanenatUser``\ 权限类,判断是不是租户用户 107 | 3. 适配了\ ``rest_framework``\ 的内置权限\ ``IsAdminUser``\ 、\ ``DjangoModelPermissions``\ 、\ ``DjangoModelPermissionsOrAnonReadOnly``\ 、\ ``DjangoObjectPermissions`` 108 | 109 | ``migrate`` 模块 110 | ~~~~~~~~~~~~~~~~ 111 | 112 | 1. 迁移租户数据库,请给\ ``migrate`` 指定\ ``--database``\ 参数值, 113 | ``--database`` 114 | 2. 也可以使用\ ``multimigrate``,必须指定\ ``--database``\ 参数值,或者直接使用\ ``--all``,来迁移所有租户表结构 115 | 116 | 支持的数据库 117 | ------------ 118 | 119 | 适配了支持\ ``django``\ 所有支持的数据库(\ ``SQLite3``\ 、\ ``MySQL``\ 、\ ``Posgres``\ 、\ ``Oracle``\ ) 120 | 121 | 例子 122 | ---- 123 | 124 | 可以参考\ ``examples``\ 的使用 -------------------------------------------------------------------------------- /examples/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.douban.com/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "*" 8 | mysqlclient = "*" 9 | django-cors-headers = "*" 10 | django-filter = "*" 11 | coreapi = "*" 12 | djangorestframework = "==3.12.4" 13 | pycryptodome = "==3.10.1" 14 | pdir2 = "*" 15 | psycopg2 = "*" 16 | cx-oracle = "*" 17 | 18 | [dev-packages] 19 | 20 | [requires] 21 | python_version = "3.8" 22 | 23 | [scripts] 24 | dev = "python manage.py runserver 0.0.0.0:8006" 25 | migrate = "python manage.py migrate" 26 | makemigrations = "python manage.py makemigrations" 27 | collect = "python manage.py collectstatic" 28 | server = "gunicorn -c config.py backend.wsgi:application" 29 | -------------------------------------------------------------------------------- /examples/demo/__init__.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | pymysql.install_as_MySQLdb() 3 | -------------------------------------------------------------------------------- /examples/demo/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for demo project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/demo/settings.mysql.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-#sl_@*kvc&=!&dfujo8x0%$m(2)3b#ukrg#yg&ogtf_*8jkj%h' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'multi_tenant.tenant', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'rest_framework', 42 | 'django_filters', 43 | 'corsheaders', 44 | 'info', 45 | 'multi_tenant.proxy' 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'corsheaders.middleware.CorsMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | # 'django.middleware.csrf.CsrfViewMiddleware', 54 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'multi_tenant.tenant.middleware.authentication.MultTenantAuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'demo.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'demo.wsgi.application' 79 | 80 | AUTH_USER_MODEL = 'tenant.GlobalUser' 81 | AUTH_TENANT_USER_MODEL = 'info.User' 82 | DEFAULT_TENANT_MODEL = 'tenant.Tenant' 83 | # AUTHENTICATION_BACKENDS = ['multi_tenant.user.backend.MultTenantModelBackend'] 84 | CORS_ALLOW_CREDENTIALS = True 85 | CORS_ORIGIN_ALLOW_ALL = True 86 | CORS_ALLOW_METHODS = ['*'] 87 | CORS_ALLOW_HEADERS = ['*'] 88 | 89 | 90 | # Database 91 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 92 | 93 | DATABASES = { 94 | 'default': { 95 | 'ENGINE': 'django.db.backends.mysql', 96 | 'NAME': 'demo1', 97 | 'USER': 'root', 98 | 'PASSWORD': os.environ.get('DB_PASSWORD', 'k3test'), 99 | 'HOST': os.environ.get('DB_HOST', '127.0.0.1'), 100 | 'PORT': os.environ.get('DB_PORT', '3306'), 101 | 102 | } 103 | } 104 | 105 | 106 | DATABASE_APPS_MAPPING = { 107 | 'tenant': 'default', 108 | 'admin': 'default', 109 | # 'auth': 'default', 110 | 'sessions': 'default' 111 | } 112 | 113 | DATABASE_ROUTERS = ['multi_tenant.tenant.utils.db.MultTenantDBRouter'] 114 | 115 | 116 | REST_FRAMEWORK = { 117 | # 'DEFAULT_AUTHENTICATION_CLASSES': [ 118 | # 'rest_framework.authentication.BasicAuthentication', 119 | # 'rest_framework.authentication.SessionAuthentication' 120 | # ], 121 | 'DEFAULT_PERMISSION_CLASSES': [ 122 | 'multi_tenant.rest.permissions.IsTanenatUser', 123 | ], 124 | # 'DEFAULT_PAGINATION_CLASS': 'utils.pagination.CustomPagination', 125 | 'PAGE_SIZE': None, 126 | 'DEFAULT_FILTER_BACKENDS': [ 127 | 'django_filters.rest_framework.DjangoFilterBackend', 128 | 'rest_framework.filters.OrderingFilter' 129 | ], 130 | 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S', 131 | 'DATETIME_INPUT_FORMATS': '%Y-%m-%d %H:%M:%S', 132 | 'DATE_FORMAT': "%Y-%m-%d", 133 | # 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 134 | } 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | # Password validation 143 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 144 | 145 | AUTH_PASSWORD_VALIDATORS = [ 146 | { 147 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 148 | }, 149 | { 150 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 151 | }, 152 | { 153 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 154 | }, 155 | { 156 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 157 | }, 158 | ] 159 | 160 | 161 | # Internationalization 162 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 163 | 164 | LANGUAGE_CODE = 'zh-hans' 165 | 166 | TIME_ZONE = 'Asia/Shanghai' 167 | 168 | USE_I18N = True 169 | 170 | USE_L10N = True 171 | 172 | USE_TZ = False 173 | 174 | 175 | 176 | # Static files (CSS, JavaScript, Images) 177 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 178 | 179 | STATIC_URL = '/static/' 180 | 181 | # Default primary key field type 182 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 183 | 184 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 185 | -------------------------------------------------------------------------------- /examples/demo/settings.postgres.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-#sl_@*kvc&=!&dfujo8x0%$m(2)3b#ukrg#yg&ogtf_*8jkj%h' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'multi_tenant.tenant', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'rest_framework', 42 | 'django_filters', 43 | 'corsheaders', 44 | 'info', 45 | 'multi_tenant.proxy' 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'corsheaders.middleware.CorsMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | # 'django.middleware.csrf.CsrfViewMiddleware', 54 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'multi_tenant.tenant.middleware.authentication.MultTenantAuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'demo.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'demo.wsgi.application' 79 | 80 | AUTH_USER_MODEL = 'tenant.GlobalUser' 81 | AUTH_TENANT_USER_MODEL = 'info.User' 82 | DEFAULT_TENANT_MODEL = 'tenant.Tenant' 83 | # AUTHENTICATION_BACKENDS = ['multi_tenant.user.backend.MultTenantModelBackend'] 84 | CORS_ALLOW_CREDENTIALS = True 85 | CORS_ORIGIN_ALLOW_ALL = True 86 | CORS_ALLOW_METHODS = ['*'] 87 | CORS_ALLOW_HEADERS = ['*'] 88 | 89 | 90 | # Database 91 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 92 | 93 | DATABASES = { 94 | 'default': { 95 | 'ENGINE': 'django.db.backends.postgresql', 96 | 'NAME': 'demo1', 97 | 'USER': 'postgres', 98 | 'PASSWORD': os.environ.get('DB_PASSWORD', 'cwx568319'), 99 | 'HOST': os.environ.get('DB_HOST', '127.0.0.1'), 100 | 'PORT': os.environ.get('DB_PORT', '5432'), 101 | 102 | } 103 | } 104 | 105 | 106 | DATABASE_APPS_MAPPING = { 107 | 'tenant': 'default', 108 | 'admin': 'default', 109 | # 'auth': 'default', 110 | 'sessions': 'default' 111 | } 112 | 113 | DATABASE_ROUTERS = ['multi_tenant.tenant.utils.db.MultTenantDBRouter'] 114 | 115 | 116 | REST_FRAMEWORK = { 117 | # 'DEFAULT_AUTHENTICATION_CLASSES': [ 118 | # 'rest_framework.authentication.BasicAuthentication', 119 | # 'rest_framework.authentication.SessionAuthentication' 120 | # ], 121 | 'DEFAULT_PERMISSION_CLASSES': [ 122 | 'multi_tenant.rest.permissions.IsTanenatUser', 123 | ], 124 | # 'DEFAULT_PAGINATION_CLASS': 'utils.pagination.CustomPagination', 125 | 'PAGE_SIZE': None, 126 | 'DEFAULT_FILTER_BACKENDS': [ 127 | 'django_filters.rest_framework.DjangoFilterBackend', 128 | 'rest_framework.filters.OrderingFilter' 129 | ], 130 | 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S', 131 | 'DATETIME_INPUT_FORMATS': '%Y-%m-%d %H:%M:%S', 132 | 'DATE_FORMAT': "%Y-%m-%d", 133 | # 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 134 | } 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | # Password validation 143 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 144 | 145 | AUTH_PASSWORD_VALIDATORS = [ 146 | { 147 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 148 | }, 149 | { 150 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 151 | }, 152 | { 153 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 154 | }, 155 | { 156 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 157 | }, 158 | ] 159 | 160 | 161 | # Internationalization 162 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 163 | 164 | LANGUAGE_CODE = 'zh-hans' 165 | 166 | TIME_ZONE = 'Asia/Shanghai' 167 | 168 | USE_I18N = True 169 | 170 | USE_L10N = True 171 | 172 | USE_TZ = False 173 | 174 | 175 | 176 | # Static files (CSS, JavaScript, Images) 177 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 178 | 179 | STATIC_URL = '/static/' 180 | 181 | # Default primary key field type 182 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 183 | 184 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 185 | -------------------------------------------------------------------------------- /examples/demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-#sl_@*kvc&=!&dfujo8x0%$m(2)3b#ukrg#yg&ogtf_*8jkj%h' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'multi_tenant.tenant', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'rest_framework', 42 | 'django_filters', 43 | 'corsheaders', 44 | 'info', 45 | 'multi_tenant.proxy' 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'corsheaders.middleware.CorsMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | # 'django.middleware.csrf.CsrfViewMiddleware', 54 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'multi_tenant.tenant.middleware.authentication.MultTenantAuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'demo.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'demo.wsgi.application' 79 | 80 | AUTH_USER_MODEL = 'tenant.GlobalUser' 81 | AUTH_TENANT_USER_MODEL = 'info.User' 82 | DEFAULT_TENANT_MODEL = 'tenant.Tenant' 83 | # AUTHENTICATION_BACKENDS = ['multi_tenant.user.backend.MultTenantModelBackend'] 84 | CORS_ALLOW_CREDENTIALS = True 85 | CORS_ORIGIN_ALLOW_ALL = True 86 | CORS_ALLOW_METHODS = ['*'] 87 | CORS_ALLOW_HEADERS = ['*'] 88 | 89 | 90 | # Database 91 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 92 | 93 | DATABASES = { 94 | 'default': { 95 | 'ENGINE': 'django.db.backends.mysql', 96 | 'NAME': 'demo1', 97 | 'USER': 'root', 98 | 'PASSWORD': os.environ.get('DB_PASSWORD', 'k3test'), 99 | 'HOST': os.environ.get('DB_HOST', '127.0.0.1'), 100 | 'PORT': os.environ.get('DB_PORT', '3306'), 101 | 102 | } 103 | } 104 | 105 | 106 | DATABASE_APPS_MAPPING = { 107 | 'tenant': 'default', 108 | 'admin': 'default', 109 | # 'auth': 'default', 110 | 'sessions': 'default' 111 | } 112 | 113 | DATABASE_ROUTERS = ['multi_tenant.tenant.utils.db.MultTenantDBRouter'] 114 | 115 | 116 | REST_FRAMEWORK = { 117 | # 'DEFAULT_AUTHENTICATION_CLASSES': [ 118 | # 'rest_framework.authentication.BasicAuthentication', 119 | # 'rest_framework.authentication.SessionAuthentication' 120 | # ], 121 | 'DEFAULT_PERMISSION_CLASSES': [ 122 | 'multi_tenant.rest.permissions.IsTanenatUser', 123 | ], 124 | # 'DEFAULT_PAGINATION_CLASS': 'utils.pagination.CustomPagination', 125 | 'PAGE_SIZE': None, 126 | 'DEFAULT_FILTER_BACKENDS': [ 127 | 'django_filters.rest_framework.DjangoFilterBackend', 128 | 'rest_framework.filters.OrderingFilter' 129 | ], 130 | 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S', 131 | 'DATETIME_INPUT_FORMATS': '%Y-%m-%d %H:%M:%S', 132 | 'DATE_FORMAT': "%Y-%m-%d", 133 | # 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 134 | } 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | # Password validation 143 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 144 | 145 | AUTH_PASSWORD_VALIDATORS = [ 146 | { 147 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 148 | }, 149 | { 150 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 151 | }, 152 | { 153 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 154 | }, 155 | { 156 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 157 | }, 158 | ] 159 | 160 | 161 | # Internationalization 162 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 163 | 164 | LANGUAGE_CODE = 'zh-hans' 165 | 166 | TIME_ZONE = 'Asia/Shanghai' 167 | 168 | USE_I18N = True 169 | 170 | USE_L10N = True 171 | 172 | USE_TZ = False 173 | 174 | 175 | 176 | # Static files (CSS, JavaScript, Images) 177 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 178 | 179 | STATIC_URL = '/static/' 180 | 181 | # Default primary key field type 182 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 183 | 184 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 185 | -------------------------------------------------------------------------------- /examples/demo/settings.sqlite3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-#sl_@*kvc&=!&dfujo8x0%$m(2)3b#ukrg#yg&ogtf_*8jkj%h' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'multi_tenant.tenant', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'rest_framework', 42 | 'django_filters', 43 | 'corsheaders', 44 | 'info', 45 | 'multi_tenant.proxy' 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'corsheaders.middleware.CorsMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | # 'django.middleware.csrf.CsrfViewMiddleware', 54 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'multi_tenant.tenant.middleware.authentication.MultTenantAuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'demo.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'demo.wsgi.application' 79 | 80 | AUTH_USER_MODEL = 'tenant.GlobalUser' 81 | AUTH_TENANT_USER_MODEL = 'info.User' 82 | DEFAULT_TENANT_MODEL = 'tenant.Tenant' 83 | # AUTHENTICATION_BACKENDS = ['multi_tenant.user.backend.MultTenantModelBackend'] 84 | CORS_ALLOW_CREDENTIALS = True 85 | CORS_ORIGIN_ALLOW_ALL = True 86 | CORS_ALLOW_METHODS = ['*'] 87 | CORS_ALLOW_HEADERS = ['*'] 88 | 89 | 90 | # Database 91 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 92 | 93 | DATABASES = { 94 | 'default': { 95 | 'ENGINE': 'django.db.backends.sqlite3', 96 | 'NAME': BASE_DIR.joinpath('main.db') 97 | 98 | } 99 | } 100 | 101 | 102 | DATABASE_APPS_MAPPING = { 103 | 'tenant': 'default', 104 | 'admin': 'default', 105 | # 'auth': 'default', 106 | 'sessions': 'default' 107 | } 108 | 109 | DATABASE_ROUTERS = ['multi_tenant.tenant.utils.db.MultTenantDBRouter'] 110 | 111 | 112 | REST_FRAMEWORK = { 113 | # 'DEFAULT_AUTHENTICATION_CLASSES': [ 114 | # 'rest_framework.authentication.BasicAuthentication', 115 | # 'rest_framework.authentication.SessionAuthentication' 116 | # ], 117 | 'DEFAULT_PERMISSION_CLASSES': [ 118 | 'multi_tenant.rest.permissions.IsTanenatUser', 119 | ], 120 | # 'DEFAULT_PAGINATION_CLASS': 'utils.pagination.CustomPagination', 121 | 'PAGE_SIZE': None, 122 | 'DEFAULT_FILTER_BACKENDS': [ 123 | 'django_filters.rest_framework.DjangoFilterBackend', 124 | 'rest_framework.filters.OrderingFilter' 125 | ], 126 | 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S', 127 | 'DATETIME_INPUT_FORMATS': '%Y-%m-%d %H:%M:%S', 128 | 'DATE_FORMAT': "%Y-%m-%d", 129 | # 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 130 | } 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | # Password validation 139 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 140 | 141 | AUTH_PASSWORD_VALIDATORS = [ 142 | { 143 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 144 | }, 145 | { 146 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 147 | }, 148 | { 149 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 150 | }, 151 | { 152 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 153 | }, 154 | ] 155 | 156 | 157 | # Internationalization 158 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 159 | 160 | LANGUAGE_CODE = 'zh-hans' 161 | 162 | TIME_ZONE = 'Asia/Shanghai' 163 | 164 | USE_I18N = True 165 | 166 | USE_L10N = True 167 | 168 | USE_TZ = False 169 | 170 | 171 | 172 | # Static files (CSS, JavaScript, Images) 173 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 174 | 175 | STATIC_URL = '/static/' 176 | 177 | # Default primary key field type 178 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 179 | 180 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 181 | -------------------------------------------------------------------------------- /examples/demo/urls.py: -------------------------------------------------------------------------------- 1 | """demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from rest_framework.documentation import include_docs_urls 20 | 21 | urlpatterns = [ 22 | # path('docs/', include_docs_urls(title='Marco API', description='App开发接口文档', authentication_classes=(), 23 | # permission_classes=())), 24 | path('admin/', admin.site.urls), 25 | path('test/', include('info.urls')) 26 | ] 27 | -------------------------------------------------------------------------------- /examples/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/info/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/examples/info/__init__.py -------------------------------------------------------------------------------- /examples/info/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from info.models import User 4 | 5 | @admin.register(User) 6 | class UserAdmin(UserAdmin): 7 | pass 8 | # Register your models here. 9 | 10 | 11 | # admin.register(User, User) -------------------------------------------------------------------------------- /examples/info/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class InfoConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'info' 7 | 8 | def ready(self) -> None: 9 | return super().ready() 10 | -------------------------------------------------------------------------------- /examples/info/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-09-09 21:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='User', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('username', models.CharField(max_length=50, unique=True)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /examples/info/migrations/0002_auto_20210909_2310.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-09-09 23:10 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('auth', '0012_alter_user_first_name_max_length'), 13 | ('info', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterModelOptions( 18 | name='user', 19 | options={'verbose_name': 'user', 'verbose_name_plural': 'users'}, 20 | ), 21 | migrations.AlterModelManagers( 22 | name='user', 23 | managers=[ 24 | ('objects', django.contrib.auth.models.UserManager()), 25 | ], 26 | ), 27 | migrations.AddField( 28 | model_name='user', 29 | name='date_joined', 30 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'), 31 | ), 32 | migrations.AddField( 33 | model_name='user', 34 | name='email', 35 | field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), 36 | ), 37 | migrations.AddField( 38 | model_name='user', 39 | name='first_name', 40 | field=models.CharField(blank=True, max_length=150, verbose_name='first name'), 41 | ), 42 | migrations.AddField( 43 | model_name='user', 44 | name='groups', 45 | field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), 46 | ), 47 | migrations.AddField( 48 | model_name='user', 49 | name='is_active', 50 | field=models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active'), 51 | ), 52 | migrations.AddField( 53 | model_name='user', 54 | name='is_staff', 55 | field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'), 56 | ), 57 | migrations.AddField( 58 | model_name='user', 59 | name='is_superuser', 60 | field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'), 61 | ), 62 | migrations.AddField( 63 | model_name='user', 64 | name='last_login', 65 | field=models.DateTimeField(blank=True, null=True, verbose_name='last login'), 66 | ), 67 | migrations.AddField( 68 | model_name='user', 69 | name='last_name', 70 | field=models.CharField(blank=True, max_length=150, verbose_name='last name'), 71 | ), 72 | migrations.AddField( 73 | model_name='user', 74 | name='password', 75 | field=models.CharField(default='', max_length=128, verbose_name='password'), 76 | preserve_default=False, 77 | ), 78 | migrations.AddField( 79 | model_name='user', 80 | name='user_permissions', 81 | field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), 82 | ), 83 | migrations.AlterField( 84 | model_name='user', 85 | name='username', 86 | field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), 87 | ), 88 | ] 89 | -------------------------------------------------------------------------------- /examples/info/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/examples/info/migrations/__init__.py -------------------------------------------------------------------------------- /examples/info/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | 4 | # Create your models here. 5 | class User(AbstractUser): 6 | pass -------------------------------------------------------------------------------- /examples/info/serilizer.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from info.models import User 4 | 5 | class UserSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = User 9 | fields = '__all__' -------------------------------------------------------------------------------- /examples/info/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/info/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework import routers, urlpatterns 2 | from .views import UserViewSet 3 | 4 | 5 | from rest_framework.routers import SimpleRouter 6 | 7 | 8 | router = SimpleRouter() 9 | router.register('user', UserViewSet) 10 | 11 | urlpatterns = router.urls -------------------------------------------------------------------------------- /examples/info/views.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from info.models import User 4 | from .serilizer import UserSerializer 5 | 6 | from rest_framework.viewsets import ModelViewSet 7 | 8 | class UserViewSet(ModelViewSet): 9 | queryset = User.objects.all() 10 | serializer_class = UserSerializer 11 | -------------------------------------------------------------------------------- /examples/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /multi_tenant/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/__init__.py -------------------------------------------------------------------------------- /multi_tenant/const.py: -------------------------------------------------------------------------------- 1 | DEFAULT_DB_ENGINE_MAP = { 2 | 'oracle': 'django.db.backends.oracle', 3 | 'mysql': 'django.db.backends.mysql', 4 | 'sqlite3': 'django.db.backends.sqlite3', 5 | 'postgres': 'django.db.backends.postgresql' 6 | } 7 | 8 | DEFAULT_TENANT_MODEL = 'tenant.Tenant' 9 | AUTH_TENANT_USER_MODEL = 'auth.User' -------------------------------------------------------------------------------- /multi_tenant/local.py: -------------------------------------------------------------------------------- 1 | from threading import local 2 | 3 | _thread_local = local() 4 | 5 | 6 | def get_current_db(): 7 | return getattr(_thread_local, 'db_name', 'default') 8 | 9 | 10 | def set_current_db(db_name): 11 | 12 | setattr(_thread_local, 'db_name', db_name) 13 | -------------------------------------------------------------------------------- /multi_tenant/proxy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/proxy/__init__.py -------------------------------------------------------------------------------- /multi_tenant/proxy/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /multi_tenant/proxy/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProxyConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'multi_tenant.proxy' 7 | -------------------------------------------------------------------------------- /multi_tenant/proxy/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-09-09 21:38 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ('contenttypes', '0002_remove_content_type_name'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ProxyContentType', 17 | fields=[ 18 | ], 19 | options={ 20 | 'managed': False, 21 | 'proxy': True, 22 | }, 23 | bases=('contenttypes.contenttype',), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /multi_tenant/proxy/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/proxy/migrations/__init__.py -------------------------------------------------------------------------------- /multi_tenant/proxy/models.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from django.db import models 4 | from django.contrib.contenttypes.models import ContentType 5 | 6 | 7 | class ContentTypeManager(models.Manager): 8 | def get_by_natural_key(self, app_label, model): 9 | 10 | return self.get(app_label=app_label, model=model) 11 | 12 | def _get_opts(self, model, for_concrete_model): 13 | if for_concrete_model: 14 | model = model._meta.concrete_model 15 | return model._meta 16 | 17 | def get_for_model(self, model, for_concrete_model=True): 18 | opts = self._get_opts(model, for_concrete_model) 19 | try: 20 | ct = self.get(app_label=opts.app_label, model=opts.model_name) 21 | except self.model.DoesNotExist: 22 | ct, created = self.get_or_create( 23 | app_label=opts.app_label, 24 | model=opts.model_name, 25 | ) 26 | return ct 27 | 28 | def get_for_models(self, *models, for_concrete_models=True): 29 | """ 30 | Given *models, return a dictionary mapping {model: content_type}. 31 | """ 32 | results = {} 33 | # Models that aren't already in the cache. 34 | needed_app_labels = set() 35 | needed_models = set() 36 | # Mapping of opts to the list of models requiring it. 37 | needed_opts = defaultdict(list) 38 | for model in models: 39 | opts = self._get_opts(model, for_concrete_models) 40 | needed_app_labels.add(opts.app_label) 41 | needed_models.add(opts.model_name) 42 | needed_opts[opts].append(model) 43 | if needed_opts: 44 | # Lookup required content types from the DB. 45 | cts = self.filter( 46 | app_label__in=needed_app_labels, 47 | model__in=needed_models 48 | ) 49 | for ct in cts: 50 | opts_models = needed_opts.pop(ct.model_class()._meta, []) 51 | for model in opts_models: 52 | results[model] = ct 53 | # Create content types that weren't in the cache or DB. 54 | for opts, opts_models in needed_opts.items(): 55 | ct = self.create( 56 | app_label=opts.app_label, 57 | model=opts.model_name, 58 | ) 59 | for model in opts_models: 60 | results[model] = ct 61 | return results 62 | 63 | def get_for_id(self, id): 64 | """ 65 | Lookup a ContentType by ID. Use the same shared cache as get_for_model 66 | (though ContentTypes are not created on-the-fly by get_by_id). 67 | """ 68 | ct = self.get(pk=id) 69 | return ct 70 | 71 | 72 | class ProxyContentType(ContentType): 73 | objects = ContentTypeManager() 74 | 75 | class Meta: 76 | managed = False 77 | proxy = True -------------------------------------------------------------------------------- /multi_tenant/proxy/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /multi_tenant/proxy/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /multi_tenant/rest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/rest/__init__.py -------------------------------------------------------------------------------- /multi_tenant/rest/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/rest/admin.py -------------------------------------------------------------------------------- /multi_tenant/rest/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RestConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'multi_tenant.rest' 7 | 8 | def ready(self): 9 | from .patch import request 10 | return super().ready() 11 | -------------------------------------------------------------------------------- /multi_tenant/rest/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/rest/migrations/__init__.py -------------------------------------------------------------------------------- /multi_tenant/rest/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/rest/models.py -------------------------------------------------------------------------------- /multi_tenant/rest/patch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/rest/patch/__init__.py -------------------------------------------------------------------------------- /multi_tenant/rest/patch/request.py: -------------------------------------------------------------------------------- 1 | from rest_framework.request import Request 2 | from rest_framework import exceptions 3 | 4 | from multi_tenant.local import set_current_db 5 | 6 | 7 | def __request_authenticate(self): 8 | """ 9 | Attempt to authenticate the request using each authentication instance 10 | in turn. 11 | """ 12 | for authenticator in self.authenticators: 13 | try: 14 | user_auth_tuple = authenticator.authenticate(self) 15 | except exceptions.APIException: 16 | self._not_authenticated() 17 | raise 18 | 19 | if user_auth_tuple is not None: 20 | self._authenticator = authenticator 21 | self.user, self.auth = user_auth_tuple 22 | if self.user and self.user.tenant: 23 | set_current_db(self.user.tenant.code) 24 | return 25 | 26 | self._not_authenticated() 27 | 28 | Request._authenticate = __request_authenticate -------------------------------------------------------------------------------- /multi_tenant/rest/permissions/__init__.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from rest_framework import exceptions 3 | from rest_framework.permissions import BasePermission 4 | from rest_framework.request import Request 5 | from multi_tenant.tenant import get_tenant_user_model 6 | 7 | SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') 8 | 9 | class IsTanenatUser(BasePermission): 10 | """ 11 | 超级用户默认拥有所有权限,如果是租户用户,公司编码不能为空, 12 | """ 13 | def has_permission(self, request: Request, view) -> bool: 14 | user = request.user 15 | if user.is_authenticated and user.tenant: 16 | return bool(request.user.tenant) 17 | else: 18 | return False 19 | 20 | 21 | class IsAdminUser(BasePermission): 22 | """ 23 | Allows access only to admin users. 24 | """ 25 | TenantUser = get_tenant_user_model() 26 | 27 | def has_permission(self, request, view): 28 | username = request.user.username 29 | current_user = None 30 | try: 31 | current_user = self.TenantUser.objects.filter(is_active=True).get(username=username) 32 | except self.TenantUser.DoesNotExist: 33 | return False 34 | 35 | return bool(current_user and current_user.is_staff) 36 | 37 | 38 | 39 | 40 | class DjangoModelPermissions(BasePermission): 41 | """ 42 | 适配了djangoModelPermissions的多租户场景 43 | """ 44 | TenantUser = get_tenant_user_model() 45 | perms_map = { 46 | 'GET': [], 47 | 'OPTIONS': [], 48 | 'HEAD': [], 49 | 'POST': ['%(app_label)s.add_%(model_name)s'], 50 | 'PUT': ['%(app_label)s.change_%(model_name)s'], 51 | 'PATCH': ['%(app_label)s.change_%(model_name)s'], 52 | 'DELETE': ['%(app_label)s.delete_%(model_name)s'], 53 | } 54 | 55 | authenticated_users_only = True 56 | 57 | def get_required_permissions(self, method, model_cls): 58 | """ 59 | Given a model and an HTTP method, return the list of permission 60 | codes that the user is required to have. 61 | """ 62 | kwargs = { 63 | 'app_label': model_cls._meta.app_label, 64 | 'model_name': model_cls._meta.model_name 65 | } 66 | 67 | if method not in self.perms_map: 68 | raise exceptions.MethodNotAllowed(method) 69 | 70 | return [perm % kwargs for perm in self.perms_map[method]] 71 | 72 | def _queryset(self, view): 73 | assert hasattr(view, 'get_queryset') \ 74 | or getattr(view, 'queryset', None) is not None, ( 75 | 'Cannot apply {} on a view that does not set ' 76 | '`.queryset` or have a `.get_queryset()` method.' 77 | ).format(self.__class__.__name__) 78 | 79 | if hasattr(view, 'get_queryset'): 80 | queryset = view.get_queryset() 81 | assert queryset is not None, ( 82 | '{}.get_queryset() returned None'.format(view.__class__.__name__) 83 | ) 84 | return queryset 85 | return view.queryset 86 | 87 | def has_permission(self, request, view): 88 | # Workaround to ensure DjangoModelPermissions are not applied 89 | # to the root view when using DefaultRouter. 90 | username = request.user.username 91 | current_user = None 92 | try: 93 | current_user = self.TenantUser.objects.filter(is_active=True).get(username=username) 94 | except self.TenantUser.DoesNotExist: 95 | return False 96 | 97 | if getattr(view, '_ignore_model_permissions', False): 98 | return True 99 | 100 | if not request.user or ( 101 | not request.user.is_authenticated and self.authenticated_users_only): 102 | return False 103 | 104 | queryset = self._queryset(view) 105 | perms = self.get_required_permissions(request.method, queryset.model) 106 | 107 | return current_user.has_perms(perms) 108 | 109 | 110 | class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): 111 | """ 112 | Similar to DjangoModelPermissions, except that anonymous users are 113 | allowed read-only access. 114 | """ 115 | authenticated_users_only = False 116 | 117 | 118 | class DjangoObjectPermissions(DjangoModelPermissions): 119 | """ 120 | 适配了DjangoObjectPermissions的多租户场景 121 | """ 122 | TenantUser = get_tenant_user_model() 123 | 124 | 125 | perms_map = { 126 | 'GET': [], 127 | 'OPTIONS': [], 128 | 'HEAD': [], 129 | 'POST': ['%(app_label)s.add_%(model_name)s'], 130 | 'PUT': ['%(app_label)s.change_%(model_name)s'], 131 | 'PATCH': ['%(app_label)s.change_%(model_name)s'], 132 | 'DELETE': ['%(app_label)s.delete_%(model_name)s'], 133 | } 134 | 135 | def get_required_object_permissions(self, method, model_cls): 136 | kwargs = { 137 | 'app_label': model_cls._meta.app_label, 138 | 'model_name': model_cls._meta.model_name 139 | } 140 | 141 | if method not in self.perms_map: 142 | raise exceptions.MethodNotAllowed(method) 143 | 144 | return [perm % kwargs for perm in self.perms_map[method]] 145 | 146 | def has_object_permission(self, request, view, obj): 147 | username = request.user.username 148 | current_user = None 149 | try: 150 | current_user = self.TenantUser.objects.filter(is_active=True).get(username=username) 151 | except self.TenantUser.DoesNotExist: 152 | return False 153 | # authentication checks have already executed via has_permission 154 | queryset = self._queryset(view) 155 | model_cls = queryset.model 156 | 157 | perms = self.get_required_object_permissions(request.method, model_cls) 158 | 159 | if not current_user.has_perms(perms, obj): 160 | if request.method in SAFE_METHODS: 161 | # Read permissions already checked and failed, no need 162 | # to make another lookup. 163 | raise Http404 164 | 165 | read_perms = self.get_required_object_permissions('GET', model_cls) 166 | if not current_user.has_perms(read_perms, obj): 167 | raise Http404 168 | 169 | # Has read permissions. 170 | return False 171 | 172 | return True 173 | 174 | -------------------------------------------------------------------------------- /multi_tenant/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, List 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.apps import apps as django_apps 5 | from django.db.models import Model 6 | from multi_tenant.const import DEFAULT_TENANT_MODEL, AUTH_TENANT_USER_MODEL 7 | from django.conf import settings 8 | logger = logging.getLogger('django.request') 9 | 10 | 11 | 12 | def get_tenant_user_model() -> Model: 13 | """ 14 | Return the User model that is active in this project. 15 | """ 16 | try: 17 | default_tenant_model = settings.AUTH_TENANT_USER_MODEL 18 | if default_tenant_model: 19 | return django_apps.get_model(default_tenant_model, require_ready=False) 20 | else: 21 | return django_apps.get_model(AUTH_TENANT_USER_MODEL, require_ready=False) 22 | except ValueError: 23 | logger.error("DEFAULT_TENANT_MODEL must be of the form 'app_label.model_name'") 24 | raise ImproperlyConfigured("DEFAULT_TENANT_MODEL must be of the form 'app_label.model_name'") 25 | except LookupError: 26 | logger.error("DEFAULT_TENANT_MODEL refers to model '%s' that has not been installed" % settings.DEFAULT_TENANT_MODEL) 27 | raise ImproperlyConfigured( 28 | "DEFAULT_TENANT_MODEL refers to model '%s' that has not been installed" % settings.DEFAULT_TENANT_MODEL 29 | ) 30 | 31 | def get_tenant_model() -> Model: 32 | """ 33 | Return the User model that is active in this project. 34 | """ 35 | try: 36 | default_tenant_model = DEFAULT_TENANT_MODEL 37 | if settings.DEFAULT_TENANT_MODEL: 38 | default_tenant_model = settings.DEFAULT_TENANT_MODEL 39 | return django_apps.get_model(default_tenant_model, require_ready=False) 40 | except ValueError: 41 | logger.error("DEFAULT_TENANT_MODEL must be of the form 'app_label.model_name'") 42 | raise ImproperlyConfigured("DEFAULT_TENANT_MODEL must be of the form 'app_label.model_name'") 43 | except LookupError: 44 | logger.error("DEFAULT_TENANT_MODEL refers to model '%s' that has not been installed" % settings.DEFAULT_TENANT_MODEL) 45 | raise ImproperlyConfigured( 46 | "DEFAULT_TENANT_MODEL refers to model '%s' that has not been installed" % settings.DEFAULT_TENANT_MODEL 47 | ) 48 | 49 | def get_tenant_db(alias: str) -> Dict[str,str]: 50 | Tenant = get_tenant_model() 51 | try: 52 | tenant = Tenant.objects.using('default').filter(is_active=True).get(code=alias) 53 | return tenant.get_db_config() 54 | except Tenant.DoesNotExist: 55 | logger.warning(f'db alias [{alias}] dont exists') 56 | pass 57 | 58 | 59 | def get_all_tenant_db() -> Dict[str,Dict]: 60 | Tenant = get_tenant_model() 61 | dbs = dict() 62 | try: 63 | queryset = Tenant.objects.using('default').filter(is_active=True) 64 | dbs = dict() 65 | for tenant in queryset: 66 | dbs[tenant.code] = tenant.get_db_config() 67 | except: 68 | pass 69 | return dbs 70 | 71 | def get_common_apps() ->List[str]: 72 | common_applist = [] 73 | for key, val in settings.DATABASE_APPS_MAPPING.items(): 74 | if val == 'default': 75 | common_applist.append(key) 76 | return common_applist -------------------------------------------------------------------------------- /multi_tenant/tenant/admin.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Tuple 2 | from multi_tenant.tenant.models import Tenant 3 | from django.contrib import admin 4 | from django.utils.translation import gettext, gettext_lazy as _ 5 | from django.contrib.auth.backends import UserModel 6 | from multi_tenant.tenant import get_tenant_model 7 | from django.db.models import Model 8 | from django.urls import path, reverse 9 | from django.http.request import HttpRequest 10 | from django.contrib import admin, messages 11 | from django.contrib.admin.options import IS_POPUP_VAR 12 | from django.contrib.admin.utils import unquote 13 | from django.contrib.auth import update_session_auth_hash 14 | from django.contrib.auth.forms import ( 15 | AdminPasswordChangeForm, UserChangeForm, UserCreationForm, 16 | ) 17 | from django.core.exceptions import PermissionDenied 18 | from django.db import router, transaction 19 | from django.http import Http404, HttpResponseRedirect 20 | from django.template.response import TemplateResponse 21 | from django.utils.decorators import method_decorator 22 | from django.utils.html import escape 23 | from django.views.decorators.csrf import csrf_protect 24 | from django.views.decorators.debug import sensitive_post_parameters 25 | 26 | csrf_protect_m = method_decorator(csrf_protect) 27 | sensitive_post_parameters_m = method_decorator(sensitive_post_parameters()) 28 | # from .middleware.admin import SysModelAdminMix 29 | 30 | 31 | Tenant = get_tenant_model() 32 | 33 | @admin.register(Tenant) 34 | class TenantAdmin(admin.ModelAdmin): 35 | list_display: Tuple = ('name', 'label', 'code', 'is_active') 36 | search_fields: Tuple = ('name', 'label', 'code') 37 | list_filter:Tuple = ('is_active', 'engine') 38 | def get_readonly_fields(self, request: HttpRequest, obj: Model=None) -> Tuple: 39 | if obj: 40 | return ("db_password",'db_name','engine', 'options') 41 | return [] 42 | 43 | def has_delete_permission(self, request: HttpRequest, obj: Model=None) -> bool: 44 | # 禁用删除按钮 45 | return False 46 | 47 | @admin.register(UserModel) 48 | class GlobalUserAdmin(admin.ModelAdmin): 49 | 50 | add_form_template = 'admin/auth/user/add_form.html' 51 | change_user_password_template = None 52 | fieldsets = ( 53 | (None, {'fields': ('username', 'password')}), 54 | (_('Permissions'), { 55 | 'fields': ('is_active', 'is_staff', 'is_super', ), 56 | }), 57 | ('所属租户', { 58 | 'fields': ('tenant', ), 59 | }), 60 | ) 61 | add_fieldsets = ( 62 | (None, { 63 | 'classes': ('wide',), 64 | 'fields': ('username', 'password1', 'password2'), 65 | }), 66 | ) 67 | form = UserChangeForm 68 | add_form = UserCreationForm 69 | change_password_form = AdminPasswordChangeForm 70 | list_display = ('username', 'is_staff', 'is_super','is_active','tenant') 71 | list_filter = ('is_staff', 'is_super', 'is_active',) 72 | search_fields = ('username',) 73 | ordering = ('username',) 74 | 75 | def has_delete_permission(self, request: HttpRequest, obj: Model=None) -> bool: 76 | # 禁用删除按钮 77 | return False 78 | 79 | def get_fieldsets(self, request: HttpRequest, obj:Model=None): 80 | if not obj: 81 | return self.add_fieldsets 82 | return super().get_fieldsets(request, obj) 83 | 84 | def get_form(self, request: HttpRequest, obj:Model=None, **kwargs): 85 | """ 86 | Use special form during user creation 87 | """ 88 | defaults = {} 89 | if obj is None: 90 | defaults['form'] = self.add_form 91 | defaults.update(kwargs) 92 | return super().get_form(request, obj, **defaults) 93 | 94 | def get_urls(self): 95 | return [ 96 | path( 97 | '/password/', 98 | self.admin_site.admin_view(self.user_change_password), 99 | name='auth_user_password_change', 100 | ), 101 | ] + super().get_urls() 102 | 103 | def lookup_allowed(self, lookup:str, value:Any): 104 | # Don't allow lookups involving passwords. 105 | return not lookup.startswith('password') and super().lookup_allowed(lookup, value) 106 | 107 | @sensitive_post_parameters_m 108 | @csrf_protect_m 109 | def add_view(self, request: HttpRequest, form_url:str = '', extra_context=None): 110 | with transaction.atomic(using=router.db_for_write(self.model)): 111 | return self._add_view(request, form_url, extra_context) 112 | 113 | def _add_view(self, request: HttpRequest, form_url:str = '', extra_context=None): 114 | # It's an error for a user to have add permission but NOT change 115 | # permission for users. If we allowed such users to add users, they 116 | # could create superusers, which would mean they would essentially have 117 | # the permission to change users. To avoid the problem entirely, we 118 | # disallow users from adding users if they don't have change 119 | # permission. 120 | user = request.user 121 | if not (user.is_active and user.is_super): 122 | raise PermissionDenied 123 | if extra_context is None: 124 | extra_context = {} 125 | username_field = self.model._meta.get_field(self.model.USERNAME_FIELD) 126 | defaults = { 127 | 'auto_populated_fields': (), 128 | 'username_help_text': username_field.help_text, 129 | } 130 | extra_context.update(defaults) 131 | return super().add_view(request, form_url, extra_context) 132 | 133 | @sensitive_post_parameters_m 134 | def user_change_password(self, request:HttpRequest, id:int, form_url:str=''): 135 | user = self.get_object(request, unquote(id)) 136 | user = request.user 137 | if not (user.is_active and user.is_super): 138 | raise PermissionDenied 139 | if user is None: 140 | raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % { 141 | 'name': self.model._meta.verbose_name, 142 | 'key': escape(id), 143 | }) 144 | if request.method == 'POST': 145 | form = self.change_password_form(user, request.POST) 146 | if form.is_valid(): 147 | form.save() 148 | change_message = self.construct_change_message(request, form, None) 149 | self.log_change(request, user, change_message) 150 | msg = gettext('Password changed successfully.') 151 | messages.success(request, msg) 152 | update_session_auth_hash(request, form.user) 153 | return HttpResponseRedirect( 154 | reverse( 155 | '%s:%s_%s_change' % ( 156 | self.admin_site.name, 157 | user._meta.app_label, 158 | user._meta.model_name, 159 | ), 160 | args=(user.pk,), 161 | ) 162 | ) 163 | else: 164 | form = self.change_password_form(user) 165 | 166 | fieldsets = [(None, {'fields': list(form.base_fields)})] 167 | adminForm = admin.helpers.AdminForm(form, fieldsets, {}) 168 | 169 | context = { 170 | 'title': _('Change password: %s') % escape(user.get_username()), 171 | 'adminForm': adminForm, 172 | 'form_url': form_url, 173 | 'form': form, 174 | 'is_popup': (IS_POPUP_VAR in request.POST or 175 | IS_POPUP_VAR in request.GET), 176 | 'is_popup_var': IS_POPUP_VAR, 177 | 'add': True, 178 | 'change': False, 179 | 'has_delete_permission': False, 180 | 'has_change_permission': True, 181 | 'has_absolute_url': False, 182 | 'opts': self.model._meta, 183 | 'original': user, 184 | 'save_as': False, 185 | 'show_save': True, 186 | **self.admin_site.each_context(request), 187 | } 188 | 189 | request.current_app = self.admin_site.name 190 | 191 | return TemplateResponse( 192 | request, 193 | self.change_user_password_template or 194 | 'admin/auth/user/change_password.html', 195 | context, 196 | ) 197 | 198 | def response_add(self, request: HttpRequest, obj: Model, post_url_continue=None): 199 | if '_addanother' not in request.POST and IS_POPUP_VAR not in request.POST: 200 | request.POST = request.POST.copy() 201 | request.POST['_continue'] = 1 202 | return super().response_add(request, obj, post_url_continue) 203 | 204 | 205 | admin.register(UserModel, GlobalUserAdmin) 206 | 207 | -------------------------------------------------------------------------------- /multi_tenant/tenant/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from multi_tenant.tenant import get_all_tenant_db 3 | from django.db import connections 4 | 5 | 6 | 7 | class TenantConfig(AppConfig): 8 | default_auto_field = 'django.db.models.BigAutoField' 9 | name = 'multi_tenant.tenant' 10 | 11 | def ready(self) -> None: 12 | from .signal import create_data_handler 13 | from .patch.connection import ConnectionHandler 14 | from .patch.contenttype import management 15 | from .patch.permission import management 16 | from .patch.user import User 17 | dbs = list(get_all_tenant_db().keys()) 18 | for db in dbs: 19 | connections[db] 20 | return super().ready() 21 | -------------------------------------------------------------------------------- /multi_tenant/tenant/backend.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.backends import ModelBackend, UserModel 2 | from django.http.request import HttpRequest 3 | 4 | from multi_tenant.local import set_current_db 5 | 6 | class MultTenantModelBackend(ModelBackend) : 7 | def authenticate(self, request:HttpRequest, username: str=None, password: str=None, **kwargs) -> UserModel: 8 | user = super().authenticate(request, username=username, password=password, **kwargs) 9 | if user and user.tenant: 10 | code = user.tenant.code 11 | set_current_db(code) 12 | return user 13 | -------------------------------------------------------------------------------- /multi_tenant/tenant/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/tenant/management/__init__.py -------------------------------------------------------------------------------- /multi_tenant/tenant/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/tenant/management/commands/__init__.py -------------------------------------------------------------------------------- /multi_tenant/tenant/management/commands/createtenant.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management utility to create superusers. 3 | """ 4 | import getpass 5 | import sys 6 | 7 | from django.core import exceptions 8 | from django.core.management.base import BaseCommand, CommandError 9 | from django.utils.text import capfirst 10 | from multi_tenant.tenant import get_tenant_model 11 | from django.conf import settings 12 | 13 | 14 | class NotRunningInTTYException(Exception): 15 | pass 16 | 17 | 18 | PASSWORD_FIELD = 'password' 19 | 20 | 21 | class Command(BaseCommand): 22 | help = 'Used to create a superuser.' 23 | requires_migrations_checks = True 24 | stealth_options = ('stdin',) 25 | 26 | def __init__(self, *args, **kwargs): 27 | super().__init__(*args, **kwargs) 28 | self.TenantModel = get_tenant_model() 29 | self.code_field = self.TenantModel._meta.get_field(self.TenantModel.CODE_FIED) 30 | self.require_fields = [''] 31 | self.engine_choices = [item[0] for item in self.TenantModel.engine_choices] 32 | 33 | def add_arguments(self, parser): 34 | parser.add_argument( 35 | '--code', 36 | help='code of tenant.', 37 | ) 38 | 39 | def execute(self, *args, **options): 40 | self.stdin = options.get('stdin', sys.stdin) # Used for testing 41 | return super().execute(*args, **options) 42 | 43 | def handle(self, *args, **options): 44 | tenant_code = options[self.TenantModel.CODE_FIED] 45 | tenant_data = dict() 46 | if tenant_code: 47 | tenant_data[self.TenantModel.CODE_FIED] = tenant_code 48 | verbose_field_name = self.code_field.verbose_name 49 | try: 50 | if hasattr(self.stdin, 'isatty') and not self.stdin.isatty(): 51 | raise NotRunningInTTYException 52 | if tenant_code: 53 | error_msg = self._validate_tenant_code(tenant_code, verbose_field_name) 54 | if error_msg: 55 | self.stderr.write(error_msg) 56 | tenant_code = None 57 | elif tenant_code == '': 58 | raise CommandError('%s cannot be blank.' % capfirst(self.code_field)) 59 | # Prompt for username. 60 | while tenant_code is None: 61 | message = 'code of tenant: ' 62 | tenant_code = input(message) 63 | if tenant_code: 64 | error_msg = self._validate_tenant_code(tenant_code, verbose_field_name) 65 | if error_msg: 66 | self.stderr.write(error_msg) 67 | username = None 68 | continue 69 | tenant_data[self.TenantModel.CODE_FIED] = tenant_code 70 | tenant_name = None 71 | while True: 72 | input_value = input('connection name: ') 73 | error_msg = self._validate_tenant_name('name', 'connection') 74 | if not error_msg: 75 | tenant_name = input_value 76 | tenant_data['name'] = tenant_name 77 | break 78 | else: 79 | self.stderr.write(error_msg) 80 | 81 | is_some_default = True 82 | while True: 83 | input_value = input('Use the same database configuration as the primary database (y/N)?: ') 84 | if input_value.lower() =='y': 85 | is_some_default = True 86 | break 87 | elif input_value.lower() == 'n': 88 | is_some_default = False 89 | break 90 | else: 91 | self.stderr.write('error input,please input y or N.') 92 | # Prompt for required fields. 93 | if not is_some_default: 94 | database_engine = None 95 | while True: 96 | input_value = input('database schema: ') 97 | error_msg = self._validate_database_engine(input_value, 'database schema') 98 | if not error_msg: 99 | database_engine = input_value 100 | tenant_data['engine'] = input_value 101 | break 102 | else: 103 | self.stderr.write('error input, schema must be in %s' %','.join(self.engine_choices)) 104 | others_required_fields = ['db_name','db_password','user','host', 'port'] 105 | option_fields = ['user','host', 'port'] 106 | if database_engine.lower() == 'sqlite3': 107 | others_required_fields = ['db_name'] 108 | for field in others_required_fields: 109 | if field.lower() == 'db_password': 110 | while True: 111 | password = getpass.getpass() 112 | password2 = getpass.getpass('Password (again): ') 113 | if password != password2: 114 | self.stderr.write("Error: Your passwords didn't match.") 115 | continue 116 | if password.strip() == '': 117 | self.stderr.write("Error: Blank passwords aren't allowed.") 118 | # Don't validate blank passwords. 119 | continue 120 | tenant_data[field] = password.strip() 121 | break 122 | else: 123 | input_value = input( field + ': ') 124 | if field in option_fields: 125 | if not tenant_data.get('options'): 126 | tenant_data['options'] = dict() 127 | tenant_data['options'][field] = input_value 128 | else: 129 | tenant_data[field] = input_value 130 | else: 131 | DAFAULT_DB = settings.DATABASES['default'] 132 | # tenant_data['engine'] = DAFAULT_DB['ENGINE'] 133 | default_engine = DAFAULT_DB['ENGINE'] 134 | engine_name = self.TenantModel.inject_engine(default_engine) 135 | tenant_data['engine'] = engine_name 136 | tenant_data['db_name'] = tenant_data['name'] 137 | tenant_data['label'] = tenant_data['name'] 138 | tenant = self.TenantModel._default_manager.db_manager('default').create_tenant(**tenant_data) 139 | try: 140 | tenant.create_database() 141 | except Exception as e: 142 | self.stderr.write(e) 143 | tenant.delete(force=True) 144 | except KeyboardInterrupt: 145 | self.stderr.write('\nOperation cancelled.') 146 | sys.exit(1) 147 | except exceptions.ValidationError as e: 148 | raise CommandError('; '.join(e.messages)) 149 | except NotRunningInTTYException: 150 | self.stdout.write( 151 | 'Superuser creation skipped due to not running in a TTY. ' 152 | 'You can run `manage.py createsuperuser` in your project ' 153 | 'to create one manually.' 154 | ) 155 | 156 | def _validate_tenant_code(self, tenant_code, verbose_field_name): 157 | """Validate tenant_code. If invalid, return a string error message.""" 158 | try: 159 | self.TenantModel._default_manager.db_manager('default').get(code=tenant_code) 160 | except self.TenantModel.DoesNotExist: 161 | pass 162 | else: 163 | return 'Error: That tenant_code is already taken.' 164 | if not tenant_code: 165 | return '%s cannot be blank.' % capfirst(verbose_field_name) 166 | def _validate_tenant_name(self, name, verbose_field_name): 167 | """Validate tenant_name. If invalid, return a string error message.""" 168 | try: 169 | self.TenantModel._default_manager.db_manager('default').get(name=name) 170 | except self.TenantModel.DoesNotExist: 171 | pass 172 | else: 173 | return 'Error: That tenant_name is already taken.' 174 | if not name: 175 | return '%s cannot be blank.' % capfirst(verbose_field_name) 176 | 177 | 178 | def _validate_database_engine(self, engine, verbose_field_name): 179 | """Validate tenant_code. If invalid, return a string error message.""" 180 | engine_choices = [item.lower() for item in self.engine_choices] 181 | if engine in engine_choices: 182 | pass 183 | else: 184 | return 'Error: That %s of database is not exists.' %engine 185 | if not engine: 186 | return '%s cannot be blank.' % capfirst(verbose_field_name) -------------------------------------------------------------------------------- /multi_tenant/tenant/management/commands/multimigrate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from importlib import import_module 3 | 4 | from django.apps import apps 5 | from django.core.management.base import CommandError, no_translations 6 | from django.core.management.sql import ( 7 | emit_post_migrate_signal, emit_pre_migrate_signal, 8 | ) 9 | from django.db import connections 10 | from django.db.migrations.autodetector import MigrationAutodetector 11 | from django.db.migrations.executor import MigrationExecutor 12 | from django.db.migrations.loader import AmbiguityError 13 | from django.db.migrations.state import ModelState, ProjectState 14 | from django.utils.module_loading import module_has_submodule 15 | 16 | from django.core.management.commands.migrate import Command as MigrateCommand 17 | 18 | 19 | class Command(MigrateCommand): 20 | help = "Updates database schema. Manages both apps with migrations and those without." 21 | requires_system_checks = [] 22 | 23 | def add_arguments(self, parser): 24 | parser.add_argument( 25 | '--all', action='store_true', 26 | help='migrate all database.', 27 | ) 28 | super().add_arguments(parser) 29 | 30 | 31 | @no_translations 32 | def handle(self, *args, **options): 33 | if options['all']: 34 | from multi_tenant.tenant import get_all_tenant_db 35 | dbs = list(get_all_tenant_db().keys()) 36 | 37 | for db in dbs: 38 | connections[db] 39 | self.execute_migrate(db, args, options) 40 | else: 41 | cur_database = options['database'] 42 | if not cur_database in connections: 43 | connections[cur_database] 44 | self.execute_migrate(cur_database, args, options) 45 | 46 | def execute_migrate(self, cur_database, args, options): 47 | if not options['skip_checks']: 48 | self.check(databases=[cur_database]) 49 | 50 | self.verbosity = options['verbosity'] 51 | self.interactive = options['interactive'] 52 | 53 | # Import the 'management' module within each installed app, to register 54 | # dispatcher events. 55 | for app_config in apps.get_app_configs(): 56 | if module_has_submodule(app_config.module, "management"): 57 | import_module('.management', app_config.name) 58 | 59 | # Get the database we're operating from 60 | 61 | connection = connections[cur_database] 62 | # Hook for backends needing any database preparation 63 | connection.prepare_database() 64 | # Work out which apps have migrations and which do not 65 | executor = MigrationExecutor(connection, self.migration_progress_callback) 66 | 67 | # Raise an error if any migrations are applied before their dependencies. 68 | executor.loader.check_consistent_history(connection) 69 | 70 | # Before anything else, see if there's conflicting apps and drop out 71 | # hard if there are any 72 | conflicts = executor.loader.detect_conflicts() 73 | if conflicts: 74 | name_str = "; ".join( 75 | "%s in %s" % (", ".join(names), app) 76 | for app, names in conflicts.items() 77 | ) 78 | raise CommandError( 79 | "Conflicting migrations detected; multiple leaf nodes in the " 80 | "migration graph: (%s).\nTo fix them run " 81 | "'python manage.py makemigrations --merge'" % name_str 82 | ) 83 | 84 | # If they supplied command line arguments, work out what they mean. 85 | run_syncdb = options['run_syncdb'] 86 | target_app_labels_only = True 87 | if options['app_label']: 88 | # Validate app_label. 89 | app_label = options['app_label'] 90 | try: 91 | apps.get_app_config(app_label) 92 | except LookupError as err: 93 | raise CommandError(str(err)) 94 | if run_syncdb: 95 | if app_label in executor.loader.migrated_apps: 96 | raise CommandError("Can't use run_syncdb with app '%s' as it has migrations." % app_label) 97 | elif app_label not in executor.loader.migrated_apps: 98 | raise CommandError("App '%s' does not have migrations." % app_label) 99 | 100 | if options['app_label'] and options['migration_name']: 101 | migration_name = options['migration_name'] 102 | if migration_name == "zero": 103 | targets = [(app_label, None)] 104 | else: 105 | try: 106 | migration = executor.loader.get_migration_by_prefix(app_label, migration_name) 107 | except AmbiguityError: 108 | raise CommandError( 109 | "More than one migration matches '%s' in app '%s'. " 110 | "Please be more specific." % 111 | (migration_name, app_label) 112 | ) 113 | except KeyError: 114 | raise CommandError("Cannot find a migration matching '%s' from app '%s'." % ( 115 | migration_name, app_label)) 116 | targets = [(app_label, migration.name)] 117 | target_app_labels_only = False 118 | elif options['app_label']: 119 | targets = [key for key in executor.loader.graph.leaf_nodes() if key[0] == app_label] 120 | else: 121 | targets = executor.loader.graph.leaf_nodes() 122 | 123 | plan = executor.migration_plan(targets) 124 | exit_dry = plan and options['check_unapplied'] 125 | 126 | if options['plan']: 127 | self.stdout.write('Planned operations:', self.style.MIGRATE_LABEL) 128 | if not plan: 129 | self.stdout.write(' No planned migration operations.') 130 | for migration, backwards in plan: 131 | self.stdout.write(str(migration), self.style.MIGRATE_HEADING) 132 | for operation in migration.operations: 133 | message, is_error = self.describe_operation(operation, backwards) 134 | style = self.style.WARNING if is_error else None 135 | self.stdout.write(' ' + message, style) 136 | if exit_dry: 137 | sys.exit(1) 138 | return 139 | if exit_dry: 140 | sys.exit(1) 141 | 142 | # At this point, ignore run_syncdb if there aren't any apps to sync. 143 | run_syncdb = options['run_syncdb'] and executor.loader.unmigrated_apps 144 | # Print some useful info 145 | if self.verbosity >= 1: 146 | self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:")) 147 | if run_syncdb: 148 | if options['app_label']: 149 | self.stdout.write( 150 | self.style.MIGRATE_LABEL(" Synchronize unmigrated app: %s" % app_label) 151 | ) 152 | else: 153 | self.stdout.write( 154 | self.style.MIGRATE_LABEL(" Synchronize unmigrated apps: ") + 155 | (", ".join(sorted(executor.loader.unmigrated_apps))) 156 | ) 157 | if target_app_labels_only: 158 | self.stdout.write( 159 | self.style.MIGRATE_LABEL(" Apply all migrations: ") + 160 | (", ".join(sorted({a for a, n in targets})) or "(none)") 161 | ) 162 | else: 163 | if targets[0][1] is None: 164 | self.stdout.write( 165 | self.style.MIGRATE_LABEL(' Unapply all migrations: ') + 166 | str(targets[0][0]) 167 | ) 168 | else: 169 | self.stdout.write(self.style.MIGRATE_LABEL( 170 | " Target specific migration: ") + "%s, from %s" 171 | % (targets[0][1], targets[0][0]) 172 | ) 173 | 174 | pre_migrate_state = executor._create_project_state(with_applied_migrations=True) 175 | pre_migrate_apps = pre_migrate_state.apps 176 | emit_pre_migrate_signal( 177 | self.verbosity, self.interactive, connection.alias, apps=pre_migrate_apps, plan=plan, 178 | ) 179 | 180 | # Run the syncdb phase. 181 | if run_syncdb: 182 | if self.verbosity >= 1: 183 | self.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:")) 184 | if options['app_label']: 185 | self.sync_apps(connection, [app_label]) 186 | else: 187 | self.sync_apps(connection, executor.loader.unmigrated_apps) 188 | 189 | # Migrate! 190 | if self.verbosity >= 1: 191 | self.stdout.write(self.style.MIGRATE_HEADING("Running migrations:")) 192 | if not plan: 193 | if self.verbosity >= 1: 194 | self.stdout.write(" No migrations to apply.") 195 | # If there's changes that aren't in migrations yet, tell them how to fix it. 196 | autodetector = MigrationAutodetector( 197 | executor.loader.project_state(), 198 | ProjectState.from_apps(apps), 199 | ) 200 | changes = autodetector.changes(graph=executor.loader.graph) 201 | if changes: 202 | self.stdout.write(self.style.NOTICE( 203 | " Your models in app(s): %s have changes that are not " 204 | "yet reflected in a migration, and so won't be " 205 | "applied." % ", ".join(repr(app) for app in sorted(changes)) 206 | )) 207 | self.stdout.write(self.style.NOTICE( 208 | " Run 'manage.py makemigrations' to make new " 209 | "migrations, and then re-run 'manage.py migrate' to " 210 | "apply them." 211 | )) 212 | fake = False 213 | fake_initial = False 214 | else: 215 | fake = options['fake'] 216 | fake_initial = options['fake_initial'] 217 | post_migrate_state = executor.migrate( 218 | targets, plan=plan, state=pre_migrate_state.clone(), fake=fake, 219 | fake_initial=fake_initial, 220 | ) 221 | # post_migrate signals have access to all models. Ensure that all models 222 | # are reloaded in case any are delayed. 223 | post_migrate_state.clear_delayed_apps_cache() 224 | post_migrate_apps = post_migrate_state.apps 225 | 226 | # Re-render models of real apps to include relationships now that 227 | # we've got a final state. This wouldn't be necessary if real apps 228 | # models were rendered with relationships in the first place. 229 | with post_migrate_apps.bulk_update(): 230 | model_keys = [] 231 | for model_state in post_migrate_apps.real_models: 232 | model_key = model_state.app_label, model_state.name_lower 233 | model_keys.append(model_key) 234 | post_migrate_apps.unregister_model(*model_key) 235 | post_migrate_apps.render_multiple([ 236 | ModelState.from_model(apps.get_model(*model)) for model in model_keys 237 | ]) 238 | 239 | # Send the post_migrate signal, so individual apps can do whatever they need 240 | # to do at this point. 241 | emit_post_migrate_signal( 242 | self.verbosity, self.interactive, connection.alias, apps=post_migrate_apps, plan=plan, 243 | ) -------------------------------------------------------------------------------- /multi_tenant/tenant/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/tenant/middleware/__init__.py -------------------------------------------------------------------------------- /multi_tenant/tenant/middleware/authentication.py: -------------------------------------------------------------------------------- 1 | from multi_tenant.local import set_current_db 2 | from django.http.request import HttpRequest 3 | from django.contrib.auth.middleware import AuthenticationMiddleware, RemoteUserMiddleware, PersistentRemoteUserMiddleware 4 | 5 | class MultTenantAuthenticationMiddleware(AuthenticationMiddleware): 6 | def process_request(self, request:HttpRequest): 7 | super().process_request(request) 8 | if hasattr(request,'user'): 9 | user = request.user 10 | if not user.is_anonymous and user.tenant: 11 | code = user.tenant.code 12 | set_current_db(code) 13 | 14 | 15 | class MultTenantRemoteUserMiddleware(RemoteUserMiddleware): 16 | def process_request(self, request:HttpRequest): 17 | super().process_request(request) 18 | if hasattr(request,'user'): 19 | user = request.user 20 | if not user.is_anonymous and user.tenant: 21 | code = user.tenant.code 22 | set_current_db(code) 23 | 24 | 25 | class MultTenantPersistentRemoteUserMiddleware(PersistentRemoteUserMiddleware): 26 | force_logout_if_no_header = False 27 | -------------------------------------------------------------------------------- /multi_tenant/tenant/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-09-09 21:38 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import multi_tenant.tenant.models.user 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Tenant', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('create_date', models.DateTimeField(auto_now_add=True)), 21 | ('name', models.CharField(max_length=20, unique=True)), 22 | ('label', models.CharField(max_length=200)), 23 | ('code', models.CharField(max_length=10, unique=True)), 24 | ('db_password', models.CharField(blank=True, max_length=128, null=True)), 25 | ('db_name', models.CharField(max_length=50)), 26 | ('engine', models.CharField(choices=[('Mysql', 'Mysql'), ('SQLite', 'SQLite'), ('Postgres', 'Postgres'), ('Oracle', 'Oracle')], max_length=10, null=True)), 27 | ('options', models.JSONField(blank=True, null=True)), 28 | ('is_active', models.BooleanField(default=True)), 29 | ], 30 | options={ 31 | 'verbose_name': '租户', 32 | 'verbose_name_plural': '租户', 33 | 'db_table': 'auth_tenant', 34 | 'abstract': False, 35 | }, 36 | ), 37 | migrations.CreateModel( 38 | name='GlobalUser', 39 | fields=[ 40 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 41 | ('username', models.CharField(max_length=50, unique=True)), 42 | ('password', models.CharField(max_length=128)), 43 | ('is_active', models.BooleanField(default=True)), 44 | ('is_staff', models.BooleanField(default=False)), 45 | ('is_super', models.BooleanField(default=False)), 46 | ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tenant.tenant', to_field='code')), 47 | ], 48 | options={ 49 | 'verbose_name': '全局用户', 50 | 'verbose_name_plural': '全局用户', 51 | 'abstract': False, 52 | }, 53 | managers=[ 54 | ('objects', multi_tenant.tenant.models.user.UserManager()), 55 | ], 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /multi_tenant/tenant/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/tenant/migrations/__init__.py -------------------------------------------------------------------------------- /multi_tenant/tenant/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .tenant import AbstractTenant, Tenant 2 | from .user import AbstractGlobalUser, GlobalUser -------------------------------------------------------------------------------- /multi_tenant/tenant/models/tenant.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Tuple 3 | from django.db import models 4 | from multi_tenant.tenant.utils.pycrypt import crypt 5 | from multi_tenant.const import DEFAULT_DB_ENGINE_MAP 6 | from django.conf import settings 7 | 8 | 9 | DAFAULT_DB = settings.DATABASES['default'] 10 | 11 | 12 | class TenantManager(models.Manager): 13 | def create_tenant(self, code, name,**kwargs): 14 | if not code: 15 | raise ValueError('The given code must be set') 16 | if not name: 17 | raise ValueError('The given name must be set') 18 | password = kwargs.pop('db_password',None) 19 | tenant = self.model(code=code, name=name, **kwargs) 20 | if password: 21 | tenant.db_password = crypt.encrypt(password) 22 | tenant.save(using=self._db) 23 | return tenant 24 | 25 | class AbstractTenant(models.Model): 26 | Mysql, SQLite, Postgres, Oracle = ('Mysql', 'SQLite3', 'Postgres', 'Oracle') 27 | engine_choices = ( 28 | (Mysql, Mysql), 29 | (SQLite, SQLite), 30 | (Postgres, Postgres), 31 | (Oracle, Oracle), 32 | ) 33 | create_date: datetime = models.DateTimeField(auto_now_add=True) 34 | name: str = models.CharField(max_length=20, unique=True) 35 | label: str = models.CharField(max_length=200) 36 | code: str = models.CharField(max_length=10, unique=True) 37 | db_password: str = models.CharField(max_length=128, null=True, blank=True) 38 | db_name: str = models.CharField(max_length=50) 39 | engine: str = models.CharField(max_length=10, null=True, blank=True, choices=engine_choices) 40 | options: str = models.JSONField(null=True, blank=True) 41 | is_active: bool = models.BooleanField(default=True) 42 | _password = None 43 | CODE_FIED = 'code' 44 | objects = TenantManager() 45 | 46 | def __str__(self) -> str: 47 | return self.name 48 | 49 | def save(self, *args, **kwargs): 50 | super().save(*args, **kwargs) 51 | if self._password is not None: 52 | if self.db_password: 53 | raw_password = crypt.encrypt(self.db_password) 54 | self.db_password = raw_password 55 | self._password = None 56 | self.save() 57 | 58 | def delete(self, using: str=None, keep_parents: bool=False, force: bool = False) -> Tuple[int, Dict[str, int]]: 59 | if force: 60 | super().delete(using,keep_parents) 61 | else: 62 | raise PermissionError(f'{self.code} can not delete') 63 | 64 | def create_database(self) -> bool: 65 | from multi_tenant.tenant.utils.db import MutlTenantOriginConnection 66 | if self.engine.lower() == self.SQLite.lower(): 67 | connection = MutlTenantOriginConnection().create_connection(tentant=self, popname=False) 68 | return True 69 | elif self.engine.lower() == self.Postgres.lower(): 70 | connection = MutlTenantOriginConnection().create_connection(tentant=self, popname=True, **{'NAME':'postgres'}) 71 | else: 72 | connection = MutlTenantOriginConnection().create_connection(tentant=self, popname=True) 73 | create_database_sql = self.create_database_sql 74 | 75 | if create_database_sql: 76 | with connection.cursor() as cursor: 77 | cursor.execute(create_database_sql) 78 | return True 79 | 80 | class Meta: 81 | db_table = 'auth_tenant' 82 | verbose_name = '租户' 83 | verbose_name_plural = '租户' 84 | abstract = True 85 | 86 | 87 | def get_db_config(self) -> Dict: 88 | if self.engine: 89 | engine_name = self.engine.lower() 90 | else: 91 | default_engine = DAFAULT_DB['ENGINE'] 92 | engine_name = self.inject_engine(default_engine) 93 | if hasattr(self,f'_create_{engine_name}_dbconfig'): 94 | return self.configure_settings(getattr(self,f'_create_{engine_name}_dbconfig')()) 95 | 96 | else: 97 | raise NotImplementedError(f'create_{engine_name}_dbconfig is not implemente') 98 | @staticmethod 99 | def inject_engine(name): 100 | for key ,value in DEFAULT_DB_ENGINE_MAP.items(): 101 | if name == value: 102 | return key 103 | def _create_common_dbconfig(self) -> Dict: 104 | password = DAFAULT_DB['PASSWORD'] 105 | engine = self.get_engine() 106 | options = self.options 107 | if not self.options: 108 | options = dict() 109 | 110 | if self.db_password: 111 | password = crypt.decrypt(self.db_password) 112 | return { 113 | 'ENGINE': engine, 114 | 'NAME': self.db_name, 115 | 'USER': options.pop('user', DAFAULT_DB['USER']), 116 | 'PASSWORD': password, 117 | 'HOST': options.pop('host', DAFAULT_DB['HOST']), 118 | 'PORT': options.pop('port', DAFAULT_DB['PORT']), 119 | **options 120 | } 121 | 122 | 123 | def _create_sqlite3_dbconfig(self) -> Dict: 124 | engine = self.get_engine() 125 | 126 | return { 127 | 'ENGINE': engine, 128 | 'NAME': settings.BASE_DIR.joinpath(self.db_name) 129 | } 130 | 131 | 132 | def _create_mysql_dbconfig(self) -> Dict: 133 | return self._create_common_dbconfig() 134 | 135 | 136 | def _create_postgres_dbconfig(self) -> Dict: 137 | return self._create_common_dbconfig() 138 | 139 | def _create_oracle_dbconfig(self,) -> Dict: 140 | return self._create_common_dbconfig() 141 | 142 | 143 | def get_engine(self) -> str: 144 | engine = DAFAULT_DB['ENGINE'] 145 | if self.engine: 146 | engine = DEFAULT_DB_ENGINE_MAP.get(self.engine.lower()) 147 | if not engine: 148 | raise ValueError(f'unkown engine {self.engine}, engine must be in {list(DEFAULT_DB_ENGINE_MAP.keys())}') 149 | return engine 150 | 151 | 152 | @property 153 | def create_database_sql(self) -> str: 154 | engine_name = self.engine.lower() 155 | if hasattr(self,f'_create_{engine_name}_database'): 156 | return getattr(self,f'_create_{engine_name}_database')() 157 | else: 158 | raise NotImplementedError(f'_create_{engine_name}_database is not implemente') 159 | 160 | def _create_sqlite3_database(self) -> str: 161 | pass 162 | 163 | def _create_mysql_database(self) -> str: 164 | return f"CREATE DATABASE IF NOT EXISTS {self.db_name} character set utf8;" 165 | 166 | def _create_postgres_database(self) -> str: 167 | return f"CREATE DATABASE \"{self.db_name}\" encoding 'UTF8';" 168 | 169 | def _create_oracle_database(self) -> str: 170 | 171 | return f"CREATE DATABASE {self.db_name} DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;" 172 | 173 | def configure_settings(self, conn): 174 | conn.setdefault('ATOMIC_REQUESTS', False) 175 | conn.setdefault('AUTOCOMMIT', True) 176 | conn.setdefault('ENGINE', 'django.db.backends.dummy') 177 | if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']: 178 | conn['ENGINE'] = 'django.db.backends.dummy' 179 | conn.setdefault('CONN_MAX_AGE', 0) 180 | conn.setdefault("CONN_HEALTH_CHECKS", False) 181 | conn.setdefault('OPTIONS', {}) 182 | conn.setdefault('TIME_ZONE', None) 183 | for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']: 184 | conn.setdefault(setting, '') 185 | return conn 186 | 187 | class Tenant(AbstractTenant): 188 | pass 189 | -------------------------------------------------------------------------------- /multi_tenant/tenant/models/user.py: -------------------------------------------------------------------------------- 1 | import unicodedata 2 | from django.db import models 3 | from django.utils.crypto import get_random_string 4 | from django.apps import apps 5 | from django.conf import settings 6 | from django.contrib.auth.hashers import ( 7 | check_password, make_password, 8 | ) 9 | from multi_tenant.tenant import get_common_apps, get_tenant_model, get_tenant_user_model 10 | 11 | 12 | 13 | class BaseUserManager(models.Manager): 14 | 15 | def make_random_password(self, length:int=10, 16 | allowed_chars:str='abcdefghjkmnpqrstuvwxyz' 17 | 'ABCDEFGHJKLMNPQRSTUVWXYZ' 18 | '23456789') -> str: 19 | """ 20 | Generate a random password with the given length and given 21 | allowed_chars. The default value of allowed_chars does not have "I" or 22 | "O" or letters and digits that look similar -- just to avoid confusion. 23 | """ 24 | return get_random_string(length, allowed_chars) 25 | 26 | def get_by_natural_key(self, username:str) ->str: 27 | return self.get(**{self.model.USERNAME_FIELD: username}) 28 | 29 | 30 | 31 | class UserManager(BaseUserManager): 32 | use_in_migrations = True 33 | 34 | def _create_user(self, username:str, password:str, **extra_fields): 35 | if not username: 36 | raise ValueError('The given username must be set') 37 | # Lookup the real model class from the global app registry so this 38 | # manager method can be used in migrations. This is fine because 39 | # managers are by definition working on the real model. 40 | GlobalUserModel = apps.get_model(self.model._meta.app_label, self.model._meta.object_name) 41 | username = GlobalUserModel.normalize_username(username) 42 | user = self.model(username=username, **extra_fields) 43 | user.password = make_password(password) 44 | user.save(using=self._db) 45 | return user 46 | 47 | def create_user(self, username, password=None, **extra_fields): 48 | extra_fields.setdefault('is_active', True) 49 | return self._create_user(username, password, **extra_fields) 50 | 51 | def create_superuser(self, username, password=None, **extra_fields): 52 | extra_fields.setdefault('is_active', True) 53 | extra_fields.setdefault('is_staff', True) 54 | extra_fields.setdefault('is_super', True) 55 | return self._create_user(username, password, **extra_fields) 56 | 57 | class AbstractGlobalUser(models.Model): 58 | Tenant = get_tenant_model() 59 | username = models.CharField(max_length=50, unique=True) 60 | password = models.CharField(max_length=128) 61 | is_active = models.BooleanField(default=True) 62 | is_staff = models.BooleanField(default=False) 63 | is_super = models.BooleanField(default=False) 64 | tenant = models.ForeignKey(Tenant,to_field='code',on_delete=models.CASCADE, null=True, blank=True) 65 | USERNAME_FIELD = 'username' 66 | REQUIRED_FIELDS = ['password'] 67 | PASSWORD_FIELD = 'password' 68 | _password = None 69 | 70 | objects = UserManager() 71 | 72 | def __str__(self) -> str: 73 | return f'{self.username}' 74 | 75 | 76 | class Meta: 77 | verbose_name = '全局用户' 78 | verbose_name_plural = '全局用户' 79 | abstract = True 80 | 81 | @property 82 | def is_anonymous(self) -> bool: 83 | return False 84 | 85 | @property 86 | def is_authenticated(self) -> bool: 87 | return True 88 | 89 | 90 | def get_username(self) -> bool: 91 | if hasattr(self,self.USERNAME_FIELD): 92 | return getattr(self, self.USERNAME_FIELD) 93 | @property 94 | def is_superuser(self) -> bool: 95 | return self.is_super 96 | 97 | 98 | @classmethod 99 | def normalize_username(cls, username:str): 100 | return unicodedata.normalize('NFKC', username) if isinstance(username, str) else username 101 | 102 | def check_password(self, raw_password:str): 103 | """ 104 | Return a boolean of whether the raw_password was correct. Handles 105 | hashing formats behind the scenes. 106 | """ 107 | def setter(raw_password): 108 | self.set_password(raw_password) 109 | # Password hash upgrades shouldn't be considered password changes. 110 | self._password = None 111 | self.save(update_fields=["password"]) 112 | return check_password(raw_password, self.password, setter) 113 | 114 | 115 | def set_password(self, raw_password:str): 116 | self.password = make_password(raw_password) 117 | self._password = raw_password 118 | 119 | def has_module_perms(self, app_label:str) -> bool: 120 | common_applist = get_common_apps() 121 | if self.tenant: 122 | if app_label in common_applist: 123 | return False 124 | else: 125 | return True 126 | else: 127 | if app_label in common_applist and self.is_super: 128 | return True 129 | else: 130 | return False 131 | 132 | def has_perm(self, permission:str) -> bool: 133 | TenantUser = get_tenant_user_model() 134 | if self.tenant: 135 | try: 136 | tenant_user = TenantUser.objects.using(self.tenant.code).get(username=self.username) 137 | all_permissions = tenant_user.get_all_permissions() 138 | if permission in all_permissions: 139 | result = tenant_user.has_perm(permission) 140 | return result 141 | else: 142 | return False 143 | except Exception as e: 144 | print(e) 145 | return False 146 | else: 147 | True 148 | 149 | return True 150 | 151 | 152 | class GlobalUser(AbstractGlobalUser): 153 | pass -------------------------------------------------------------------------------- /multi_tenant/tenant/patch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/tenant/patch/__init__.py -------------------------------------------------------------------------------- /multi_tenant/tenant/patch/connection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.db.utils import ConnectionHandler 3 | from multi_tenant.tenant import get_tenant_db 4 | logger = logging.getLogger('django.db.backends') 5 | 6 | def __connection_handler__getitem__(self, alias: str) -> ConnectionHandler: 7 | if isinstance(alias, str): 8 | try: 9 | return getattr(self._connections, alias) 10 | except AttributeError: 11 | if alias not in self.settings: 12 | tenant_db = get_tenant_db(alias) 13 | if tenant_db: 14 | self.settings[alias] = tenant_db 15 | else: 16 | logger.error(f"The connection '{alias}' doesn't exist.") 17 | raise self.exception_class(f"The connection '{alias}' doesn't exist.") 18 | conn = self.create_connection(alias) 19 | setattr(self._connections, alias, conn) 20 | return conn 21 | 22 | else: 23 | logger.error(f'The connection alias [{alias}] must be string') 24 | raise Exception(f'The connection alias [{alias}] must be string') 25 | 26 | ConnectionHandler.__getitem__ = __connection_handler__getitem__ -------------------------------------------------------------------------------- /multi_tenant/tenant/patch/contenttype.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps as global_apps 2 | from django.conf import settings 3 | from django.contrib.contenttypes import management 4 | from django.contrib.contenttypes.management import get_contenttypes_and_models 5 | from django.db import DEFAULT_DB_ALIAS 6 | 7 | from multi_tenant.tenant import get_common_apps 8 | 9 | 10 | def create_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs): 11 | """ 12 | Create content types for models in the given app. 13 | """ 14 | if not app_config.models_module: 15 | return 16 | 17 | app_label = app_config.label 18 | try: 19 | app_config = apps.get_app_config(app_label) 20 | ContentType = apps.get_model('contenttypes', 'ContentType') 21 | except LookupError: 22 | return 23 | 24 | 25 | common_applist = get_common_apps() 26 | if app_config.name in common_applist: 27 | return None 28 | 29 | content_types, app_models = get_contenttypes_and_models(app_config, using, ContentType) 30 | if not app_models: 31 | return 32 | 33 | cts = [ 34 | ContentType( 35 | app_label=app_label, 36 | model=model_name, 37 | ) 38 | for (model_name, model) in app_models.items() 39 | if model_name not in content_types 40 | ] 41 | ContentType.objects.using(using).bulk_create(cts) 42 | if verbosity >= 2: 43 | for ct in cts: 44 | print("Adding content type '%s | %s'" % (ct.app_label, ct.model)) 45 | 46 | management.create_contenttypes = create_contenttypes -------------------------------------------------------------------------------- /multi_tenant/tenant/patch/permission.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates permissions for all installed apps that need permissions. 3 | """ 4 | 5 | from django.apps import apps as global_apps 6 | from django.contrib.contenttypes.management import create_contenttypes 7 | from django.db import DEFAULT_DB_ALIAS, router 8 | from django.contrib.auth.models import Permission 9 | from django.contrib.auth.management import _get_all_permissions 10 | from django.contrib.auth import management 11 | from django.contrib.auth import backends 12 | from django.conf import settings 13 | 14 | from multi_tenant.tenant import get_common_apps 15 | 16 | 17 | def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs): 18 | if not app_config.models_module: 19 | return 20 | 21 | create_contenttypes(app_config, verbosity=verbosity, interactive=interactive, using=using, apps=apps, **kwargs) 22 | 23 | app_label = app_config.label 24 | try: 25 | app_config = apps.get_app_config(app_label) 26 | ContentType = apps.get_model('contenttypes', 'ContentType') 27 | Permission = apps.get_model('auth', 'Permission') 28 | except LookupError: 29 | return 30 | 31 | common_applist = get_common_apps() 32 | if app_config.name in common_applist: 33 | return 34 | 35 | if not router.allow_migrate_model(using, Permission): 36 | return 37 | 38 | # This will hold the permissions we're looking for as 39 | # (content_type, (codename, name)) 40 | searched_perms = [] 41 | # The codenames and ctypes that should exist. 42 | ctypes = set() 43 | 44 | for klass in app_config.get_models(): 45 | # Force looking up the content types in the current database 46 | # before creating foreign keys to them. 47 | 48 | ctype = ContentType.objects.db_manager(using).get_for_model(klass, for_concrete_model=False) 49 | 50 | ctypes.add(ctype) 51 | for perm in _get_all_permissions(klass._meta): 52 | searched_perms.append((ctype, perm)) 53 | 54 | # Find all the Permissions that have a content_type for a model we're 55 | # looking for. We don't need to check for codenames since we already have 56 | # a list of the ones we're going to create. 57 | all_perms = set(Permission.objects.using(using).filter( 58 | content_type__in=ctypes, 59 | ).values_list( 60 | "content_type", "codename" 61 | )) 62 | 63 | perms = [ 64 | Permission(codename=codename, name=name, content_type_id=ct.id) 65 | for ct, (codename, name) in searched_perms 66 | if (ct.pk, codename) not in all_perms 67 | ] 68 | Permission.objects.using(using).bulk_create(perms) 69 | if verbosity >= 2: 70 | for perm in perms: 71 | print("Adding permission '%s'" % perm) 72 | 73 | 74 | def _get_group_permissions(self, user_obj): 75 | 76 | user_groups_field = user_obj._meta.get_field('groups') 77 | user_groups_query = 'group__%s' % user_groups_field.related_query_name() 78 | return Permission.objects.filter(**{user_groups_query: user_obj}) 79 | 80 | 81 | backends.ModelBackend._get_group_permissions = _get_group_permissions 82 | 83 | management.create_permissions = create_permissions -------------------------------------------------------------------------------- /multi_tenant/tenant/patch/user.py: -------------------------------------------------------------------------------- 1 | from django.core import checks 2 | from django.db.models.query_utils import DeferredAttribute 3 | from django.db.models.signals import post_migrate 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from django.contrib.auth.models import AbstractUser, User 7 | # from django.contrib.auth import get_user_model 8 | from django.contrib.auth.checks import check_models_permissions, check_user_model 9 | # from django.contrib.auth.signals import user_logged_in 10 | 11 | from django.contrib.auth.apps import AuthConfig 12 | 13 | 14 | 15 | 16 | class Meta(AbstractUser.Meta): 17 | swappable = 'AUTH_TENANT_USER_MODEL' 18 | 19 | 20 | def ready(self): 21 | from django.contrib.auth.management import create_permissions 22 | post_migrate.connect( 23 | create_permissions, 24 | dispatch_uid="django.contrib.auth.management.create_permissions" 25 | ) 26 | checks.register(check_user_model, checks.Tags.models) 27 | checks.register(check_models_permissions, checks.Tags.models) 28 | 29 | User.Meta = Meta 30 | AuthConfig.ready = ready 31 | 32 | -------------------------------------------------------------------------------- /multi_tenant/tenant/signal.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from multi_tenant.tenant.models import Tenant, GlobalUser 3 | from django.db.models.signals import post_save # 另外一个内置的常用信号 4 | import logging 5 | from django.dispatch import receiver 6 | from multi_tenant.tenant import get_tenant_model, get_tenant_user_model 7 | 8 | 9 | Tenant = get_tenant_model() 10 | logger = logging.getLogger('django.request') 11 | 12 | 13 | @receiver(post_save, sender=Tenant) 14 | def create_data_handler(sender, signal, instance, created, **kwargs): 15 | if created: 16 | try: 17 | instance.create_database() 18 | logger.info(f'create database : [{instance.db_name}] successfuly for {instance.code}') 19 | thread = Thread(target=migrate,args=[instance.code]) 20 | thread.start() 21 | except Exception as e: 22 | logger.error(e) 23 | instance.delete(force=True) 24 | 25 | 26 | 27 | def migrate(database: str): 28 | try: 29 | from django.core.management import execute_from_command_line 30 | except ImportError as exc: 31 | logger.error('migrate fail') 32 | raise ImportError( 33 | "Couldn't import Django. Are you sure it's installed and " 34 | "available on your PYTHONPATH environment variable? Did you " 35 | "forget to activate a virtual environment?" 36 | ) from exc 37 | execute_from_command_line(['manage.py', 'migrate', f'--database={database}']) 38 | logger.info('migrate successfuly!') 39 | 40 | @receiver(post_save, sender=GlobalUser) 41 | def assign_user_handler(sender, signal, instance, created, **kwargs): 42 | if instance.tenant: 43 | TenantUser = get_tenant_user_model() 44 | TenantUser.objects.using(instance.tenant.code).get_or_create( 45 | defaults={ 46 | 'is_active':instance.is_active, 47 | 'is_staff':instance.is_staff, 48 | 'is_superuser':instance.is_superuser 49 | }, 50 | username=instance.username, 51 | 52 | ) 53 | -------------------------------------------------------------------------------- /multi_tenant/tenant/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /multi_tenant/tenant/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnsGoo/djangoMultiTenant/5b0ad3ea49c70efd55b37b23a1b822010905d2ef/multi_tenant/tenant/utils/__init__.py -------------------------------------------------------------------------------- /multi_tenant/tenant/utils/db.py: -------------------------------------------------------------------------------- 1 | from multi_tenant.tenant import get_tenant_model 2 | from django.db.utils import load_backend 3 | from django.db.models import Model 4 | from django.conf import settings 5 | from multi_tenant.local import get_current_db 6 | from django.db.utils import ConnectionHandler 7 | 8 | class MutlTenantOriginConnection(ConnectionHandler): 9 | ''' 10 | 创建原始数据库连接 11 | ''' 12 | 13 | Tenant = get_tenant_model() 14 | def create_connection(self, tentant:Tenant, popname: bool=False,**kwargs): 15 | ''' 16 | 根据租户信息创建原始数据库连接 17 | ''' 18 | engine = tentant.get_engine() 19 | alias = tentant.code 20 | db_config = tentant.get_db_config() 21 | if popname: 22 | db_config['NAME'] = None 23 | conn = { **db_config, **kwargs} 24 | backend = load_backend(engine) 25 | return backend.DatabaseWrapper(conn, alias) 26 | 27 | 28 | class MultTenantDBRouter: 29 | def db_for_read(self, model:Model, **hints) -> str: 30 | if model._meta.app_label in settings.DATABASE_APPS_MAPPING: 31 | return settings.DATABASE_APPS_MAPPING[model._meta.app_label] 32 | 33 | return get_current_db() 34 | 35 | def db_for_write(self, model:Model, **hints): 36 | if model._meta.app_label in settings.DATABASE_APPS_MAPPING: 37 | return settings.DATABASE_APPS_MAPPING[model._meta.app_label] 38 | 39 | return get_current_db() 40 | 41 | def allow_migrate(self, db:str, app_label:str, **hints) -> bool: 42 | if app_label == 'contenttypes': 43 | return True 44 | app_db = settings.DATABASE_APPS_MAPPING.get(app_label) 45 | if app_db == 'default' and db == 'default': 46 | return True 47 | elif app_db != 'default' and db != 'default': 48 | return True 49 | else: 50 | return False 51 | -------------------------------------------------------------------------------- /multi_tenant/tenant/utils/pycrypt.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from Crypto.Cipher import AES 3 | from Crypto.Util import Padding 4 | from django.conf import settings 5 | 6 | SECRET_KEY = settings.SECRET_KEY[33:-1] 7 | 8 | class Crypt: 9 | 10 | def encrypt(self, clear_text:str) -> str: 11 | """ 12 | 加密 13 | """ 14 | aes = AES.new(SECRET_KEY.encode(),AES.MODE_ECB) 15 | text_b = clear_text.encode() 16 | pad_text = Padding.pad(text_b,AES.block_size,style='pkcs7') 17 | encrypt_text = aes.encrypt(pad_text) 18 | encrypt_text = base64.b64encode(encrypt_text).decode() 19 | return encrypt_text 20 | 21 | 22 | def decrypt(self, cipher_text: str) -> str: 23 | """ 24 | 解密 25 | """ 26 | aes = AES.new(SECRET_KEY.encode(),AES.MODE_ECB) 27 | text = base64.b64decode(cipher_text.encode()) 28 | plain_text = aes.decrypt(text) 29 | # pad后的数据可能在末尾加了字符,避免影响json识别,需进行unpad。 30 | plain_text = Padding.unpad(plain_text, AES.block_size, style='pkcs7').decode() 31 | return plain_text 32 | 33 | 34 | crypt = Crypt() 35 | 36 | 37 | __all__ = ['crypt'] -------------------------------------------------------------------------------- /multi_tenant/tenant/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.rst', 'r', encoding='utf-8') as fh: 4 | long_description = fh.read() 5 | setup( 6 | name='django-multi-tenancy', 7 | version='0.1.3', 8 | keywords=['python', 'django','mult-tenant'], 9 | description='Django multi tenant implementation', 10 | long_description=long_description, 11 | long_description_content_type='text/markdown', 12 | license='MIT Licence', 13 | url='https://github.com/AnsGoo/djangoMultiTenant', 14 | author='ansgoo', 15 | author_email='haiven_123@163.com', 16 | packages=find_packages(), 17 | include_package_data=True, 18 | platforms='any', 19 | install_requires=[ 20 | 'django>=3.2.0', 21 | 'pycryptodome>=3.10.1', 22 | ], 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3 :: Only', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Programming Language :: Python :: 3.8', 30 | 'Programming Language :: Python :: 3.9', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: Unix', 33 | 'Operating System :: POSIX :: Linux', 34 | 'Environment :: Console', 35 | 'Environment :: MacOS X', 36 | ] 37 | ) 38 | --------------------------------------------------------------------------------