Skip to content

Commit fc80fa8

Browse files
committed
Custom User Ids for User Restricted Resource Access.
The feature now supports custom User Ids, allowing for more flexibility and token revocation with token-based authentication. - 'AUTH_USERNAME_FIELD' renamed to 'AUTH_FIELD' - 'auth_username_field' renamed to 'auth_field' - BasicAuth and subclasses now support the user_id property This change breaks backward compatibility. If you want to preserve the old 'AUTH_USERNAME_FIELD' behavior, just set 'self.user_id' to 'username'. Closes pyeve#73.
1 parent 75b23b0 commit fc80fa8

11 files changed

Lines changed: 140 additions & 57 deletions

File tree

CHANGES

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ Version 0.0.9
88

99
Not released yet.
1010

11+
- Custom user ids for User-Restricted Resource Access, allowing for more
12+
flexibility and token revocation with token-based authentication. Closes #73.
13+
- ``AUTH_USERNAME_FIELD`` renamed to ``AUTH_FIELD``.
14+
- ``auth_username_field`` renamed to ``auth_field``.
15+
- BasicAuth and subclasses now support ``user_id`` property.
1116
- Updated the event hooks naming system to be more robuts and consistent.
1217
Closes #80.
1318
- To emphasize the fact that they are tied to a method, all ``on_<method>``

docs/authentication.rst

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -393,21 +393,60 @@ unless they are made explicitly public.
393393

394394
User-Restricted Resource Access
395395
-------------------------------
396-
When this feature is enabled, authorized users can only read/update/delete
397-
items created by themselves.
396+
When this feature is enabled, each stored document is associated with the
397+
account that created it. This allows the API to transparently serve only
398+
account-created documents on all kind of requests: read, edit, delete and of
399+
course create. User autentication needs to be enabled for this to work
400+
properly.
401+
402+
At global level this feature is enabled by setting ``AUTH_FIELD`` and locally
403+
(at endpoint level) by setting ``auth_field``. These properties define the name
404+
of the field used to store the id of the user who created the document. So for
405+
example by setting ``AUTH_FIELD`` to ``user_id``, you are effectively (and
406+
trasparently to the user) adding a ``user_id`` field to every stored
407+
document. This will then be used to retrieve/edit/delete documents stored by
408+
the user.
409+
410+
But how do you set the ``auth_field`` value? By simply setting it in your
411+
custom class. Let us revise our BCrypt-authentication example from above:
412+
413+
.. code-block:: python
414+
:emphasize-lines: 25-27
398415
399-
What actually happens behind the scenes is that the value of
400-
`Authorization.username` request header is automatically added to new documents
401-
stored by the API. A filter on the same header is also transparently applied to
402-
all read, edit and delete requests.
416+
# -*- coding: utf-8 -*-
417+
418+
"""
419+
Auth-BCrypt
420+
~~~~~~~~~~~
421+
422+
Securing an Eve-powered API with Basic Authentication (RFC2617).
403423
404-
``AUTH_USERNAME_FIELD`` defines the name of the database field used to store
405-
the `Authorization.username` header. ``auth_username_field`` is the
406-
resource-level equivalent, which allows to effectively override the global
407-
setting, if present.
424+
You will need to install py-bcrypt: ``pip install py-bcrypt``
408425
409-
This feature can be used with both :ref:`basic` and :ref:`token` as they both
410-
rely on the `Authorization.username` field.
426+
This snippet by Nicola Iarocci can be used freely for anything you like.
427+
Consider it public domain.
428+
"""
429+
430+
import bcrypt
431+
from eve import Eve
432+
from eve.auth import BasicAuth
433+
434+
435+
class BCryptAuth(BasicAuth):
436+
def check_auth(self, username, password, allowed_roles, resource):
437+
# use Eve's own db driver; no additional connections/resources are used
438+
accounts = app.data.driver.db['accounts']
439+
account = accounts.find_one({'username': username})
440+
# set 'auth_field' value to the account's ObjectId
441+
# (instead of _Id, you might want to use ID_FIELD)
442+
self.user_id = account['_Id']
443+
return account and \
444+
bcrypt.hashpw(password, account['password']) == account['password']
445+
446+
447+
if __name__ == '__main__':
448+
app = Eve(auth=BCryptAuth)
449+
app.run()
411450
412451
.. admonition:: Please note
413452

docs/config.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,14 @@ uppercase.
227227
present. Can and most likely will be overriden
228228
when configuring single resource endpoints.
229229

230-
``AUTH_USERNAME_FIELD`` Enables :ref:`user-restricted`. When the
230+
``AUTH_FIELD`` Enables :ref:`user-restricted`. When the
231231
feature is enabled users can only
232232
read/update/delete resource items created by
233233
themselves. The keyword contains the actual
234-
name of the field used to store the username of
234+
name of the field used to store the id of
235235
the user who created the resource item. Can be
236236
overwritten by resource settings. Defaults to
237-
``''``, which disables the feature.
237+
``None``, which disables the feature.
238238

239239
``ALLOW_UNKNOWN`` When ``True`` this option will allow insertion
240240
and edition of arbitrary, unknown fields to
@@ -461,13 +461,13 @@ always lowercase.
461461
collections. See `Advanced Datasource
462462
Patterns`_.
463463

