Added local accounts

This commit is contained in:
Francesco Siddi 2016-04-26 12:33:48 +02:00
parent aa47c2b4a6
commit 2a2d35827c
6 changed files with 182 additions and 9 deletions

View File

@ -387,7 +387,9 @@ file_storage.setup_app(app, url_prefix='/storage')
from modules.encoding import encoding
from modules.blender_id import blender_id
from modules import projects
from modules import local_auth
app.register_blueprint(encoding, url_prefix='/encoding')
app.register_blueprint(blender_id, url_prefix='/blender_id')
projects.setup_app(app, url_prefix='/p')
local_auth.setup_app(app, url_prefix='/auth')

View File

@ -0,0 +1,84 @@
import base64
import datetime
import hashlib
import logging
import rsa
import bcrypt
from bson import tz_util
from eve.methods.post import post_internal
from flask import abort, Blueprint, current_app, jsonify, request
from application.utils.authentication import store_token
from application.utils.authentication import create_new_user_document
from application.utils.authentication import make_unique_username
blueprint = Blueprint('authentication', __name__)
log = logging.getLogger(__name__)
def get_auth_credentials(user, provider):
return next((credentials for credentials in user['auth'] if 'provider'
in credentials and credentials['provider'] == provider), None)
def create_local_user(email, password):
"""For internal user only. Given username and password, create a user."""
# Hash the password
hashed_password = hash_password(password, bcrypt.gensalt())
db_user = create_new_user_document(email, '', email, provider='local',
token=hashed_password)
# Make username unique
db_user['username'] = make_unique_username(email)
# Create the user
r, _, _, status = post_internal('users', db_user)
if status != 201:
log.error('internal response: %r %r', status, r)
return abort(500)
# Return user ID
return r['_id']
@blueprint.route('/make-token', methods=['POST'])
def make_token():
"""Direct login for a user, without OAuth, using local database. Generates
a token that is passed back to Pillar Web and used in subsequent
transactions.
:return: a token string
"""
username = request.form['username']
password = request.form['password']
# Look up user in db
users_collection = current_app.data.driver.db['users']
user = users_collection.find_one({'username': username})
if not user:
return abort(403)
# Check if user has "local" auth type
credentials = get_auth_credentials(user, 'local')
if not credentials:
return abort(403)
# Verify password
salt = credentials['token']
hashed_password = hash_password(password, salt)
if hashed_password != credentials['token']:
return abort(403)
# Generate Token
token = base64.b64encode(rsa.randnum.read_random_bits(256))
# TODO look into alternative implementations
token_expiry = datetime.datetime.now(tz=tz_util.utc) + datetime.timedelta(
days=15)
store_token(user['_id'], token, token_expiry)
return jsonify(token=token)
def hash_password(password, salt):
if isinstance(salt, unicode):
salt = salt.encode('utf-8')
encoded_password = base64.b64encode(hashlib.sha256(password).digest())
return bcrypt.hashpw(encoded_password, salt)
def setup_app(app, url_prefix):
app.register_blueprint(blueprint, url_prefix=url_prefix)

View File

@ -83,7 +83,7 @@ def find_token(token, is_subclient_token=False, **extra_filters):
return db_token
def store_token(user_id, token, token_expiry, oauth_subclient_id):
def store_token(user_id, token, token_expiry, oauth_subclient_id=False):
"""Stores an authentication token.
:returns: the token document from MongoDB
@ -92,9 +92,11 @@ def store_token(user_id, token, token_expiry, oauth_subclient_id):
token_data = {
'user': user_id,
'token': token,
'is_subclient_token': bool(oauth_subclient_id),
'expire_time': token_expiry,
}
if oauth_subclient_id:
token_data['is_subclient_token'] = True
r, _, _, status = post_internal('tokens', token_data)
if status not in {200, 201}:
@ -120,17 +122,20 @@ def create_new_user(email, username, user_id):
return user_id
def create_new_user_document(email, user_id, username):
"""Creates a new user document, without storing it in MongoDB."""
def create_new_user_document(email, user_id, username, provider='blender-id',
token=''):
"""Creates a new user document, without storing it in MongoDB. The token
parameter is a password in case provider is "local".
"""
user_data = {
'full_name': username,
'username': username,
'email': email,
'auth': [{
'provider': 'blender-id',
'provider': provider,
'user_id': str(user_id),
'token': ''}], # TODO: remove 'token' field altogether.
'token': token}],
'settings': {
'email_communications': 1
},

View File

@ -774,5 +774,10 @@ def expire_all_project_links(project_uuid):
print('Expired %i links' % result.matched_count)
@manager.command
def register_local_user(email, password):
from application.modules.local_auth import create_local_user
create_local_user(email, password)
if __name__ == '__main__':
manager.run()

View File

@ -85,11 +85,13 @@ users_schema = {
'schema': {
'provider': {
'type': 'string',
'allowed': ["blender-id",],
'allowed': ["blender-id", "local"],
},
'user_id': {
'type': 'string'
},
# A token is considered a "password" in case the provider is
# "local".
'token': {
'type': 'string'
}
@ -360,7 +362,6 @@ tokens_schema = {
'is_subclient_token': {
'type': 'boolean',
'required': False,
'default': False,
}
}

76
tests/test_local_auth.py Normal file
View File

@ -0,0 +1,76 @@
import json
import datetime
from bson import tz_util
from common_test_class import AbstractPillarTest
class LocalAuthTest(AbstractPillarTest):
def create_test_user(self):
from application.modules import local_auth
with self.app.test_request_context():
user_id = local_auth.create_local_user('koro@example.com', 'oti')
return user_id
def test_create_local_user(self):
user_id = self.create_test_user()
with self.app.test_request_context():
users = self.app.data.driver.db['users']
db_user = users.find_one(user_id)
self.assertIsNotNone(db_user)
def test_login_existing_user(self):
user_id = self.create_test_user()
resp = self.client.post('/auth/make-token',
data={'username': 'koro',
'password': 'oti'})
self.assertEqual(200, resp.status_code, resp.data)
token_info = json.loads(resp.data)
token = token_info['token']
headers = {'Authorization': self.make_header(token)}
resp = self.client.get('/users/%s' % user_id,
headers=headers)
self.assertEqual(200, resp.status_code, resp.data)
def test_login_expired_token(self):
user_id = self.create_test_user()
resp = self.client.post('/auth/make-token',
data={'username': 'koro',
'password': 'oti'})
self.assertEqual(200, resp.status_code, resp.data)
token_info = json.loads(resp.data)
token = token_info['token']
with self.app.test_request_context():
tokens = self.app.data.driver.db['tokens']
exp = datetime.datetime.now(tz=tz_util.utc) - datetime.timedelta(1)
result = tokens.update_one({'token': token},
{'$set': {'expire_time': exp}})
self.assertEqual(1, result.modified_count)
headers = {'Authorization': self.make_header(token)}
resp = self.client.get('/users/%s' % user_id,
headers=headers)
self.assertEqual(403, resp.status_code, resp.data)
def test_login_nonexistant_user(self):
resp = self.client.post('/auth/make-token',
data={'username': 'proog',
'password': 'oti'})
self.assertEqual(403, resp.status_code, resp.data)
def test_login_bad_pwd(self):
resp = self.client.post('/auth/make-token',
data={'username': 'koro',
'password': 'koro'})
self.assertEqual(403, resp.status_code, resp.data)