Skip to content
This repository has been archived by the owner on Sep 28, 2022. It is now read-only.

Commit

Permalink
Merge pull request #24 from postatum/93337716_auth_model
Browse files Browse the repository at this point in the history
Refactored AuthUser model. Fixed apply_privacy wrapper. Fixed tests
  • Loading branch information
jstoiko committed May 6, 2015
2 parents 57fa025 + 766a541 commit 74756cc
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 76 deletions.
122 changes: 77 additions & 45 deletions nefertari/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from nefertari.json_httpexceptions import *
from nefertari import engine as eng
from nefertari.utils import dictset

log = logging.getLogger(__name__)
crypt = cryptacular.bcrypt.BCRYPTPasswordManager()
Expand All @@ -22,50 +23,38 @@ def crypt_password(password):
return password


class AuthUser(eng.BaseDocument):
""" Class that is meant to be User class in Auth system.
class AuthModelDefaultMixin(object):
""" Mixin that implements all methods required for Ticket and Token
auth systems to work.
Implements basic operations to support Pyramid Ticket-based and custom
ApiKey token-based authentication.
All implemented methods must me class methods.
"""
__tablename__ = 'nefertari_authuser'

id = eng.IdField(primary_key=True)
username = eng.StringField(
min_length=1, max_length=50, unique=True,
required=True, processors=[lower_strip])
email = eng.StringField(
unique=True, required=True, processors=[lower_strip])
password = eng.StringField(
min_length=3, required=True, processors=[crypt_password])
groups = eng.ListField(
item_type=eng.StringField,
choices=['admin', 'user'], default=['user'])

uid = property(lambda self: str(self.id))

def is_admin(self):
return 'admin' in self.groups

def verify_password(self, password):
return crypt.check(self.password, password)
@classmethod
def is_admin(cls, user):
""" Determine if :user: is an admin. Is used by `apply_privacy` wrapper.
"""
return 'admin' in user.groups

@classmethod
def get_api_credentials(cls, userid, request):
""" Get username and api token for user with id of :userid: """
def token_credentials(cls, username, request):
""" Get api token for user with username of :username:
Is used by Token-based auth as `credentials_callback` kwarg.
"""
try:
user = cls.get_resource(id=userid)
user = cls.get_resource(username=username)
except Exception as ex:
log.error(unicode(ex))
forget(request)
if user:
return user.username, user.api_key.token
return None, None
return user.api_key.token

@classmethod
def authenticate_token(cls, username, token, request):
def groups_by_token(cls, username, token, request):
""" Get user's groups if user with :username: exists and his api key
token equals to :token:
Is used by Token-based authentication as `check` kwarg.
"""
try:
user = cls.get_resource(username=username)
Expand All @@ -76,8 +65,14 @@ def authenticate_token(cls, username, token, request):
return ['g:%s' % g for g in user.groups]

@classmethod
def authenticate(cls, params):
""" Authenticate user with login and password from :params: """
def authenticate_by_password(cls, params):
""" Authenticate user with login and password from :params:
Is used both by Token and Ticket-based auths (called from views).
"""
def verify_password(user, password):
return crypt.check(user.password, password)

login = params['login'].lower().strip()
key = 'email' if '@' in login else 'username'
try:
Expand All @@ -89,14 +84,17 @@ def authenticate(cls, params):

if user:
password = params.get('password', None)
success = (password and user.verify_password(password))
success = (password and verify_password(user, password))
return success, user

@classmethod
def groupfinder(cls, userid, request):
""" Return group identifiers of user with id :userid: """
def groups_by_userid(cls, userid, request):
""" Return group identifiers of user with id :userid:
Is used by Ticket-based auth as `callback` kwarg.
"""
try:
user = cls.get_resource(id=userid)
user = cls.get_resource(**{cls.id_field(): userid})
except Exception as ex:
log.error(unicode(ex))
forget(request)
Expand All @@ -106,7 +104,11 @@ def groupfinder(cls, userid, request):

@classmethod
def create_account(cls, params):
""" Create AuthUser instance with data from :params: """
""" Create auth user instance with data from :params:.
Is used by both Token and Ticket-based auths to register a user (
called from views).
"""
user_params = dictset(params).subset(
['username', 'email', 'password'])
try:
Expand All @@ -117,20 +119,49 @@ def create_account(cls, params):
raise JHTTPBadRequest('Failed to create account.')

@classmethod
def get_auth_user_by_id(cls, request):
""" Get user by ID """
def authuser_by_userid(cls, request):
""" Get user by ID.
Is used by Ticket-based auth. Is added as request method to populate
`request.user`.
"""
_id = authenticated_userid(request)
if _id:
return cls.get_resource(id=_id)
return cls.get_resource(**{cls.id_field(): _id})

@classmethod
def get_auth_user_by_name(cls, request):
""" Get user by username """
def authuser_by_name(cls, request):
""" Get user by username
Is used by Token-based auth. Is added as request method to populate
`request.user`.
"""
username = authenticated_userid(request)
if username:
return cls.get_resource(username=username)


class AuthUser(AuthModelDefaultMixin, eng.BaseDocument):
""" Class that is meant to be User class in Auth system.
Implements basic operations to support Pyramid Ticket-based and custom
ApiKey token-based authentication.
"""
__tablename__ = 'nefertari_authuser'

id = eng.IdField(primary_key=True)
username = eng.StringField(
min_length=1, max_length=50, unique=True,
required=True, processors=[lower_strip])
email = eng.StringField(
unique=True, required=True, processors=[lower_strip])
password = eng.StringField(
min_length=3, required=True, processors=[crypt_password])
groups = eng.ListField(
item_type=eng.StringField,
choices=['admin', 'user'], default=['user'])


def apikey_token():
""" Generate ApiKey.token using uuid library. """
return uuid.uuid4().hex.replace('-', '')
Expand Down Expand Up @@ -159,8 +190,9 @@ def apikey_model(user_model):
'ref_column': None,
}
if hasattr(user_model, '__tablename__'):
fk_kwargs['ref_column'] = '.'.join([user_model.__tablename__, 'id'])
fk_kwargs['ref_column_type'] = eng.IdField
fk_kwargs['ref_column'] = '.'.join([
user_model.__tablename__, user_model.id_field()])
fk_kwargs['ref_column_type'] = user_model.id_field_type()

class ApiKey(eng.BaseDocument):
__tablename__ = 'nefertari_apikey'
Expand Down
20 changes: 11 additions & 9 deletions nefertari/authentication/policies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pyramid.authentication import CallbackAuthenticationPolicy

from nefertari import engine as eng
from nefertari import engine
from .models import apikey_model


Expand All @@ -11,9 +11,11 @@ class ApiKeyAuthenticationPolicy(CallbackAuthenticationPolicy):
`Authorization: ApiKey username:token`
To use this policy, instantiate it with required arguments, as described
in `__init__` method and register it with Pyramid's `Configurator.set_authentication_policy`.
in `__init__` method and register it with Pyramid's
`Configurator.set_authentication_policy`.
You may also find usefull `nefertari.authentication.views.TokenAuthenticationView`
You may also find usefull `nefertari.authentication.views.
TokenAuthenticationView`
view which offers basic functionality to create, claim, reset token.
"""
def __init__(self, user_model, check=None, credentials_callback=None):
Expand All @@ -25,29 +27,29 @@ def __init__(self, user_model, check=None, credentials_callback=None):
:check: A callback passed the username, api_key and the request,
expected to return None if user doesn't exist or a sequence of
principal identifiers (possibly empty) if the user does exist.
If callback is None, the userid will be assumed to exist with
If callback is None, the username will be assumed to exist with
no principals. Optional.
:credentials_callback: A callback passed the userid, expected to
return tuple containing 2 elements: username and user's api key.
:credentials_callback: A callback passed the username and current
request, expected to return and user's api key.
Is used to generate 'WWW-Authenticate' header with a value of
valid 'Authorization' request header that should be used to
perform requests.
"""
self.user_model = user_model
if isinstance(self.user_model, basestring):
self.user_model = eng.get_document_cls(self.user_model)
self.user_model = engine.get_document_cls(self.user_model)
apikey_model(self.user_model)

self.check = check
self.credentials_callback = credentials_callback
super(ApiKeyAuthenticationPolicy, self).__init__()

def remember(self, request, userid, **kw):
def remember(self, request, username, **kw):
""" Return 'WWW-Authenticate' header with a value that should be used
in 'Authorization' header.
"""
if self.credentials_callback:
username, token = self.credentials_callback(userid, request)
token = self.credentials_callback(username, request)
api_key = 'ApiKey {}:{}'.format(username, token)
return [('WWW-Authenticate', api_key)]

Expand Down
17 changes: 10 additions & 7 deletions nefertari/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def register(self):
if not created:
raise JHTTPConflict('Looks like you already have an account.')

headers = remember(self.request, user.uid)
id_field = user.id_field()
headers = remember(self.request, getattr(user, id_field))
return JHTTPOk('Registered', headers=headers)

def login(self, **params):
Expand All @@ -32,10 +33,11 @@ def login(self, **params):
next = '' # never use the login form itself as next

unauthorized_url = self._params.get('unauthorized', None)
success, user = self._model_class.authenticate(self._params)
success, user = self._model_class.authenticate_by_password(self._params)

if success:
headers = remember(self.request, user.uid)
id_field = user.id_field()
headers = remember(self.request, getattr(user, id_field))
if next:
return JHTTPOk('Logged in', headers=headers)
else:
Expand Down Expand Up @@ -71,7 +73,7 @@ def register(self):
if not created:
raise JHTTPConflict('Looks like you already have an account.')

headers = remember(self.request, user.uid)
headers = remember(self.request, user.username)
return JHTTPOk('Registered', headers=headers)

def claim_token(self, **params):
Expand All @@ -81,10 +83,11 @@ def claim_token(self, **params):
header.
"""
self._params.update(params)
success, self.user = self._model_class.authenticate(self._params)
success, self.user = self._model_class.authenticate_by_password(
self._params)