464-
``auth_username_field`` Enables :ref:`user-restricted`. When the
464+
``auth_field`` Enables :ref:`user-restricted`. When the
465465
feature is enabled users can only
466466
read/update/delete resource items created by
467467
themselves. The keyword contains the actual
468-
name of the field used to store the username of
468+
name of the field used to store the id of
469469
the user who created the resource item. Locally
470-
overrides ``AUTH_USERNAME_FIELD``.
470+
overrides ``AUTH_FIELD``.
471471

472472
``allow_unknown`` When ``True`` this option will allow insertion
473473
and edition of arbitrary, unknown fields to

docs/tutorials/account_management.rst

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,52 @@ associated with the account that created it. This allows the API to transparentl
253253
serve only account-created documents on all kind of requests: read, edit, delete
254254
and of course create.
255255

256-
The only thing that we need to do is configure the name of the field that will
257-
be used to store the owner of the document. It can be done at a global level
258-
(all endpoints will use the same field) and/or at endpoint level (see feature
259-
documentation for details). Let's just set the global field name in our
260-
settings file:
256+
There are only two things that we need to do in order to activate this feature:
257+
258+
1. configure the name of the field that will be used to store the owner of the
259+
document
260+
2. set the document owner on each incoming POST request.
261+
262+
263+
Since we want to enable this feature for all of our API endpoints we'll just
264+
update our ``settings.py`` file by setting a proper ``AUTH_FIELD`` value:
265+
266+
::
267+
268+
# Name of the field used to store the owner of each document
269+
AUTH_FIELD = 'user_id'
270+
271+
272+
Then, we want to update our authentication class to properly update the field's
273+
value:
261274

262275
.. code-block:: python
276+
:emphasize-lines: 15-17
277+
263278
264-
AUTH_USERNAME_FIELD: 'username'
279+
from eve import Eve
280+
from eve.auth import BasicAuth
281+
from werkzeug.security import check_password_hash
282+
283+
284+
class RolesAuth(BasicAuth):
285+
def check_auth(self, username, password, allowed_roles, resource):
286+
# use Eve's own db driver; no additional connections/resources are used
287+
accounts = app.data.driver.db['accounts']
288+
lookup = {'username': username}
289+
if allowed_roles:
290+
# only retrieve a user if his roles match ``allowed_roles``
291+
lookup['roles'] = {'$in': allowed_roles}
292+
account = accounts.find_one(lookup)
293+
# set 'AUTH_FIELD' value to the account's ObjectId
294+
# (instead of _Id, you might want to use ID_FIELD)
295+
self.user_id = account['_id']
296+
return account and check_password_hash(account['password'], password)
297+
298+
299+
if __name__ == '__main__':
300+
app = Eve(auth=RolesAuth)
301+
app.run()
265302
266303
This is all we need to do. Now, when a user hits the, say, ``/invoices/``
267304
endpoint with a GET request, he will only be served with the invoices created
@@ -483,19 +520,9 @@ methods. See :ref:`auth` for more details.
483520
6. Only allowing access to account resources
484521
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
485522
This is achieved with the :ref:`user-restricted` feature, as seen in
486-
:ref:`accounts_basic`. Update the settings file with the following global
487-
setting (or use the local ``auth_username_field`` if you only want to enable
488-
the feature on selected endpoints):
489-
490-
.. code-block:: python
491-
492-
AUTH_USERNAME_FIELD: 'token'
493-
494-
Stored documents will be associated with their account token. When a user hits
495-
the, say, ``/invoices/`` endpoint with a GET request, he will only be served
496-
with the invoices created by his own token. The same will happen with DELETE
497-
and PATCH, making it impossible for an authenticated user to accidentally
498-
retrieve, edit or delete other people data.
523+
:ref:`accounts_basic`. You might want to store the user token as your
524+
``AUTH_FIELD`` value, but if you want user tokens to be easily revocable, then
525+
your best option is to use the account unique id for this.
499526

500527
Basic vs Token: Final Considerations
501528
------------------------------------
@@ -505,10 +532,4 @@ stored on the client and being sent over the wire with every request. If
505532
you're sending your tokens out-of-band, and you're on SSL/TLS, that's quite
506533
a lot of additional security.
507534

508-
If you are using the :ref:`user-restricted` feature then a second and not
509-
irrelevant advantage is that, since you are just storing tokens with documents,
510-
when the user will eventually change his/her username no maintenance will be
511-
needed, as the token itself won't change. With Basic Authentication, since we
512-
would be storing usernames with documents, we'd be forced to update them all.
513-
514535
.. _SSL/TLS: http://en.wikipedia.org/wiki/Transport_Layer_Security

