Skip to content

Commit

Permalink
Added current_user. Fixes #154
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Nov 23, 2017
1 parent 1553ed0 commit e4d9a10
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
``workflow`` parameter
* New: ``requestquery`` and ``requestform`` to complement
``coaster.views.requestargs``
* New: ``coaster.user`` module with a ``current_user`` proxy that provides
a standardised API for login managers to use, modeled on Flask-Login


0.6.0
Expand Down
5 changes: 3 additions & 2 deletions coaster/docflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from flask import g
import docflow
from werkzeug.exceptions import Forbidden
from .user import current_user

__all__ = ['WorkflowStateException', 'WorkflowTransitionException',
'WorkflowPermissionException', 'WorkflowState', 'WorkflowStateGroup',
Expand Down Expand Up @@ -78,6 +79,6 @@ def permissions(self):
if g:
if hasattr(g, 'permissions'):
perms.update(g.permissions or [])
if hasattr(self.document, 'permissions') and hasattr(g, 'user'):
perms = self.document.permissions(g.user, perms)
if hasattr(self.document, 'permissions'):
perms = self.document.permissions(current_user.self, perms)
return perms
5 changes: 5 additions & 0 deletions coaster/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pprint import pprint
import six
from flask import g, request, session
from .user import current_user


# global var as lazy in-memory cache
Expand Down Expand Up @@ -109,6 +110,10 @@ def formatException(self, ei):
print("App context:", file=sio)
pprint_with_indent(vars(g), sio)

print('\n----------\n', file=sio)
print("Current user:", file=sio)
pprint_with_indent(vars(current_user), sio)

s = sio.getvalue()
sio.close()
if s[-1:] == "\n":
Expand Down
103 changes: 103 additions & 0 deletions coaster/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-

"""
User account management
=======================
Provides a :obj:`current_user` for handling user accounts. Login managers
must comply with its API for Coaster's view handlers to work. Mostly
compatible with Flask-Login for the use cases Coaster requires.
Login managers must install themselves as ``current_app.login_manager``, and
must provide a ``_load_user()`` method, which loads the user object into
Flask's request context as ``_request_ctx_stack.top.user``.
"""

from __future__ import absolute_import
from werkzeug.local import LocalProxy
from flask import _request_ctx_stack, current_app, has_request_context


class UserProxy(object):
"""
Proxy class for user objects. Passes through access to all attributes, but
if you need the actual underlying object to assign or compare with, access
it from the ``self`` attribute. This proxy is constructed by
:obj:`current_user`. Typical uses:
Check if you have a valid authenticated user in the current request::
if current_user.is_authenticated:
Reverse check, for anonymous user. Your login manager may or may not
treat these as special database objects::
if current_user.is_anonymous:
Directly read and write attributes, and call methods on the user object.
These are passed through to the underlying object::
if current_user.name == 'foo':
current_user.name = 'bar'
current_user.set_updated()
However, for assignments and comparisons of the user object itself, you
must address it with the proxy's :attr:`self` attribute. This aspect is
where :class:`UserProxy` is incompatible with the ``UserMixin`` class in
Flask-Login::
if document.user == current_user.self:
other_document.user = current_user.self
"""
def __init__(self, user):
object.__setattr__(self, 'self', user)

def __getattr__(self, attr):
return getattr(self.self, attr)

def __setattr__(self, attr, value):
setattr(self.self, attr, value)

@property
def is_anonymous(self):
"""
Property that returns ``True`` if the proxy is not affiliated with a
user object. Returns the user object's ``is_anonymous`` attribute if
present, defaulting to ``False``. Login managers can supply a special
anonymous user object with this attribute set, if anonymous user objects
are required by the app.
"""
if self.self:
return getattr(self.self, 'is_anonymous', False)
return True

@property
def is_authenticated(self):
"""
Property that returns the opposite of :attr:`is_anonymous`. Using this
property is recommended for compatibility with Flask-Login and Django.
"""
return not self.is_anonymous


def _get_user():
if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
# Flask-Login, Flask-Lastuser or equivalent must add this
if hasattr(current_app, 'login_manager'):
current_app.login_manager._load_user()

return UserProxy(getattr(_request_ctx_stack.top, 'user', None))


#: A proxy object that returns the currently logged in user, attempting to
#: load it if not already loaded. Returns a :class:`UserProxy`. Typical use::
#:
#: from coaster.user import current_user
#:
#: @app.route('/')
#: def user_check():
#: if current_user.is_authenticated:
#: return "We have a user"
#: else:
#: return "User not logged in"
current_user = LocalProxy(_get_user)
15 changes: 7 additions & 8 deletions coaster/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from werkzeug.wrappers import Response as WerkzeugResponse
import six
from six.moves.urllib.parse import urlsplit
from .user import current_user

__jsoncallback_re = re.compile(r'^[a-z$_][0-9a-z$_]*$', re.I)

Expand Down Expand Up @@ -266,12 +267,10 @@ def profile_view(profile):
:param permission: If present, ``load_model`` calls the
:meth:`~coaster.sqlalchemy.PermissionMixin.permissions` method of the
retrieved object with ``g.user`` as a parameter. If ``permission`` is not
present in the result, ``load_model`` aborts with a 403. ``g`` is the Flask
request context object and you are expected to setup a request environment
in which ``g.user`` is the currently logged in user. Flask-Lastuser does this
automatically for you. The permission may be a string or a list of strings,
in which case access is allowed if any of the listed permissions are available
retrieved object with ``current_user.self`` as a parameter. If
``permission`` is not present in the result, ``load_model`` aborts with
a 403. The permission may be a string or a list of strings, in which
case access is allowed if any of the listed permissions are available
:param addlperms: Iterable or callable that returns an iterable containing additional
permissions available to the user, apart from those granted by the models. In an app
Expand Down Expand Up @@ -307,7 +306,7 @@ def load_models(*chain, **kwargs):
In the following example, load_models loads a Folder with a name matching the name in the
URL, then loads a Page with a matching name and with the just-loaded Folder as parent.
If the Page provides a 'view' permission to the current user (`g.user`), the decorated
If the Page provides a 'view' permission to the current user, the decorated
function is called::
@app.route('/<folder_name>/<page_name>')
Expand Down Expand Up @@ -369,7 +368,7 @@ def decorated_function(**kw):
return redirect(location, code=307)

if permission_required:
permissions = item.permissions(g.user, inherited=permissions)
permissions = item.permissions(current_user, inherited=permissions)
addlperms = kwargs.get('addlperms') or []
if callable(addlperms):
addlperms = addlperms() or []
Expand Down
7 changes: 4 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ Coaster is available under the BSD license, the same license as Flask.
logger
manage
assets
utils/index
shortuuid
user
views
docflow
sqlalchemy/index
db
utils/index
shortuuid
gfm
nlp
docflow

Indices and tables
==================
Expand Down
2 changes: 2 additions & 0 deletions docs/user.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. automodule:: coaster.user
:members:
12 changes: 6 additions & 6 deletions tests/test_loadmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relationship

from werkzeug.exceptions import Forbidden, NotFound
from flask import Flask, g

from coaster.views import load_model, load_models
from coaster.sqlalchemy import BaseMixin, BaseNameMixin, BaseScopedIdMixin
from coaster.db import db

from .test_models import (app1, app2, Container, NamedDocument,
ScopedNamedDocument, IdNamedDocument, ScopedIdDocument,
ScopedIdNamedDocument, User)

from werkzeug.exceptions import Forbidden, NotFound
from flask import Flask, g
ScopedIdNamedDocument, User, login_manager)