if success:
headers = remember(self.request, self.user.uid)
headers = remember(self.request, self.user.username)
return JHTTPOk('Token claimed', headers=headers)
if self.user:
raise JHTTPUnauthorized('Wrong login or password')
Expand All @@ -102,5 +105,5 @@ def token_reset(self, **params):
return response

self.user.api_key.reset_token()
headers = remember(self.request, self.user.uid)
headers = remember(self.request, self.user.username)
return JHTTPOk('Registered', headers=headers)
19 changes: 12 additions & 7 deletions nefertari/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def _filter_fields(self, data):
# User authenticated
if user:
# User not admin
if not user.is_admin():
if not self.is_admin:
fields &= auth_fields

# User not authenticated
Expand All @@ -152,13 +152,18 @@ def _filter_fields(self, data):
def __call__(self, **kwargs):
result = kwargs['result']
data = result.get('data', result)
if not data:
return data

if issequence(data) and not isinstance(data, dict):
data = [apply_privacy(self.request)(result=d) for d in data]
else:
data = self._filter_fields(data)
if data:
self.is_admin = kwargs.get('is_admin')
if self.is_admin is None:
user = getattr(self.request, 'user', None)
self.is_admin = user is not None and type(user).is_admin(user)
if issequence(data) and not isinstance(data, dict):
kwargs = {'is_admin': self.is_admin}
data = [apply_privacy(self.request)(result=d, **kwargs)
for d in data]
else:
data = self._filter_fields(data)

if 'data' in result:
result['data'] = data
Expand Down

0 comments on commit 74756cc

Please sign in to comment.