eve/auth.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,17 @@ class BasicAuth(object):
4848
""" Implements Basic AUTH logic. Should be subclassed to implement custom
4949
authorization checking.
5050
51+
.. versionchanged:: 0.0.9
52+
Support for user_id property.
53+
5154
.. versionchanged:: 0.0.7
5255
Support for 'resource' argument.
5356
5457
.. versionadded:: 0.0.4
5558
"""
59+
def __init__(self):
60+
self.user_id = None
61+
5662
def check_auth(self, username, password, allowed_roles, resource):
5763
""" This function is called to check if a username / password
5864
combination is valid. Must be overridden with custom logic.

eve/default_settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
:license: BSD, see LICENSE for more details.
1313
1414
.. versionchanged:: 0.0.9
15+
'AUTH_USERNAME_FIELD' renamed to 'AUTH_FIELD', and default value set to
16+
None.
1517
'DATE_FORMAT now using GMT instead of UTC.
1618
1719
.. versionchanged:: 0.0.7
@@ -67,7 +69,7 @@
6769
EXTRA_RESPONSE_FIELDS = []
6870

6971

70-
AUTH_USERNAME_FIELD = '' # user-restricted resource access is disabled
72+
AUTH_FIELD = None # user-restricted resource access is disabled
7173
# by default.
7274

7375
ALLOW_UNKNOWN = False # don't allow unknown key/value pairs for

eve/flaskapp.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def set_defaults(self):
287287
or global configuration settings.
288288
289289
.. versionchanged:: 0.0.9
290+
'auth_username_field' renamed to 'auth_field'.
290291
Always include automatic fields despite of datasource projections.
291292
292293
.. versionchanged:: 0.0.8
@@ -348,8 +349,8 @@ def set_defaults(self):
348349
else:
349350
item_methods = eve.ITEM_METHODS
350351
settings.setdefault('item_methods', item_methods)
351-
settings.setdefault('auth_username_field',
352-
self.config['AUTH_USERNAME_FIELD'])
352+
settings.setdefault('auth_field',
353+
self.config['AUTH_FIELD'])
353354
settings.setdefault('allow_unknown', self.config['ALLOW_UNKNOWN'])
354355
settings.setdefault('extra_response_fields',
355356
self.config['EXTRA_RESPONSE_FIELDS'])

eve/io/base.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ def _datasource_ex(self, resource, query=None, client_projection=None):
157157
to which an API resource refers to
158158
159159
.. versionchanged:: 0.0.9
160-
support for Python 3.3.
160+
Storing self.app.auth.userid in auth_field when 'user-restricted
161+
resource access' is enabled.
162+
Support for Python 3.3.
161163
162164
.. versionchanged:: 0.0.6
163165
'auth_username_field' is injected even in empty queries.
@@ -199,11 +201,11 @@ def _datasource_ex(self, resource, query=None, client_projection=None):
199201
request.endpoint == 'item_endpoint' and request.method
200202
not in config.DOMAIN[resource]['public_item_methods']
201203
):
202-
203204
# if 'user-restricted resource access' is enabled and there's an
204205
# Auth request active, add the username field to the query
205-
username_field = config.DOMAIN[resource].get('auth_username_field')
206-
if username_field and request.authorization and query is not None:
207-
query.update({username_field: request.authorization.username})
206+
auth_field = config.DOMAIN[resource].get('auth_field')
207+
if auth_field and self.app.auth.user_id:
208+
if request.authorization and query is not None:
209+
query.update({auth_field: self.app.auth.user_id})
208210

209211
return datasource, query, fields

eve/methods/post.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def post(resource, payl=None):
4646
renamed to 'on_insert'.
4747
You can now pass a pre-defined custom payload to the funcion.
4848
49+
.. versionchanged:: 0.0.9
50+
Storing self.app.auth.userid in auth_field when 'user-restricted
51+
resource access' is enabled.
52+
4953
.. versionchanged: 0.0.7
5054
Support for Rate-Limiting.
5155
Support for 'extra_response_fields'.
@@ -100,9 +104,11 @@ def post(resource, payl=None):
100104

101105
# if 'user-restricted resource access' is enabled and there's
102106
# an Auth request active, inject the username into the document
103-
username_field = resource_def['auth_username_field']
104-
if username_field and request.authorization:
105-
document[username_field] = request.authorization.username
107+
auth_field = resource_def['auth_field']
108+
if auth_field:
109+
userid = app.auth.user_id
110+
if userid and request.authorization:
111+
document[auth_field] = userid
106112

107113
else:
108114
# validation errors added to list of document issues

eve/tests/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
class ValidBasicAuth(BasicAuth):
1111
def check_auth(self, username, password, allowed_roles, resource):
12+
self.user_id = 123
1213
return username == 'admin' and password == 'secret' and \
1314
(allowed_roles == ['admin'] if allowed_roles else True)
1415

@@ -237,7 +238,7 @@ def setUp(self):
237238
self.test_client = self.app.test_client()
238239
self.valid_auth = [('Authorization', 'Basic YWRtaW46c2VjcmV0')]
239240
self.invalid_auth = [('Authorization', 'Basic IDontThinkSo')]
240-
self.field_name = 'auth_username_field'
241+
self.field_name = 'auth_field'
241242
self.data = {'item1': json.dumps({"ref": "0123456789123456789012345"})}
242243
for resource, settings in self.app.config['DOMAIN'].items():
243244
settings[self.field_name] = 'username'

0 commit comments

Comments
 (0)