Merge lp:~ricardokirkner/django-pgtools/fix-role-username-unescaped into lp:django-pgtools

Proposed by Ricardo Kirkner
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: 14
Merged at revision: 11
Proposed branch: lp:~ricardokirkner/django-pgtools/fix-role-username-unescaped
Merge into: lp:django-pgtools
Diff against target: 323 lines (+90/-55)
6 files modified
pgtools/dbrole.py (+20/-12)
pgtools/dbuser.py (+11/-7)
pgtools/management/commands/grantuser.py (+9/-8)
pgtools/tests.py (+48/-26)
requirements.txt (+1/-1)
tox.ini (+1/-1)
To merge this branch: bzr merge lp:~ricardokirkner/django-pgtools/fix-role-username-unescaped
Reviewer Review Type Date Requested Status
William Grant code Approve
Daniel Manrique (community) Approve
Review via email: mp+321928@code.launchpad.net

Commit message

escape role and username properly

To post a comment you must log in.
Revision history for this message
Daniel Manrique (roadmr) wrote :

Weird! LGTM, I tried to ensure the quote shuffling looks OK but ooh, all those quotes/double quotes...

review: Approve
13. By Ricardo Kirkner

better quoting of identifiers

Revision history for this message
William Grant (wgrant) :
review: Approve (code)
14. By Ricardo Kirkner

