Итак, я работаю над приложением Django, которое имеет следующие требования:
1 - Приложение должно иметь возможность обрабатывать мультитенантов (в нашем приложении мы называем их организациями) с отдельной базой данных для каждой организации / арендатора.
2 - Приложение может иметь общую базу данных для обработки модели User для каждой организации, таким образом, аутентификация и вход в систему осуществляется через эту общую базу данных.
3- После того, как новый администратор организации зарегистрирует новую учетную запись в нашем приложении, он / она должен иметь возможность - через форму - инициализировать создание новой базы данных для своей организации и, конечно же, перенесите эту базу данных и начните выполнять над ней операции CRUD.
Поскольку Django поддерживает multi базы данных, нет проблем с выполнением операций CRUD для любой модели, когда указывается, что django использует соответствующую базу данных.
А поскольку размещение всех пользователей в одной таблице в базе данных sahred и аутентификация их оттуда в порядке с требованиями нашего проекта, поэтому у меня нет проблем с тесно связанным сервером аутентификации с общей базой данных.
Проблема, с которой я сталкиваюсь, заключается в необходимом автоматическом создании новой базы данных для каждого администратора организации. Под автоматическим пониманием я подразумеваю, что администратор организации - это тот, кто запускает процесс создания / переноса базы данных без необходимости вмешательства наших разработчиков или системных администраторов.
Кроме того, Django должен быть «осведомлен» о каждой новой базе данных, созданной после ее создания, и начинает с ней соединение без перезапуска сервера django.
Итак, я нашел решение для этой проблемы, но это решение не упоминается в документации, и я не видел его реализованным где-либо в Интернете или SO.
Поэтому меня беспокоит, подходит ли это решение, или оно может содержать ошибки или какие-либо уязвимости в безопасности, поэтому я и прошу помощи.
Мое решение
Модель пользователя по умолчанию должна быть настроена таким образом, чтобы в качестве поля указывалось имя организации. Это будет использоваться позже в представлениях для указания django на правильную базу данных:
from django.contrib.auth.models import AbstractUser
...
class CustomUser(AbstractUser):
organization = models.CharField(max_length=255)
и, конечно, в settings.py
:
AUTH_USER_MODEL = 'base.CustomUser'
Диктовка Databases
будет содержать базу данных по умолчанию как обычно:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'db_name',
'USER': 'db_user',
'PASSWORD': 'db_pass',
'HOST': 'localhost',
'PORT': 5432,
}
}
Пока ничего необычного. Теперь нам нужно создать еще одну таблицу в общей базе данных для хранения списка баз данных организации вместе с их деталями:
class Organization(models.Model):
org_name = models.CharField(max_length=255)
django_db_name = models.CharField(max_length=255)
postgres_db_name = models.CharField(max_length=255)
Итак, теперь нам нужно, чтобы django узнал о новых базах данных, хранящихся в модели Organization
, всякий раз, когда он запускает свой сервер и пытается «переварить» файл settings.py
, как это всегда происходит в начале. Databases
диктуем добавить следующее:
from django.db.utils import ProgrammingError
from base.models import Organization
...
DATABASES = {
'default': {
...
}
}
try:
organizations = Organization.objects.all()
for organization in organizations:
# Dynamically adding the new databases instead of hard coding them in the settings file.
DATABASES[organization.django_db_name] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': organization.postgres_db_name,
'USER': 'db_user',
'PASSWORD': 'db_pass', # In production this should be encrypted and stored and retrieved from the shared db Organization table.
'HOST': 'localhost',
'PORT': 5432,
}
except ProgrammingError:
pass # The Organization table was not found in the db, i.e. hasn't been created yet.
До сих пор в приведенном выше коде нам удалось динамически добавлять новые базы данных в настройки Django без необходимости редактировать файл настроек каждый раз, когда мы добавляем новую базу данных.
Далее нам нужно реализовать способ автоматического создания и переноса базы данных, а также сохранить ее детали в модели Organization
, которая существует в общей базе данных для последующего извлечения.
Кроме того, нам нужен способ, чтобы django «знал» о вновь созданной базе данных и приказал ему установить соединение с ним без необходимости перезапуска сервера django.
Итак, мы создадим группу служебных функций, которые будут обрабатывать все это, давайте поместим это, например, в. utils/db.py
import psycopg2
import os
from base.models import Organization
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from django.db import connections
from django.db.backends.postgresql.base import DatabaseWrapper
def _create_new_db(postgres_db_name):
con = psycopg2.connect(dbname='db_name', user='db_user', password='db_pass', host='localhost', port=5432)
con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = con.cursor()
cur.execute('CREATE DATABASE {postgres_db_name} OWNER db_user'.format(postgres_db_name=postgres_db_name))
def _update_django_settings_and_migrate(django_db_name, postgres_db_name):
connections.databases[django_db_name] = connections.databases['default'] # copying the default db dict to be used as a template
connections.databases[django_db_name]['NAME'] = postgres_db_name
# This is the only way to make django aware of the new db, I came up with this through digging in the source code, there is no reference in the docs for this:
wrapper = DatabaseWrapper(connections.databases[django_db_name], django_db_name)
connections[django_db_name] = wrapper
# Now migrating the db
os.system('python manage.py migrate --database={}'.format(django_db_name))
def _store_new_settings_in_shared_db(org_name, django_db_name, postgres_db_name):
Organization(org_name=org_name, django_db_name=django_db_name, postgres_db_name=postgres_db_name).save(using='default')
def new_user_org_setup(org_name, django_db_name, postgres_db_name):
_create_new_db(postgres_db_name)
_store_new_settings_in_shared_db(org_name, django_db_name, postgres_db_name)
_update_django_settings_and_migrate(django_db_name, postgres_db_name)
Итак, теперь мы можем вызвать функцию new_user_org_setup
с нашей точки зрения так:
base/views.py
from django.contrib.auth import get_user_model
from django.shortcuts import redirect
from base.forms import UserForm
from utils.db import new_user_org_setup
...
def sign_up(request):
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
organization = form.cleaned_data.get('organization')
new_user_org_setup(org_name=organization, django_db_name=organization, postgres_db_name=organization)
user = get_user_model()(username=username)
user.set_password(password)
user.organization = organization
user.save()
return redirect('base:home')
else:
form = UserForm()
return render(request, 'base/sign_up.html', {'form':form})
Вышеуказанная настройка удовлетворяет всем необходимым требованиям, но, как я уже говорил, я боюсь неожиданного, поэтому, пожалуйста, если кто-то может указать на уязвимость или предложить лучший подход или улучшить мой код, я буду будь благодарен.