Recently I downloaded the django_messages app (a private user to user messaging django app) and added it to my django project.
settings.py
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'mydjangoapp.mydjangoappdb',
'tastypie',
'gunicorn',
'south',
'relationships',
'pyapns',
'django_messages',
The app works great and plays well with Django. However for features such as messaging, the database can get quite large. I decided to create a dedicated database to store all the django_messages data.
settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'django_db',
'USER': 'django_login',
'PASSWORD': 'xxxx',
'HOST': '',
'PORT': '',
},
'message_db': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'django_messagedb',
'USER': 'django_login',
'PASSWORD': 'xxxx',
'HOST': 'XX.XXX.XX.XXX',
'PORT': '5432',
}
DATABASE_ROUTERS = ['mydjangoapp.messagerouter.MessageRouter']
And just for clarification, here is my messagerouter.py
class MessageRouter(object):
"""
A custom router written by Riegie to
control all database operations on models in the
django_messages application
"""
def db_for_read(self, model, **hints):
"""
Attempts to read django_messages models go to
message_db.
"""
if model._meta.app_label == 'django_messages':
return 'message_db'
return None
def db_for_write(self, model, **hints):
"""
Attempts to write django_messages models to go to
message_db.
"""
if model._meta.app_label == 'django_messages':
return 'message_db'
return None
def allow_relation(self, obj1, obj2, **hints):
"""
Allow relations if a model in the django_messages.
"""
if obj1._meta.app_label == 'django_messages' or \
obj2._meta.app_label == 'django_messages':
return True
return None
def allow_syncdb(self, db, model):
"""
Make sure the django_messages app only appears in the
'message_db" database.
"""
if db == 'message_db':
return model._meta.app_label == 'django_messages'
elif model._meta.app_label == 'django_messages':
return False
return None
As you can see, I have two databases, one on the local machine on which Django is running and another database on a remote machine. Out of the box, upon installation, django_messages naturally creates tables on the default database. However, I would like it to create tables only on the "message_db" database.
I've looked into the Multi-db Setup Django documentation, but it goes into specifics about Master/Slave configuration. I used the Auth Router example and created the messagerouter.py. Everything syncs up and I get no errors.
When I check the remote database however, the table isn't there! Why is that? Is it because it is not possible to have a remote foreign key table relationship to the Django User?
UPDATE
So I managed to sync the Django_messages app to the other database using the following command: ./manage.py syncdb --database=message_db. This is great. However when I access the app from the Django admin page, I am given the following error:
DatabaseError at /admin/django_messages/message/
relation "django_messages_message" does not exist
LINE 1: SELECT COUNT(*) FROM "django_messages_message"
I find this error odd because I can see the table on the other server through pgadmin III. So the syncing has worked correctly, but now it seems Django can't recognize that table. Am I doing something wrong with my messagerouter.py perhaps?
So after doing a lot of research, I finally came across this, I wish I had seen it earlier. Django doesn't support cross-database relations: https://docs.djangoproject.com/en/dev/topics/db/multi-db/#no-cross-database-relations
As it states:
Django doesn’t currently provide any support for foreign key or many-to-many relationships spanning multiple databases. If you have used a router to partition models to different databases, any foreign key and many-to-many relationships defined by those models must be internal to a single database.
This is because of referential integrity. In order to maintain a relationship between two objects, Django needs to know that the primary key of the related object is valid. If the primary key is stored on a separate database, it’s not possible to easily evaluate the validity of a primary key.
If you’re using Postgres, Oracle, or MySQL with InnoDB, this is enforced at the database integrity level – database level key constraints prevent the creation of relations that can’t be validated.
However, if you’re using SQLite or MySQL with MyISAM tables, there is no enforced referential integrity; as a result, you may be able to ‘fake’ cross database foreign keys. However, this configuration is not officially supported by Django.
Hope this answer saves many of you the trouble.
I've also found this Django patch: https://code.djangoproject.com/attachment/ticket/17875/foreignkey-db-using.patch this might help for cross database relationships
Related
Friends,
I am facing issue while trying to use aws read replica (mysql) for django project.
Setup is as follows:
Django project with multiple apps in it.
Having AWS RDS Instance (currently used for write/read both)
Issue: When the load is high RDS instance CPU usage hits around 95% and site becomes unresponsive, till date solution applied was vertical scaling where higher CPU type has been used to handle more load - but that comes with the price and also not a scalable solution.
Now we are trying to have AWS read replica and redirect all read requests for specific app only to read replica as a POC and later redirect more apps to read replica
Code snippet
router.py
class DBRouter:
def db_for_write(self, model, **hints):
return 'default'
def db_for_read(self, model, **hints):
if model._meta.app_label == 'my-app':
return 'read_replica'
return 'default'
settings.py
DATABASES = {
'default': {
'HOST': 'xxxx.rds.amazonaws.com',
'NAME': 'my-db',
'ENGINE': 'django.db.backends.mysql',
'USER': 'xxxx',
'PASSWORD': 'xxxx'
},
'read_replica': {
'HOST': 'xxxx-read-replica.rds.amazonaws.com',
'NAME': 'my-db',
'ENGINE': 'django.db.backends.mysql',
'USER': 'xxxx',
'PASSWORD': 'xxxx'
}
}
DATABASE_ROUTERS = ['<<path.to.>>DBRouter']
Things work well where only get query is there but at some places get_or_create() method has been used which take it to read replica and throws an error saying operation is not allowed because its pointing to read-replica
I tried to check django multiple database documentation and in other forums but could not find much details on how get_or_create() works in this scenario. Can someone help me with more details or pointers on how to handle such situation?
Thank you in advance.
I have 2 instances each running its own postgres database.
One is for production usage. The other is a read-only database that performs replication from the production database.
Both instances run the same django 1.11 application codebase.
When I attempt to login to the django read-only version, I cannot login because the very action of login apparently execute some update or insert statements.
I get an internal error about read-only database: cannot execute INSERT in a read-only transaction
What are my options if I want to allow users to access the read-only database using the same codebase?
UPDATE
I have already tried django-postgres-readonly. Same results.
On the codebase that's talking to the read-only database
Step 1: install django-no-last-login v0.1.0
Step 2: inside settings.py add/change the following
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
INSTALLED_APPS += [
'nolastlogin',
]
NO_UPDATE_LAST_LOGIN = True
By default Django uses database for session engine, so switch to something else.
Also the plugin makes it easy to turn off the update last login behavior by Django.
Django auto updates the last login time. Since we want zero database writes, so we need to use that.
Django need to update tables like django_session.
My advice is to use 2 different databases for the "django-tables" and your "read-only-tables"
How?
Create a simple and empty sqlite3 database and use the class AuthRouter
for managed them.
For your database settings use something like:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
},
'otherdb': {
'NAME': 'user_data',
'ENGINE': 'django.db.backends.postgresql',
'USER': 'ypurusername',
'PASSWORD': 'yourpassword',
'HOST': '0.0.0.0'
}
}
Example of AuthRouter:
class AuthRouter:
"""
A router to control all database operations on models in the
auth application.
"""
def db_for_read(self, model, **hints):
"""
Attempts to read auth models go to auth_db.
"""
if model._meta.db_table == 'django-table':
return 'defaul'
return otherdb
def db_for_write(self, model, **hints):
"""
Attempts to write auth models go to auth_db.
"""
if model._meta.app_label == 'auth':
return 'auth_db'
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
"""
Make sure the auth app only appears in the 'auth_db'
database.
"""
if app_label == 'migrations':
return db == 'default'
return otherdb
Here the link to the docs
In my settings.py I have this:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'foo',
'USER': 'foo',
'PASSWORD': 'foo',
'HOST': 'localhost',
},
}
I've ran this SQL on my db:
GRANT ALL ON test_foo.* TO 'foo'#'localhost';
I've distilled what I'm trying to figure out with my test to this:
from django.test import TestCase
from django.contrib.auth.models import User
class UserTestCase(TestCase):
def test_create_user(self):
user = User.objects.create_user(
username='jacob', email='jacob#bla.net', password='top_secret')
user.save()
user = User.objects.get(username="jacob")
import ipdb; ipdb.set_trace()
self.assertTrue(False)
When I run the test and hit the break point, I query the auth_user table in test_foo and it is empty.
Why is this? Thanks
Django's TestCase class makes use of database transactions (if the database supports it) to speed up tests.
This means that within a test method, any database modifications is only visible to that test and is not committed to the database. You won't be able to inspect the changes using an external tool.
If you need tests to commit changes to the test database, you can extend TransactionTestCase instead. E.g.
class UserTestCase(TransactionTestCase):
def test_create_user(self):
# any database changes will be visible in the test database
....
At the end of each TransactionTestCase test, the database is reset to a known state. Do note that this will make your unit tests slower. Once again, only databases that support transactions (e.g. PostgreSQL, MySQL+InnoDB) will exhibit this behavior.
See https://docs.djangoproject.com/en/1.9/topics/testing/tools/#transactiontestcase for more information.
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#the-test-database
Tests that require a database (namely, model tests) will not use your “real” (production) database. Separate, blank databases are created for the tests.
The default database name starts with test_, per the docs linked above.
Two answers to this question, depending on whether sharing is across different sites or different subdomains Second answer: Multiple Django apps, shared authentication
A user goes to site1.com and logs in. Now, if he goes to site2.com, then he should already be logged in (authenticated) at that site.
site1.com and site2.com are handled by different Django apps on the same sever.
I get that the sites can share the database containing the authentication tables. What I don't get is how the session data is handled. After logging in to site1, the user goes to site2. Here he always has request.user = "AnonymousUser: AnonymousUser" instead of a user_id.
I have set it up as here: https://docs.djangoproject.com/en/dev/topics/db/multi-db/ :
site1's settings has a single database that contains the auth models as well as some other data tables. site2's settings has 2 databases. One with its own data tables and also the one used by user1. I essentially copied class AuthRouter and set up the database router.
Is it not possible what I am trying to do? I don't actually understand how the two sites can share session data. Do I need something special outside of Django? Or should this work? I can include my code here, but don't want to confuse the issue if my basic thinking about this is wrong.
EDIT: here is my setup. I am trying this on localhost.
Site1 is running on localhost:8080
site2 is running on localhost:8000
SITE2 APP:
db_router.py:
class AuthRouter(object):
def db_for_read(self, model, **hints):
if model._meta.app_label == 'auth':
return 'the_ui'
return None
# same for write
def allow_syncdb(self, db, model):
if db == 'the_ui':
return model._meta.app_label == 'auth'
elif model._meta.app_label == 'auth':
return False
return None
class OtherRouter(object):
def db_for_read(self, model, **hints):
return "default"
# same for write, relation, syncdb
settings.py:
DATABASE_ROUTERS = ['site2_app.db_router.AuthRouter', 'site2_app.db_router.OtherRouter']
SESSION_COOKIE_DOMAIN = 'http://localhost:8080'
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
DATABASES = {
'default': {
# ...
},
'the_ui': {
# ...
}
}
SITE 1 APP:
# no router
# only single database, same as the "the_ui" used in site2
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
The marked answer is correct based on the initial question of using different sites.
Here is the answer for different subdomains, eg www.site.com and shop.site.com
Use the shared database authentication as described in the question. And then, in both settings.py:
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
SESSION_COOKIE_DOMAIN = '.site.com' #notice the period
SESSION_COOKIE_NAME = 'my_cookie'
SECRET_KEY = "" the same in both settings.py
There might be some issue about what happens if you have other subdomains that should NOT share this information. Or, maybe not, if you give their cookies different names??
Not sure if this can work on localhost.
As you said, the two sites can have the same authentication data by sharing the database or syncing the Users table between their respective databases.
This will ensure any user of site1.com will automatically become a member of site2.com and vice versa.
But your requirement of- any user who logs into site1.com should get automatically logged in site2.com is a bit tricky. What you really need is Single Sign On (SSO).
Why it can't be achieved by merely sharing the database (including session data) is because site2.com can never gain access to a cookie set by site1.com on the browser because of cross domain issues.
There are many SSO solutions using Django. Have a look at this SO question. Though I have never used it, Django-openid seems a good option.
You can use database routers for specifying which database should be used for auth backend.
Here I have given a example router code below:
class UserSessionRouter(object):
def db_for_read(self, model, **hints):
if model._meta.app_label == 'auth':
return 'usersandsessions'
elif model._meta.app_label == 'accounts':
return 'usersandsessions'
elif model._meta.app_label == 'sessions':
return 'usersandsessions'
return None
def db_for_write(self, model, **hints):
if model._meta.app_label == 'auth':
return 'usersandsessions'
elif model._meta.app_label == 'accounts':
return 'usersandsessions'
elif model._meta.app_label == 'sessions':
return 'usersandsessions'
return None
Then specify router using the database setting DATABASE_ROUTERS and SESSION_COOKIE_DOMAIN as given below
DATABASE_ROUTERS = ['site2.routers.UserSessionRouter']
SESSION_COOKIE_DOMAIN = 'site1.com'
As Sudipta mentioned, openid is one way to accomplish SSO.
Another way is to use SAML directly (there are some tools out there for this), or a hosted service like Stormpath (https://stormpath.com) which does SSO stuff for you, and provides directly support with Django's auth system: https://github.com/stormpath/stormpath-django
I work at Stormpath, so pretty biased, but figured I'd chime in as there's quite a lot of confusion around regarding SSO + Django solutions.
I'm overhauling a site I'd originally made using Joomla to Django, and I was wondering if I can import the user records directly from Joomla (my main concern is the user passwords as they are encrypted).
Yes, you can, but you'll have to do some work. Joomla keeps users in some specific DB table structure, so you'll have to pull them out and insert them into a users table you create in your Django application. As for encryption, if the algorithm is known, it's probably the hash value that's kept in the DB, and you can just transfer it as-is as long as you implement the same hashing algorithm in your Django application.
Remember: Django is a more general 'concept' than Joomla - it's a framework for writing web application, hence in theory you can even re-implement Joomla completely with it.
Joomla users in Django (Django auth backend, that populates users from Joomla)
Once I was in need to use our existing Joomla users in my new API, written in Django.
Problem is that I could not just copy Joomla users into a Django database, because:
Joomla password hashing system differs from Django one.
J-users and D-users had different set of fields (this is easy to fix, but still)
So instead I made a custom auth backend for Django, and now I can confidently say that
Django can authenticate users against the Joomla database, without need to decrypt password hashes or to copy all users from Joomla DB at once.
Algorithm:
connect the Joomla database to the Django project
create JoomlaUser model, to populate users from the Joomla DB
implement check_joomla_password() function, that validates user passwords the same way as Joomla
add custom "Joomla Auth Backend" that copies each user from Joomla to Django at the first login
Implementation:
To understand what's going on, you should have some experience with Django.
The code have to be modified accordingly to your django project.
However the code is taken from the working project with minimum changes,
and it should be easy to set up for your needs.
1. connect to Joomla DB:
Read https://docs.djangoproject.com/en/dev/topics/db/multi-db/
Add to /project_name/settings.py:
DATABASES = {
'default': {"your default DB settings"},
'joomla_db': {
'ENGINE': 'django.db.backends.mysql',
'OPTIONS': {},
'NAME': 'joomla_database_name',
# Don't store passwords in the code, instead use env vars:
'USER': os.environ['joomla_db_user'],
'PASSWORD': os.environ['joomla_db_pass'],
'HOST': 'joomla_db_host, can be localhost or remote IP',
'PORT': '3306',
}
}
# add logging to see DB requests:
LOGGING = {
'version': 1,
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}
2. create Joomla user model
Read https://docs.djangoproject.com/en/2.1/howto/legacy-databases/
Think where to keep new "Joomla user" model.
In my project I've created 'users' app, where my custom user models live,
and the custom Joomla backend will be placed.
inspect how the user is stored in the existing Joomla DB:
python manage.py inspectdb --database="joomla_db"
Find and carefully examine the users table.
Add to users/models.py:
class JoomlaUser(models.Model):
""" Represents our customer from the legacy Joomla database. """
username = models.CharField(max_length=150, primary_key=True)
email = models.CharField(max_length=100)
password = models.CharField(max_length=100)
# you can copy more fields from `inspectdb` output,
# but it's enough for the example
class Meta:
# joomla db user table. WARNING, your case can differs.
db_table = 'live_users'
# readonly
managed = False
# tip for the database router
app_label = "joomla_users"
To ensure, that JoomlaUser model will use right DB, add a database router:
Create file "db_routers.py" in the project folder, where the "settings.py" file is stored:
# project_name/db_routers.py
class DbRouter:
"""this router makes sure that django uses legacy 'Joomla' database for models, that are stored there (JoomlaUser)"""
def db_for_read(self, model, **kwargs):
if model._meta.app_label == 'joomla_users':
return 'joomla_db'
return None
def db_for_write(self, model, **kwargs):
if model._meta.app_label == 'joomla_users':
return 'joomla_db'
return None
register new router, for that, add in settings.py:
# ensure that Joomla users are populated from the right database:
DATABASE_ROUTERS = ['project_name.db_routers.DbRouter']
Now go to django shell ./manage.py shell and try to populate some users, e.g.
>>> from users.models import JoomlaUser
>>> print(JoomlaUser.objects.get(username='someuser'))
JoomlaUser object (someuser)
>>>
If everything works - move on to the next step. Otherwise look into errors, fix settings, etc
3. Check Joomla user passwords
Joomla does not store user password, but the password hash, e.g.
$2y$10$aoZ4/bA7pe.QvjTU0R5.IeFGYrGag/THGvgKpoTk6bTz6XNkY0F2e
Starting from Joomla v3.2, user passwords are hashed using BLOWFISH algorithm.
So I've downloaded a python blowfish implementation:
pip install bcrypt
echo bcrypt >> requirements.txt
And created Joomla password check function in the users/backend.py:
def check_joomla_password(password, hashed):
"""
Check if password matches the hashed password,
using same hashing method (Blowfish) as Joomla >= 3.2
If you get wrong results with this function, check that
the Hash starts from prefix "$2y", otherwise it is
probably not a blowfish hash from Joomla.
:return: True/False
"""
import bcrypt
if password is None:
return False
# bcrypt requires byte strings
password = password.encode('utf-8')
hashed = hashed.encode('utf-8')
return hashed == bcrypt.hashpw(password, hashed)
Old versions Warning! Joomla < 3.2 uses different hashing method (md5+salt),
so this function won't work.
In this case read joomla password encryption
and implement a hash checker in python, which probably will look something like:
# WARNING - THIS FUNCTION NOT TESTED WITH REAL JOOMLA USERS
# and definitely has some errors
def check_old_joomla_password(password, hashed):
from hashlib import md5
password = password.encode('utf-8')
hashed = hashed.encode('utf-8')
if password is None:
return False
# check carefully this part:
hash, salt = hashed.split(':')
return hash == md5(password+salt).hexdigest()
Unfortunately I have no old Joomla instance running, thus I couldn't test this function for you.
4. Joomla Authentication Backend
Now you are ready to create a Joomla authentication backend for Django.
read how to modify django auth backends: https://docs.djangoproject.com/en/dev/topics/auth/customizing/
Register Jango (not yet existing) backend in the project/settings.py:
AUTHENTICATION_BACKENDS = [
# Check if user already in the local DB
# by using default django users backend
'django.contrib.auth.backends.ModelBackend',
# If user was not found among django users,
# use Joomla backend, which:
# - search for user in Joomla DB
# - check joomla user password
# - copy joomla user into Django user.
'users.backend.JoomlaBackend',
]
Create Joomla authentication Backend in users/backend.py:
from django.contrib.auth.models import User
from .models import JoomlaUser
""" check password function we wrote before """
def check_joomla_password(password, hashed):
...
class JoomlaBackend:
def authenticate(self, request, username=None, password=None):
"""
IF joomla user exists AND password is correct:
create django user
return user object
ELSE:
return None
"""
try:
joomla_user = JoomlaUser.objects.get(username=username)
except JoomlaUser.DoesNotExist:
return None
if check_joomla_password(password, joomla_user.password):
# Password is correct, let's create identical Django user:
return User.objects.create_user(
username=username,
email=joomla_user.email,
password=password,
# any additional fields from the Joomla user:
...
)
# this method is required to match Django Auth Backend interface
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Test & documentation
Congratulations - now your customers from old Joomla site can use their credentials on the new Django site or rest api, etc
Now, add proper tests and documentation to cover this new code.
It's logic is quite tricky, so if you won't make tests&docs (lazy dude) - maintaining the project will be a pain in your (or somebody's else) ass.
Kind regards,
# Dmytro Gierman
Update 11.04.2019 - errors fixed.
I think there is 3 ways to approach this problem:
1) You can read about how joomla and django make hash of passwords and make the migration with a script
2) You can make your own authentication backend
3) You can use a ETL tool
Joomla (PHP) is a CMS while Django (Python) is a web framework.
I wonder whether this is really possible. What i can conclude at this point of time is that it is not possible. However someone may have any idea about this.
Thanks :)