Skip to content

Commit 53e31a9

Browse files
authored
Fix peewee - backported from 4.0 (#364)
Closes: #347
1 parent d115f59 commit 53e31a9

File tree

7 files changed

+133
-45
lines changed

7 files changed

+133
-45
lines changed

CHANGES.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ Fixed
3030
(which is set if ``basic`` @auth_required method is used), a 401 is returned. See below
3131
for backwards compatibility concerns.
3232

33-
- (:pr:`xx`) As part of figuring out issue 359 - a redirect loop was found. In release 3.3.0 code was put
33+
- (:pr:`362`) As part of figuring out issue 359 - a redirect loop was found. In release 3.3.0 code was put
3434
in to redirect to :py:data:`SECURITY_POST_LOGIN_VIEW` when GET or POST was called and the caller was already authenticated. The
3535
method used would honor the request ``next`` query parameter. This could cause redirect loops. The pre-3.3.0 behavior
3636
of redirecting to :py:data:`SECURITY_POST_LOGIN_VIEW` and ignoring the ``next`` parameter has been restored.
3737

38+
- (:issue:`347`) Fix peewee. Turns out - due to lack of unit tests - peewee hasn't worked since 'permissions' were added in 3.3.
39+
Furthermore, changes in 3.4 around get_id and alternative tokens also didn't work since peewee defines its own get_id.
40+
3841
Compatibility Concerns
3942
++++++++++++++++++++++
4043

41-
In 3.3.0, `.Security.auth_required` was changed to add a default argument if none was given. The default
44+
In 3.3.0, :func:`.auth_required` was changed to add a default argument if none was given. The default
4245
include all current methods - ``session``, ``token``, and ``basic``. However ``basic`` really isn't like the others
4346
and requires that we send back a ``WWW-Authenticate`` header if authentication fails (and return a 401 and not redirect).
4447
``basic`` has been removed from the default set and must once again be explicitly requested.

docs/quickstart.rst

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ possible using MongoEngine:
273273
class Role(db.Document, RoleMixin):
274274
name = db.StringField(max_length=80, unique=True)
275275
description = db.StringField(max_length=255)
276+
permissions = db.StringField(max_length=255)
276277

277278
class User(db.Document, UserMixin):
278279
email = db.StringField(max_length=255)
@@ -348,15 +349,18 @@ possible using Peewee:
348349
# Create database connection object
349350
db = FlaskDB(app)
350351

351-
class Role(db.Model, RoleMixin):
352+
class Role(RoleMixin, db.Model):
352353
name = CharField(unique=True)
353354
description = TextField(null=True)
355+
permissions = TextField(null=True)
354356

355-
class User(db.Model, UserMixin):
357+
# N.B. order is important since db.Model also contains a get_id() -
358+
# we need the one from UserMixin.
359+
class User(UserMixin, db.Model):
356360
email = TextField()
357361
password = TextField()
358362
active = BooleanField(default=True)
359-
fs_uniquifier = TextField()
363+
fs_uniquifier = TextField(unique=True, null=False)
360364
confirmed_at = DateTimeField(null=True)
361365

362366
class UserRoles(db.Model):
@@ -368,6 +372,9 @@ possible using Peewee:
368372
name = property(lambda self: self.role.name)
369373
description = property(lambda self: self.role.description)
370374

375+
def get_permissions(self):
376+
return self.role.get_permissions()
377+
371378
# Setup Flask-Security
372379
user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
373380
security = Security(app, user_datastore)

flask_security/core.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,9 @@ def add_permissions(self, permissions):
678678
Caller must commit to DB.
679679
680680
.. versionadded:: 3.3.0
681+
682+
.. deprecated:: 3.4.4
683+
Use :meth:`.UserDatastore.remove_permissions_from_role`
681684
"""
682685
if hasattr(self, "permissions"):
683686
current_perms = self.get_permissions()
@@ -688,7 +691,7 @@ def add_permissions(self, permissions):
688691
else:
689692
perms = {permissions}
690693
self.permissions = ",".join(current_perms.union(perms))
691-
else:
694+
else: # pragma: no cover
692695
raise NotImplementedError("Role model doesn't have permissions")
693696

694697
def remove_permissions(self, permissions):
@@ -700,6 +703,9 @@ def remove_permissions(self, permissions):
700703
Caller must commit to DB.
701704
702705
.. versionadded:: 3.3.0
706+
707+
.. deprecated:: 3.4.4
708+
Use :meth:`.UserDatastore.remove_permissions_from_role`
703709
"""
704710
if hasattr(self, "permissions"):
705711
current_perms = self.get_permissions()
@@ -710,7 +716,7 @@ def remove_permissions(self, permissions):
710716
else:
711717
perms = {permissions}
712718
self.permissions = ",".join(current_perms.difference(perms))
713-
else:
719+
else: # pragma: no cover
714720
raise NotImplementedError("Role model doesn't have permissions")
715721

716722

@@ -791,9 +797,8 @@ def has_permission(self, permission):
791797
792798
"""
793799
for role in self.roles:
794-
if hasattr(role, "permissions"):
795-
if permission in role.get_permissions():
796-
return True
800+
if permission in role.get_permissions():
801+
return True
797802
return False
798803

799804
def get_security_payload(self):

flask_security/datastore.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,48 @@ def remove_role_from_user(self, user, role):
202202
self.put(user)
203203
return rv
204204

205+
def add_permissions_to_role(self, role, permissions):
206+
"""Add one or more permissions to role.
207+
208+
:param role: The role to modify. Can be a Role object or
209+
string role name
210+
:param permissions: a set, list, or single string.
211+
:return: True if permissions added, False if role doesn't exist.
212+
213+
Caller must commit to DB.
214+
215+
.. versionadded:: 3.4.4
216+
"""
217+
218+
rv = False
219+
user, role = self._prepare_role_modify_args(None, role)
220+
if role:
221+
rv = True
222+
role.add_permissions(permissions)
223+
self.put(role)
224+
return rv
225+
226+
def remove_permissions_from_role(self, role, permissions):
227+
"""Remove one or more permissions from a role.
228+
229+
:param role: The role to modify. Can be a Role object or
230+
string role name
231+
:param permissions: a set, list, or single string.
232+
:return: True if permissions removed, False if role doesn't exist.
233+
234+
Caller must commit to DB.
235+
236+
.. versionadded:: 3.4.4
237+
"""
238+
239+
rv = False
240+
user, role = self._prepare_role_modify_args(None, role)
241+
if role:
242+
rv = True
243+
role.remove_permissions(permissions)
244+
self.put(role)
245+
return rv
246+
205247
def toggle_active(self, user):
206248
"""Toggles a user's active status. Always returns True."""
207249
user.active = not user.active

tests/conftest.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ def mongoengine_setup(request, app, tmpdir, realdburl):
235235
class Role(db.Document, RoleMixin):
236236
name = db.StringField(required=True, unique=True, max_length=80)
237237
description = db.StringField(max_length=255)
238+
permissions = db.StringField(max_length=255)
238239
meta = {"db_alias": db_name}
239240

240241
class User(db.Document, UserMixin):
@@ -347,6 +348,7 @@ class Role(Base, RoleMixin):
347348
id = Column(Integer(), primary_key=True)
348349
name = Column(String(80), unique=True)
349350
description = Column(String(255))
351+
permissions = Column(String(255))
350352

351353
class User(Base, UserMixin):
352354
__tablename__ = "user"
@@ -424,12 +426,14 @@ def peewee_setup(request, app, tmpdir, realdburl):
424426

425427
db = FlaskDB(app)
426428

427-
class Role(db.Model, RoleMixin):
429+
class Role(RoleMixin, db.Model):
428430
name = CharField(unique=True, max_length=80)
429431
description = TextField(null=True)
432+
permissions = TextField(null=True)
430433

431-
class User(db.Model, UserMixin):
434+
class User(UserMixin, db.Model):
432435
email = TextField()
436+
fs_uniquifier = TextField(unique=True, null=False)
433437
username = TextField()
434438
security_number = IntegerField(null=True)
435439
password = TextField(null=True)
@@ -455,6 +459,9 @@ class UserRoles(db.Model):
455459
name = property(lambda self: self.role.name)
456460
description = property(lambda self: self.role.description)
457461

462+
def get_permissions(self):
463+
return self.role.get_permissions()
464+
458465
with app.app_context():
459466
for Model in (Role, User, UserRoles):
460467
Model.drop_table()
@@ -600,6 +607,27 @@ def client_nc(request, sqlalchemy_app):
600607
return app.test_client(use_cookies=False)
601608

602609

610+
@pytest.fixture(params=["cl-sqlalchemy", "c2", "cl-mongo", "cl-peewee"])
611+
def clients(request, app, tmpdir, realdburl):
612+
if request.param == "cl-sqlalchemy":
613+
ds = sqlalchemy_setup(request, app, tmpdir, realdburl)
614+
elif request.param == "c2":
615+
ds = sqlalchemy_session_setup(request, app, tmpdir, realdburl)
616+
elif request.param == "cl-mongo":
617+
ds = mongoengine_setup(request, app, tmpdir, realdburl)
618+
elif request.param == "cl-peewee":
619+
ds = peewee_setup(request, app, tmpdir, realdburl)
620+
elif request.param == "cl-pony":
621+
# Not working yet.
622+
ds = pony_setup(request, app, tmpdir, realdburl)
623+
app.security = Security(app, datastore=ds)
624+
populate_data(app)
625+
if request.param == "cl-peewee":
626+
# peewee is insistent on a single connection?
627+
ds.db.close_db(None)
628+
return app.test_client()
629+
630+
603631
@pytest.yield_fixture()
604632
def in_app_context(request, sqlalchemy_app):
605633
app = sqlalchemy_app()

tests/test_common.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -269,42 +269,42 @@ def test_unauthorized_access_with_referrer(client, get_message):
269269

270270

271271
@pytest.mark.settings(unauthorized_view="/unauthz")
272-
def test_roles_accepted(client):
272+
def test_roles_accepted(clients):
273273
# This specificaly tests that we can pass a URL for unauthorized_view.
274274
275-
authenticate(client, user)
276-
response = client.get("/admin_or_editor")
275+
authenticate(clients, user)
276+
response = clients.get("/admin_or_editor")
277277
assert b"Admin or Editor Page" in response.data
278-
logout(client)
278+
logout(clients)
279279

280-
authenticate(client, "[email protected]")
281-
response = client.get("/admin_or_editor", follow_redirects=True)
280+
authenticate(clients, "[email protected]")
281+
response = clients.get("/admin_or_editor", follow_redirects=True)
282282
assert b"Unauthorized" in response.data
283283

284284

285285
@pytest.mark.settings(unauthorized_view="unauthz")
286-
def test_permissions_accepted(client):
286+
def test_permissions_accepted(clients):
287287
288-
authenticate(client, user)
289-
response = client.get("/admin_perm")
288+
authenticate(clients, user)
289+
response = clients.get("/admin_perm")
290290
assert b"Admin Page with full-write or super" in response.data
291-
logout(client)
291+
logout(clients)
292292

293-
authenticate(client, "[email protected]")
294-
response = client.get("/admin_perm", follow_redirects=True)
293+
authenticate(clients, "[email protected]")
294+
response = clients.get("/admin_perm", follow_redirects=True)
295295
assert b"Unauthorized" in response.data
296296

297297

298298
@pytest.mark.settings(unauthorized_view="unauthz")
299-
def test_permissions_required(client):
299+
def test_permissions_required(clients):
300300
for user in ["[email protected]"]:
301-
authenticate(client, user)
302-
response = client.get("/admin_perm_required")
301+
authenticate(clients, user)
302+
response = clients.get("/admin_perm_required")
303303
assert b"Admin Page required" in response.data
304-
logout(client)
304+
logout(clients)
305305

306-
authenticate(client, "[email protected]")
307-
response = client.get("/admin_perm_required", follow_redirects=True)
306+
authenticate(clients, "[email protected]")
307+
response = clients.get("/admin_perm_required", follow_redirects=True)
308308
assert b"Unauthorized" in response.data
309309

310310

@@ -315,15 +315,15 @@ def test_unauthenticated_role_required(client, get_message):
315315

316316

317317
@pytest.mark.settings(unauthorized_view="unauthz")
318-
def test_multiple_role_required(client):
318+
def test_multiple_role_required(clients):
319319
320-
authenticate(client, user)
321-
response = client.get("/admin_and_editor", follow_redirects=True)
320+
authenticate(clients, user)
321+
response = clients.get("/admin_and_editor", follow_redirects=True)
322322
assert b"Unauthorized" in response.data
323-
client.get("/logout")
323+
clients.get("/logout")
324324

325-
authenticate(client, "[email protected]")
326-
response = client.get("/admin_and_editor", follow_redirects=True)
325+
authenticate(clients, "[email protected]")
326+
response = clients.get("/admin_and_editor", follow_redirects=True)
327327
assert b"Admin and Editor Page" in response.data
328328

329329

tests/test_datastore.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ def test_create_user_with_roles_and_permissions(app, datastore):
258258
user = datastore.find_user(email="[email protected]")
259259
assert user.has_role("test1") is True
260260
assert user.has_permission("read") is True
261+
assert user.has_permission("write") is False
261262

262263

263264
def test_permissions_strings(app, datastore):
@@ -305,20 +306,22 @@ def test_modify_permissions(app, datastore):
305306

306307
t1 = ds.find_role("test1")
307308
assert perms == t1.get_permissions()
308-
orig_update_time = t1.update_datetime
309-
assert t1.update_datetime <= datetime.datetime.utcnow()
309+
if hasattr(t1, "update_datetime"):
310+
orig_update_time = t1.update_datetime
311+
assert t1.update_datetime <= datetime.datetime.utcnow()
310312

311-
t1.add_permissions("execute")
313+
ds.add_permissions_to_role(t1, "execute")
312314
ds.commit()
313315

314316
t1 = ds.find_role("test1")
315317
assert perms.union({"execute"}) == t1.get_permissions()
316318

317-
t1.remove_permissions("read")
319+
ds.remove_permissions_from_role(t1, "read")
318320
ds.commit()
319321
t1 = ds.find_role("test1")
320322
assert {"write", "execute"} == t1.get_permissions()
321-
assert t1.update_datetime > orig_update_time
323+
if hasattr(t1, "update_datetime"):
324+
assert t1.update_datetime > orig_update_time
322325

323326

324327
def test_get_permissions(app, datastore):
@@ -350,13 +353,13 @@ def test_modify_permissions_multi(app, datastore):
350353
assert {"read", "write"} == t1.get_permissions()
351354

352355
# send in a list
353-
t1.add_permissions(["execute", "whatever"])
356+
ds.add_permissions_to_role(t1, ["execute", "whatever"])
354357
ds.commit()
355358

356359
t1 = ds.find_role("test1")
357360
assert {"read", "write", "execute", "whatever"} == t1.get_permissions()
358361

359-
t1.remove_permissions(["read", "whatever"])
362+
ds.remove_permissions_from_role(t1, ["read", "whatever"])
360363
ds.commit()
361364
assert {"write", "execute"} == t1.get_permissions()
362365

@@ -366,13 +369,13 @@ def test_modify_permissions_multi(app, datastore):
366369
ds.commit()
367370

368371
t2 = ds.find_role("test2")
369-
t2.add_permissions({"execute", "whatever"})
372+
ds.add_permissions_to_role(t2, {"execute", "whatever"})
370373
ds.commit()
371374

372375
t2 = ds.find_role("test2")
373376
assert {"read", "write", "execute", "whatever"} == t2.get_permissions()
374377

375-
t2.remove_permissions({"read", "whatever"})
378+
ds.remove_permissions_from_role(t2, {"read", "whatever"})
376379
ds.commit()
377380
assert {"write", "execute"} == t2.get_permissions()
378381

0 commit comments

Comments
 (0)