Skip to content

Commit

Permalink
Merge pull request #5 from cramshaw/develop
Browse files Browse the repository at this point in the history
Merge develop
  • Loading branch information
Chris authored Dec 3, 2020
2 parents 0dd339a + 80c2b66 commit 5ffe053
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 32 deletions.
55 changes: 37 additions & 18 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ DATABASES = {
}
}
```

And you've discovered that after ~15 minutes you make a request and receive `Access Denied for user@instance` because the password has expired.

This package allows you to generate the password at connection time by passing a callable instead:
Expand Down Expand Up @@ -62,20 +63,21 @@ def generate_pw():
pip install django-mysql-rds
```

or
or

```
git clone [email protected]:cramshaw/django-mysql-rds.git
```

## Why?

When I searched for a way to connect to an AWS RDS MySQL instance using SSL inside Django, I was unable to find anything that could handle the fact that the db auth token generated by AWS would expire every 15 minutes.

The problem is that when anything in the settings module changes, Django needs to reload. This isn't practical in a long running web app. I needed a way for the password to be generated at the time of connection.

## How?

On close inspection of the `django.db.backends.mysql` code, it became clear that the `DatabaseWrapper.get_connection_params` method takes the settings dict, and transforms it into the kwargs that are passed to `mysql.connect`. I have subclassed this and extended to recognise if the password passed in is a callable, and if so, to call it and pass on the returned value. This leads to
On close inspection of the `django.db.backends.mysql` code, it became clear that the `DatabaseWrapper.get_connection_params` method takes the settings dict, and transforms it into the kwargs that are passed to `mysql.connect`. I have subclassed this and extended to recognise if the password passed in is a callable, and if so, to call it and pass on the returned value. This leads to
Django receiving a fresh password every time a connection is created.

A very similar thing happens in the `DatabaseClient.settings_to_cmd_args` which is used for things like dumping and loading data. This has also been subclassed and changed to ensure the password generation method actually runs before attempting to create a run a shell.
Expand All @@ -98,8 +100,9 @@ python -m unittest tests/test*

Bump version in setup.py
then:

```
rm -rf dist/
python3 setup.py sdist bdist_wheel
python3 -m twine upload dist/*
```
```
4 changes: 2 additions & 2 deletions mysql_rds/backend/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
class DatabaseClient(MySQLClient):

@classmethod
def settings_to_cmd_args(cls, settings_dict):
def settings_to_cmd_args(cls, settings_dict, parameters):

if callable(settings_dict['PASSWORD']):
settings_dict['PASSWORD'] = settings_dict['PASSWORD']()
return super().settings_to_cmd_args(settings_dict)
return super().settings_to_cmd_args(settings_dict, parameters)
10 changes: 5 additions & 5 deletions mysql_rds/backend/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
from django.db.backends.mysql.creation import DatabaseCreation as MySQLCreation
from .client import DatabaseClient

# https://github.com/django/django/blob/stable/2.2.x/django/db/backends/mysql/creation.py
# https://github.com/django/django/blob/stable/3.1.x/django/db/backends/mysql/creation.py


class DatabaseCreation(MySQLCreation):
def _clone_db(self, source_database_name, target_database_name):
dump_args = DatabaseClient.settings_to_cmd_args(
self.connection.settings_dict)[1:]
dump_args[-1] = source_database_name
dump_cmd = ['mysqldump', '--routines', '--events'] + dump_args
self.connection.settings_dict, [])[1:]
dump_cmd = ['mysqldump', *dump_args[:-1],
'--routines', '--events', source_database_name]
load_cmd = DatabaseClient.settings_to_cmd_args(
self.connection.settings_dict)
self.connection.settings_dict, [])
load_cmd[-1] = target_database_name

with subprocess.Popen(dump_cmd, stdout=subprocess.PIPE) as dump_proc:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name='django-mysql-rds',
version='0.3.1',
version='0.4.0',
packages=find_packages(exclude=['tests']),
include_package_data=True,
license='Mozilla Public License 2.0 (MPL 2.0)',
Expand Down
7 changes: 4 additions & 3 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ class DatabaseClientTest(TestCase):
def test_get_callable_cmd_args(self):
conn_settings = SETTINGS_DICT
conn_settings['PASSWORD'] = generate_pw
rds_args = DatabaseClient.settings_to_cmd_args(conn_settings)
rds_args = DatabaseClient.settings_to_cmd_args(conn_settings, [])
self.assertEqual(rds_args[2], f'--password={CALLABLE_PASSWORD}')

def test_get_cmd_args_strings(self):
conn_settings = SETTINGS_DICT
conn_settings['PASSWORD'] = STRING_PASSWORD
rds_args = DatabaseClient.settings_to_cmd_args(conn_settings)
mysql_args = MySQLDatabaseClient.settings_to_cmd_args(conn_settings)
rds_args = DatabaseClient.settings_to_cmd_args(conn_settings, [])
mysql_args = MySQLDatabaseClient.settings_to_cmd_args(
conn_settings, [])
self.assertEqual(rds_args, mysql_args)
self.assertEqual(rds_args[2], f'--password={STRING_PASSWORD}')

0 comments on commit 5ffe053

Please sign in to comment.