# --- Models ------------------------------------------------------------------
Expand Down Expand Up @@ -253,7 +253,7 @@ def tearDown(self):

def test_container(self):
with self.app.test_request_context():
g.user = User(username='test')
login_manager.set_user_for_testing(User(username='test'), load=True)
self.assertEqual(t_container(container=u'c'), self.container)

def test_named_document(self):
Expand Down Expand Up @@ -335,7 +335,7 @@ def test_unmutated_inherited_permissions(self):

def test_loadmodel_permissions(self):
with self.app.test_request_context():
g.user = User(username='foo')
login_manager.set_user_for_testing(User(username='foo'), load=True)
self.assertEqual(t_dotted_document_view(document=u'parent', child=1), self.child1)
self.assertEqual(t_dotted_document_edit(document=u'parent', child=1), self.child1)
self.assertRaises(Forbidden, t_dotted_document_delete, document=u'parent', child=1)
Expand Down
3 changes: 3 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
UuidMixin, UUIDType, add_primary_relationship, auto_init_default)
from coaster.utils import uuid2buid, uuid2suuid
from coaster.db import db
from .test_user import LoginManager


app1 = Flask(__name__)
Expand All @@ -28,6 +29,8 @@
app2.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app1)
db.init_app(app2)
login_manager = LoginManager(app1)
LoginManager(app2)


# --- Models ------------------------------------------------------------------
Expand Down
Loading

0 comments on commit e4d9a10

Please sign in to comment.