make sure to respect psycopg2>=2.7 requirement in tox.ini

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'pgtools/dbrole.py'
2--- pgtools/dbrole.py 2017-01-16 19:08:45 +0000
3+++ pgtools/dbrole.py 2017-04-05 13:23:18 +0000
4@@ -5,6 +5,8 @@
5 from django.core.management.base import CommandError
6 from django.db import connection, transaction
7
8+from psycopg2.extensions import quote_ident
9+
10
11 class DatabaseRole(object):
12 ROLE_NAME = 'rolname'
13@@ -23,7 +25,8 @@
14 raise CommandError("Role already exists: %s" % rolename)
15
16 cursor = self.connection.cursor()
17- cursor.execute("CREATE ROLE %s" % rolename)
18+ role = quote_ident(rolename, cursor.cursor)
19+ cursor.execute('CREATE ROLE %s' % role)
20 if commit and not self.exists(rolename):
21 raise CommandError("Role couldn't be created: %s" % rolename)
22 elif not exists:
23@@ -31,7 +34,8 @@
24
25 def delete(self, commit=True):
26 cursor = self.connection.cursor()
27- cursor.execute("DROP ROLE %s" % self.rolename)
28+ role = quote_ident(self.rolename, cursor.cursor)
29+ cursor.execute('DROP ROLE %s' % role)
30 if commit and self.exists(self.rolename):
31 raise CommandError("Role cannot be deleted: %s" % self.rolename)
32
33@@ -40,19 +44,19 @@
34 db_connection = connection if db_connection is None else db_connection
35
36 cursor = db_connection.cursor()
37- cursor.execute('SELECT * FROM pg_roles WHERE rolname = %s', (rolename,))
38+ cursor.execute('SELECT * FROM pg_roles WHERE rolname=%s', (rolename,))
39 role = cursor.fetchone()
40 return role is not None
41
42 def get_users(self, order_by=ROLE_NAME):
43 cursor = self.connection.cursor()
44- query = ("SELECT rolname FROM pg_authid "
45- " WHERE oid IN ("
46- " SELECT member FROM pg_auth_members "
47- " WHERE roleid = ("
48- " SELECT oid FROM pg_authid "
49- " WHERE rolname = %%s)) "
50- "ORDER BY %s" % order_by)
51+ query = ('SELECT rolname FROM pg_authid '
52+ ' WHERE oid IN ('
53+ ' SELECT member FROM pg_auth_members '
54+ ' WHERE roleid = ('
55+ ' SELECT oid FROM pg_authid '
56+ ' WHERE rolname=%%s)) '
57+ 'ORDER BY %s' % order_by)
58 cursor.execute(query, (self.rolename,))
59 users_result = cursor.fetchall()
60 users = [result[0] for result in users_result]
61@@ -60,10 +64,14 @@
62
63 def grant(self, user, commit=True):
64 cursor = self.connection.cursor()
65- cursor.execute("GRANT %s TO %s" % (self.rolename, user.username))
66+ role = quote_ident(self.rolename, cursor.cursor)
67+ username = quote_ident(user.username, cursor.cursor)
68+ cursor.execute('GRANT %s TO %s' % (role, username))
69
70 def revoke(self, user, commit=True):
71 cursor = self.connection.cursor()
72- cursor.execute("REVOKE %s FROM %s" % (self.rolename, user.username))
73+ role = quote_ident(self.rolename, cursor.cursor)
74+ username = quote_ident(user.username, cursor.cursor)
75+ cursor.execute('REVOKE %s FROM %s' % (role, username))
76 if commit and user.has_role(self):
77 raise CommandError("Role cannot be revoked: %s" % self.rolename)
78
79=== modified file 'pgtools/dbuser.py'
80--- pgtools/dbuser.py 2017-01-16 19:08:45 +0000
81+++ pgtools/dbuser.py 2017-04-05 13:23:18 +0000
82@@ -5,6 +5,8 @@
83 from django.core.management.base import CommandError
84 from django.db import connection, transaction
85
86+from psycopg2.extensions import quote_ident
87+
88
89 class DatabaseUser(object):
90
91@@ -22,11 +24,12 @@
92 raise CommandError("User already exists: %s" % username)
93
94 cursor = self.connection.cursor()
95+ user = quote_ident(username, cursor.cursor)
96 if password is None:
97- cursor.execute("CREATE USER %s" % username)
98+ cursor.execute('CREATE USER %s' % user)
99 else:
100- cursor.execute("CREATE USER %s PASSWORD %%s" % username,
101- (password,))
102+ cursor.execute('CREATE USER %s PASSWORD %%s' % user,
103+ (password,))
104 if commit and not self.exists(username):
105 raise CommandError(
106 "User couldn't be created: %s" % username)
107@@ -35,7 +38,8 @@
108
109 def delete(self, commit=True):
110 cursor = self.connection.cursor()
111- cursor.execute("DROP USER %s" % self.username)
112+ user = quote_ident(self.username, cursor.cursor)
113+ cursor.execute('DROP USER %s' % user)
114 if commit and self.exists(self.username):
115 raise CommandError("User cannot be deleted: %s" % self.username)
116
117@@ -43,7 +47,7 @@
118 def exists(cls, username, db_connection=None):
119 db_connection = connection if db_connection is None else db_connection
120 cursor = db_connection.cursor()
121- cursor.execute('SELECT * FROM pg_roles WHERE rolname = %s', (username,))
122+ cursor.execute('SELECT * FROM pg_roles WHERE rolname=%s', (username,))
123 user = cursor.fetchone()
124 return user is not None
125
126@@ -52,10 +56,10 @@
127 query = ('SELECT * FROM pg_auth_members '
128 ' WHERE roleid = ('
129 ' SELECT oid FROM pg_authid '
130- ' WHERE rolname = %s) '
131+ ' WHERE rolname=%s) '
132 ' AND member = ('
133 ' SELECT oid FROM pg_authid '
134- ' WHERE rolname = %s)')
135+ ' WHERE rolname=%s)')
136 cursor.execute(query, (role.rolename, self.username))
137 membership = cursor.fetchone()
138 return bool(membership)
139
140=== modified file 'pgtools/management/commands/grantuser.py'
141--- pgtools/management/commands/grantuser.py 2017-04-03 13:41:34 +0000
142+++ pgtools/management/commands/grantuser.py 2017-04-05 13:23:18 +0000
143@@ -4,6 +4,7 @@
144
145 from django.core.management.base import BaseCommand
146 from django.db import connection
147+from psycopg2.extensions import quote_ident
148
149 from pgtools.decorators import graceful_db_errors
150 from pgtools.utils import (
151@@ -14,8 +15,8 @@
152 )
153
154
155-GRANT_SQL = "GRANT ALL ON %s TO %s;"
156-EXISTS_SQL = "SELECT 1 FROM pg_class WHERE relname='%s';"
157+GRANT_SQL = 'GRANT ALL ON %s TO %s'
158+EXISTS_SQL = 'SELECT 1 FROM pg_class WHERE relname=%s'
159
160
161 class Command(BaseCommand):
162@@ -58,20 +59,20 @@
163 connection.connection.commit()
164
165 def _check_exists(self, table_or_sequence):
166- sql = EXISTS_SQL % table_or_sequence
167- self.cursor.execute(sql)
168+ self.cursor.execute(EXISTS_SQL, (table_or_sequence,))
169 result = self.cursor.fetchone()
170 return bool(result)
171
172 def _grant_one(self, table_or_sequence):
173 print('Granting access to {}'.format(table_or_sequence))
174- sql = GRANT_SQL % (table_or_sequence, self.username)
175- self.cursor.execute(sql)
176+ table = quote_ident(table_or_sequence, self.cursor.cursor)
177+ username = quote_ident(self.username, self.cursor.cursor)
178+ self.cursor.execute(GRANT_SQL % (table, username))
179
180 def _grant_many_to_many(self, field):
181 table = field.m2m_db_table()
182- self.cursor.execute("select pg_get_serial_sequence('%s', 'id')" %
183- table)
184+ self.cursor.execute(
185+ "SELECT pg_get_serial_sequence(%s, %s)", (table, 'id'))
186 sequence = self.cursor.fetchone()[0]
187 self._grant_one(table)
188 self._grant_one(sequence)
189
190=== modified file 'pgtools/tests.py'
191--- pgtools/tests.py 2017-04-03 13:50:04 +0000
192+++ pgtools/tests.py 2017-04-05 13:23:18 +0000
193@@ -22,8 +22,8 @@
194 from django.db.utils import load_backend
195 from django.test import TransactionTestCase
196 from django.test.utils import override_settings
197-from mock import Mock, patch
198-from psycopg2 import ProgrammingError
199+from mock import Mock, call, patch
200+from psycopg2 import ProgrammingError, errorcodes
201
202 from pgtools.dbrole import DatabaseRole
203 from pgtools.dbuser import DatabaseUser
204@@ -38,10 +38,10 @@
205 parse_username_and_rolename,
206 )
207
208-EMPTY_ROLE_NAME = 'test_empty_role'
209-MASTER_ROLE_NAME = 'test_master_role'
210-TEST_USER_NAME1 = 'test_user1'
211-TEST_USER_NAME2 = 'test_user2'
212+EMPTY_ROLE_NAME = 'test-empty_role'
213+MASTER_ROLE_NAME = 'test-master_role'
214+TEST_USER_NAME1 = 'test_user-1'
215+TEST_USER_NAME2 = 'test_user-2'
216
217
218 class CommandBaseTestCase(TransactionTestCase):
219@@ -309,9 +309,9 @@
220 def test_graceful_db_errors_permission_denied(self):
221 @graceful_db_errors
222 def test_func():
223- err = ProgrammingError()
224- err.pgcode = '42501'
225- raise err
226+ class Error(ProgrammingError):
227+ pgcode = errorcodes.INSUFFICIENT_PRIVILEGE
228+ raise Error
229 try:
230 test_func()
231 self.fail('No errors raised.')
232@@ -322,9 +322,9 @@
233 def test_graceful_db_errors_other(self):
234 @graceful_db_errors
235 def test_func():
236- err = ProgrammingError()
237- err.pgcode = 100
238- raise err
239+ class Error(ProgrammingError):
240+ pgcode = 100
241+ raise Error
242 try:
243 test_func()
244 self.fail('No errors raised.')
245@@ -635,20 +635,42 @@
246 self.assertEqual(len(mock_grant_one.call_args_list), 1)
247
248 @patch('pgtools.management.commands.grantuser.get_models')
249- @patch('pgtools.management.commands.grantuser.connection')
250- def test__grant_many_to_many(self, mock_connection, mock_get_models):
251- mock_connection.cursor.return_value.fetchone.return_value = ['foo']
252- mock_get_models.return_value = self.get_models('auth')
253-
254- call_command('grantuser', 'payments')
255-
256- args_list = mock_connection.cursor.return_value.execute.call_args_list
257- calls = [x[0][0] for x in args_list]
258- for expected in [
259- "select pg_get_serial_sequence('auth_user_groups', 'id')",
260- 'GRANT ALL ON foo TO payments;'
261- ]:
262- self.assertTrue(expected in calls)
263+ def test__grant_many_to_many(self, mock_get_models):
264+ sql = ["CREATE TABLE pgtools_bar (id integer PRIMARY KEY, baz integer);",
265+ "CREATE TABLE pgtools_baz (id integer PRIMARY KEY, foo integer);"]
266+ cursor = connection.cursor()
267+ for query in sql:
268+ cursor.execute(query)
269+
270+ class Bar(models.Model):
271+ baz = models.ManyToManyField('Baz')
272+
273+ class Baz(models.Model):
274+ foo = models.IntegerField()
275+
276+ mock_get_models.return_value = [Bar, Baz]
277+
278+ # make sure role exists
279+ DatabaseRole('payments')
280+
281+ name = 'pgtools.management.commands.grantuser.connection.cursor'
282+ with patch(name) as mock_cursor:
283+ mock_cursor.return_value.cursor = cursor.cursor
284+ mock_cursor.return_value.fetchone.return_value = [
285+ 'pgtools_bar_baz_id_seq'
286+ ]
287+ call_command('grantuser', 'payments')
288+
289+ mock_execute = mock_cursor.return_value.execute
290+ expected_calls = [
291+ call('SELECT pg_get_serial_sequence(%s, %s)',
292+ ('pgtools_bar_baz', 'id')),
293+ call('GRANT ALL ON "pgtools_bar_baz" TO "payments"'),
294+ call('GRANT ALL ON "pgtools_bar_baz_id_seq" TO "payments"'),
295+ ]
296+
297+ for call_ in expected_calls:
298+ self.assertIn(call_, mock_execute.call_args_list)
299
300
301 @skipIf(DJANGO_VERSION > (1, 6), "Django 1.7 and later use django.apps for getting models")
302
303=== modified file 'requirements.txt'
304--- requirements.txt 2017-01-16 19:01:08 +0000
305+++ requirements.txt 2017-04-05 13:23:18 +0000
306@@ -1,3 +1,3 @@
307 mock
308-psycopg2==2.4.5
309+psycopg2>=2.7
310 django
311
312=== modified file 'tox.ini'
313--- tox.ini 2017-04-03 13:41:34 +0000
314+++ tox.ini 2017-04-05 13:23:18 +0000
315@@ -6,7 +6,7 @@
316 basepython =
317 python2.7
318 deps =
319- psycopg2 == 2.4.5
320+ psycopg2 >= 2.7
321 mock
322
323 [testenv:py3]

Subscribers

People subscribed via source and target branches