Django multi-tenancy с автоматическим созданием базы данных для каждого арендатора - PullRequest
1 голос
/ 04 апреля 2019

Итак, я работаю над приложением 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})

Вышеуказанная настройка удовлетворяет всем необходимым требованиям, но, как я уже говорил, я боюсь неожиданного, поэтому, пожалуйста, если кто-то может указать на уязвимость или предложить лучший подход или улучшить мой код, я буду будь благодарен.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...