Added local accounts
This commit is contained in:
parent
aa47c2b4a6
commit
2a2d35827c
@ -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')
|
||||
|
84
pillar/application/modules/local_auth.py
Normal file
84
pillar/application/modules/local_auth.py
Normal 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)
|
@ -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
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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
76
tests/test_local_auth.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user