-
-
Notifications
You must be signed in to change notification settings - Fork 795
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Honor database assignment from router (#1450)
* Improve multiple database support. The token models might not be stored in the default database. There might not _be_ a default database. Intead, the code now relies on Django's routers to determine the actual database to use when creating transactions. This required moving from decorators to context managers for those transactions. To test the multiple database scenario a new settings file as added which derives from settings.py and then defines different databases and the routers needed to access them. The commit is larger than might be expected because when there are multiple databases the Django tests have to be told which databases to work on. Rather than copying the various test cases or making multiple database specific ones the decision was made to add wrappers around the standard Django TestCase classes and programmatically define the databases for them. This enables all of the same test code to work for both the one database and the multi database scenarios with minimal maintenance costs. A tox environment that uses the multi db settings file has been added to ensure both scenarios are always tested. * changelog entry and authors update * PR review response. Document multiple database requires in advanced_topics.rst. Add an ImproperlyConfigured validator to the ready method of the DOTConfig app. Fix IDToken doc string. Document the use of _save_bearer_token. Define LocalIDToken and use it for validating the configuration test. Questionably, define py39-multi-db-invalid-token-configuration-dj42. This will consistently cause tox runs to fail until it is worked out how to mark this as an expected failure. * move migration * update migration * use django checks system * drop misconfigured db check. Let's find a better way. * run checks * maybe a better test definition * listing tests was breaking things * No more magic. * Oops. Debugger. * Use retrieven_current_databases in django_db marked tests. * Updates. Prove the checks work. Document test requirements. * fix typo --------- Co-authored-by: Alan Crosswell <[email protected]> Co-authored-by: Alan Crosswell <[email protected]>
- Loading branch information
1 parent
1d19e3d
commit 9561866
Showing
40 changed files
with
392 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from django.apps import apps | ||
from django.core import checks | ||
from django.db import router | ||
|
||
from .settings import oauth2_settings | ||
|
||
|
||
@checks.register(checks.Tags.database) | ||
def validate_token_configuration(app_configs, **kwargs): | ||
databases = set( | ||
router.db_for_write(apps.get_model(model)) | ||
for model in ( | ||
oauth2_settings.ACCESS_TOKEN_MODEL, | ||
oauth2_settings.ID_TOKEN_MODEL, | ||
oauth2_settings.REFRESH_TOKEN_MODEL, | ||
) | ||
) | ||
|
||
# This is highly unlikely, but let's warn people just in case it does. | ||
# If the tokens were allowed to be in different databases this would require all | ||
# writes to have a transaction around each database. Instead, let's enforce that | ||
# they all live together in one database. | ||
# The tokens are not required to live in the default database provided the Django | ||
# routers know the correct database for them. | ||
if len(databases) > 1: | ||
return [checks.Error("The token models are expected to be stored in the same database.")] | ||
|
||
return [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from django.conf import settings | ||
from django.test import TestCase as DjangoTestCase | ||
from django.test import TransactionTestCase as DjangoTransactionTestCase | ||
|
||
|
||
# The multiple database scenario setup for these tests purposefully defines 'default' as | ||
# an empty database in order to catch any assumptions in this package about database names | ||
# and in particular to ensure there is no assumption that 'default' is a valid database. | ||
# | ||
# When there are multiple databases defined, Django tests will not work unless they are | ||
# told which database(s) to work with. | ||
|
||
|
||
def retrieve_current_databases(): | ||
if len(settings.DATABASES) > 1: | ||
return [name for name in settings.DATABASES if name != "default"] | ||
else: | ||
return ["default"] | ||
|
||
|
||
class OAuth2ProviderBase: | ||
@classmethod | ||
def setUpClass(cls): | ||
cls.databases = retrieve_current_databases() | ||
super().setUpClass() | ||
|
||
|
||
class OAuth2ProviderTestCase(OAuth2ProviderBase, DjangoTestCase): | ||
"""Place holder to allow overriding behaviors.""" | ||
|
||
|
||
class OAuth2ProviderTransactionTestCase(OAuth2ProviderBase, DjangoTransactionTestCase): | ||
"""Place holder to allow overriding behaviors.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
apps_in_beta = {"some_other_app", "this_one_too"} | ||
|
||
# These are bare minimum routers to fake the scenario where there is actually a | ||
# decision around where an application's models might live. | ||
|
||
|
||
class AlphaRouter: | ||
# alpha is where the core Django models are stored including user. To keep things | ||
# simple this is where the oauth2 provider models are stored as well because they | ||
# have a foreign key to User. | ||
|
||
def db_for_read(self, model, **hints): | ||
if model._meta.app_label not in apps_in_beta: | ||
return "alpha" | ||
return None | ||
|
||
def db_for_write(self, model, **hints): | ||
if model._meta.app_label not in apps_in_beta: | ||
return "alpha" | ||
return None | ||
|
||
def allow_relation(self, obj1, obj2, **hints): | ||
if obj1._state.db == "alpha" and obj2._state.db == "alpha": | ||
return True | ||
return None | ||
|
||
def allow_migrate(self, db, app_label, model_name=None, **hints): | ||
if app_label not in apps_in_beta: | ||
return db == "alpha" | ||
return None | ||
|
||
|
||
class BetaRouter: | ||
def db_for_read(self, model, **hints): | ||
if model._meta.app_label in apps_in_beta: | ||
return "beta" | ||
return None | ||
|
||
def db_for_write(self, model, **hints): | ||
if model._meta.app_label in apps_in_beta: | ||
return "beta" | ||
return None | ||
|
||
def allow_relation(self, obj1, obj2, **hints): | ||
if obj1._state.db == "beta" and obj2._state.db == "beta": | ||
return True | ||
return None | ||
|
||
def allow_migrate(self, db, app_label, model_name=None, **hints): | ||
if app_label in apps_in_beta: | ||
return db == "beta" | ||
|
||
|
||
class CrossDatabaseRouter: | ||
# alpha is where the core Django models are stored including user. To keep things | ||
# simple this is where the oauth2 provider models are stored as well because they | ||
# have a foreign key to User. | ||
def db_for_read(self, model, **hints): | ||
if model._meta.model_name == "accesstoken": | ||
return "beta" | ||
return None | ||
|
||
def db_for_write(self, model, **hints): | ||
if model._meta.model_name == "accesstoken": | ||
return "beta" | ||
return None | ||
|
||
def allow_relation(self, obj1, obj2, **hints): | ||
if obj1._state.db == "beta" and obj2._state.db == "beta": | ||
return True | ||
return None | ||
|
||
def allow_migrate(self, db, app_label, model_name=None, **hints): | ||
if model_name == "accesstoken": | ||
return db == "beta" | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Generated by Django 3.2.25 on 2024-08-08 22:47 | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import uuid | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
('tests', '0006_basetestapplication_token_family'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='LocalIDToken', | ||
fields=[ | ||
('id', models.BigAutoField(primary_key=True, serialize=False)), | ||
('jti', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='JWT Token ID')), | ||
('expires', models.DateTimeField()), | ||
('scope', models.TextField(blank=True)), | ||
('created', models.DateTimeField(auto_now_add=True)), | ||
('updated', models.DateTimeField(auto_now=True)), | ||
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), | ||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tests_localidtoken', to=settings.AUTH_USER_MODEL)), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Import the test settings and then override DATABASES. | ||
|
||
from .settings import * # noqa: F401, F403 | ||
|
||
|
||
DATABASES = { | ||
"alpha": { | ||
"ENGINE": "django.db.backends.sqlite3", | ||
"NAME": ":memory:", | ||
}, | ||
"beta": { | ||
"ENGINE": "django.db.backends.sqlite3", | ||
"NAME": ":memory:", | ||
}, | ||
# As https://docs.djangoproject.com/en/4.2/topics/db/multi-db/#defining-your-databases | ||
# indicates, it is ok to have no default database. | ||
"default": {}, | ||
} | ||
DATABASE_ROUTERS = ["tests.db_router.AlphaRouter", "tests.db_router.BetaRouter"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from .multi_db_settings import * # noqa: F401, F403 | ||
|
||
|
||
OAUTH2_PROVIDER = { | ||
# The other two tokens will be in alpha. This will cause a failure when the | ||
# app's ready method is called. | ||
"ID_TOKEN_MODEL": "tests.LocalIDToken", | ||
} |
Oops, something went wrong.