New authentication logic

We are replacing the existing mixed BaseAuth TokenAuth authentication
logic and permissions system with a more streamlined solution, based on
user id and groups checking against node_type stored permissions. Such
permissions can be overridden on the node level (and complement the
public GET operations on the node entry point).
This commit is contained in:
Francesco Siddi 2015-10-11 22:20:18 +02:00
parent 474ddfc7af
commit 018ddfa20b
3 changed files with 292 additions and 134 deletions

View File

@ -17,14 +17,15 @@ from bson import ObjectId
from flask import g from flask import g
from flask import request from flask import request
from flask import url_for from flask import url_for
from flask import abort
from pre_hooks import pre_GET from pre_hooks import pre_GET
from pre_hooks import pre_PUT from pre_hooks import pre_PUT
from pre_hooks import pre_PATCH from pre_hooks import pre_PATCH
from pre_hooks import pre_POST from pre_hooks import pre_POST
from pre_hooks import pre_DELETE from pre_hooks import pre_DELETE
from pre_hooks import check_permissions # from pre_hooks import check_permissions
from pre_hooks import compute_permissions # from pre_hooks import compute_permissions
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
@ -47,9 +48,13 @@ class SystemUtility():
def validate(token): def validate(token):
"""Validate a Token against Blender ID server """Validate a token against the Blender ID server. This simple lookup
""" returns a dictionary with the following keys:
- message: a success message
- valid: a boolean, stating if the token is valid
- user: a dictionary with information regarding the user
"""
payload = dict( payload = dict(
token=token) token=token)
try: try:
@ -59,41 +64,55 @@ def validate(token):
raise e raise e
if r.status_code == 200: if r.status_code == 200:
message = r.json()['message'] response = r.json()
valid = r.json()['valid'] validation_result = dict(
user = r.json()['user'] message=response['message'],
valid=response['valid'],
user=response['user'])
else: else:
message = "" validation_result = dict(valid=False)
valid = False return validation_result
user = None
return dict(valid=valid, message=message, user=user)
def validate_token(): def validate_token():
"""Validate the token provided in the request and populate the current_user
flask.g object, so that permissions and access to a resource can be defined
from it.
"""
if not request.authorization: if not request.authorization:
# If no authorization headers are provided, we are getting a request
# from a non logged in user. Proceed accordingly.
return None return None
current_user = {}
token = request.authorization.username token = request.authorization.username
tokens = app.data.driver.db['tokens'] tokens = app.data.driver.db['tokens']
users = app.data.driver.db['users']
lookup = {'token': token, 'expire_time': {"$gt": datetime.now()}} lookup = {'token': token, 'expire_time': {"$gt": datetime.now()}}
dbtoken = tokens.find_one(lookup) db_token = tokens.find_one(lookup)
if not dbtoken: if not db_token:
# If no valid token is found, we issue a new request to the Blender ID
# to verify the validity of the token. We will get basic user info if
# the user is authorized and we will make a new token.
validation = validate(token) validation = validate(token)
if validation['valid']: if validation['valid']:
users = app.data.driver.db['users']
email = validation['user']['email'] email = validation['user']['email']
dbuser = users.find_one({'email': email}) db_user = users.find_one({'email': email})
tmpname = email.split('@')[0] tmpname = email.split('@')[0]
if not dbuser: if not db_user:
user_data = { user_data = {
'first_name': tmpname, 'first_name': tmpname,
'last_name': tmpname, 'last_name': tmpname,
'email': email, 'email': email,
'role': ['admin'],
} }
r = post_internal('users', user_data) r = post_internal('users', user_data)
user_id = r[0]["_id"] user_id = r[0]['_id']
groups = None
else: else:
user_id = dbuser['_id'] user_id = db_user['_id']
groups = db_user['groups']
token_data = { token_data = {
'user': user_id, 'user': user_id,
@ -101,20 +120,27 @@ def validate_token():
'expire_time': datetime.now() + timedelta(hours=1) 'expire_time': datetime.now() + timedelta(hours=1)
} }
post_internal('tokens', token_data) post_internal('tokens', token_data)
return token_data current_user = dict(
user_id=user_id,
token=token,
groups=groups,
token_expire_time=datetime.now() + timedelta(hours=1))
#return token_data
else: else:
return None return None
else: else:
token_data = { users = app.data.driver.db['users']
'user': dbtoken['user'], db_user = users.find_one(db_token['user'])
'token': dbtoken['token'], current_user = dict(
'expire_time': dbtoken['expire_time'] user_id=db_token['user'],
} token=db_token['token'],
return token_data groups=db_user['groups'],
token_expire_time=db_token['expire_time'])
setattr(g, 'current_user', current_user)
class TokensAuth(TokenAuth): class TokensAuth(TokenAuth):
def check_auth(self, token, allowed_roles, resource, method): def check_auth(self, token, allowed_roles, resource, method):
if not token: if not token:
return False return False
@ -127,21 +153,13 @@ class TokensAuth(TokenAuth):
# return validation['valid'] # return validation['valid']
return True return True
"""
users = app.data.driver.db['users']
lookup = {'first_name': token['username']}
if allowed_roles:
lookup['role'] = {'$in': allowed_roles}
user = users.find_one(lookup)
if not user:
return False
return token
"""
class BasicsAuth(BasicAuth): class BasicsAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method): def check_auth(self, username, password, allowed_roles, resource, method):
# return username == 'admin' and password == 'secret' # return username == 'admin' and password == 'secret'
print username
print password
return True return True
@ -157,12 +175,29 @@ class CustomTokenAuth(BasicsAuth):
return self.authorized_protected( return self.authorized_protected(
self, allowed_roles, resource, method) self, allowed_roles, resource, method)
else: else:
print 'is auth'
return self.token_auth.authorized(allowed_roles, resource, method) return self.token_auth.authorized(allowed_roles, resource, method)
def authorized_protected(self): def authorized_protected(self):
pass pass
class NewAuth(TokenAuth):
def check_auth(self, token, allowed_roles, resource, method):
if not token:
return False
else:
print '---'
print 'validating'
print token
print resource
print method
print '---'
validate_token()
return True
class ValidateCustomFields(Validator): class ValidateCustomFields(Validator):
def convert_properties(self, properties, node_schema): def convert_properties(self, properties, node_schema):
for prop in node_schema: for prop in node_schema:
@ -229,11 +264,10 @@ def post_item(entry, data):
# automatically. The default path (which work in Docker) can be overriden with # automatically. The default path (which work in Docker) can be overriden with
# an env variable. # an env variable.
settings_path = os.environ.get('EVE_SETTINGS', '/data/dev/pillar/pillar/settings.py') settings_path = os.environ.get('EVE_SETTINGS', '/data/dev/pillar/pillar/settings.py')
app = Eve(settings=settings_path, validator=ValidateCustomFields, auth=CustomTokenAuth) app = Eve(settings=settings_path, validator=ValidateCustomFields, auth=NewAuth)
import config import config
app.config.from_object(config.Deployment) app.config.from_object(config.Deployment)
app.config['MONGO_HOST'] = os.environ.get('MONGO_HOST', 'localhost')
client = MongoClient(app.config['MONGO_HOST'], 27017) client = MongoClient(app.config['MONGO_HOST'], 27017)
db = client.eve db = client.eve
@ -243,8 +277,10 @@ def global_validation():
token_data = validate_token() token_data = validate_token()
if token_data: if token_data:
setattr(g, 'token_data', token_data) setattr(g, 'token_data', token_data)
setattr(g, 'validate', validate(token_data['token'])) #setattr(g, 'validate', validate(token_data['token']))
check_permissions(token_data['user'], app.data.driver) check_permissions(token_data['user'], app.data.driver)
else:
print 'NO TOKEN'
def pre_GET_nodes(request, lookup): def pre_GET_nodes(request, lookup):
@ -270,11 +306,13 @@ def pre_PATCH_nodes(request):
def pre_POST_nodes(request): def pre_POST_nodes(request):
global_validation() # global_validation()
# print ("Post") # print ("Post")
# print ("World: {0}".format(g.get('world_permissions'))) # print ("World: {0}".format(g.get('world_permissions')))
# print ("Group: {0}".format(g.get('groups_permissions'))) # print ("Group: {0}".format(g.get('groups_permissions')))
return pre_POST(request, app.data.driver) # return pre_POST(request, app.data.driver)
print 'pre posting'
print request
def pre_DELETE_nodes(request, lookup): def pre_DELETE_nodes(request, lookup):
@ -287,20 +325,80 @@ def pre_DELETE_nodes(request, lookup):
return pre_DELETE(request, lookup, app.data.driver) return pre_DELETE(request, lookup, app.data.driver)
app.on_pre_GET_nodes += pre_GET_nodes #app.on_pre_GET_nodes += pre_GET_nodes
app.on_pre_POST_nodes += pre_POST_nodes app.on_pre_POST_nodes += pre_POST_nodes
app.on_pre_PATCH_nodes += pre_PATCH_nodes # app.on_pre_PATCH_nodes += pre_PATCH_nodes
app.on_pre_PUT_nodes += pre_PUT_nodes #app.on_pre_PUT_nodes += pre_PUT_nodes
app.on_pre_DELETE_nodes += pre_DELETE_nodes app.on_pre_DELETE_nodes += pre_DELETE_nodes
def check_permissions(resource, method):
"""Check user permissions to access a node. We look up node permissions from
world to groups to users and match them with the computed user permissions.
If there is not match, we return 403.
"""
current_user = g.get('current_user', None)
if 'permissions' in resource:
# If permissions are embedde in the node (this overrides any other
# permission previously set)
resource_permissions = resource['permissions']
elif type(resource['node_type']) is dict:
# If the node_type is embedded in the document, extract permissions
# from there
resource_permissions = resource['node_type']['permissions']
else:
# If the node_type is referenced with an ObjectID (was not embedded on
# request) query for if from the database and get the permissions
node_types_collection = app.data.driver.db['node_types']
node_type = node_types_collection.find_one(resource['node_type'])
resource_permissions = node_type['permissions']
if current_user:
# If the user is authenticated, proceed to compare the group permissions
for permission in resource_permissions['groups']:
if permission['group'] in current_user['groups']:
if method in permission['methods']:
return
for permission in resource_permissions['users']:
if current_user['user_id'] == permission['user']:
if method in permission['methods']:
return
# Check if the node is public or private. This must be set for non logged
# in users to see the content. For most BI projects this is on by default,
# while for private project this will not be set at all.
if 'world' in permissions and method in permissions['world']:
return
abort(403)
def before_returning_node(response):
# Run validation process, since GET on nodes entry point is public
validate_token()
check_permissions(response, 'GET')
def before_replacing_node(item, original):
check_permissions(original, 'PUT')
def before_inserting_nodes(items):
for item in items:
check_permissions(item, 'POST')
app.on_fetched_item_nodes += before_returning_node
app.on_replace_nodes += before_replacing_node
app.on_insert_nodes += before_inserting_nodes
def post_GET_user(request, payload): def post_GET_user(request, payload):
print 'computing permissions'
json_data = json.loads(payload.data) json_data = json.loads(payload.data)
# Check if we are querying the users endpoint (instead of the single user) # Check if we are querying the users endpoint (instead of the single user)
if json_data.get('_id') is None: if json_data.get('_id') is None:
return return
json_data['computed_permissions'] = \ # json_data['computed_permissions'] = \
compute_permissions(json_data['_id'], app.data.driver) # compute_permissions(json_data['_id'], app.data.driver)
payload.data = json.dumps(json_data) payload.data = json.dumps(json_data)
app.on_post_GET_users += post_GET_user app.on_post_GET_users += post_GET_user

View File

@ -1,4 +1,5 @@
import os import os
from eve.methods.put import put_internal
from application import app from application import app
from application import db from application import db
from application import post_item from application import post_item
@ -48,25 +49,6 @@ def clear_db():
db.drop_collection('users') db.drop_collection('users')
@manager.command
def remove_properties_order():
"""Removes properties.order
"""
from pymongo import MongoClient
client = MongoClient(MONGO_HOST, 27017)
db = client.eve
nodes = db.nodes.find()
for node in nodes:
new_prop = {}
for prop in node['properties']:
if prop == 'order':
continue
else:
new_prop[prop] = node['properties'][prop]
db.nodes.update({"_id": node['_id']},
{"$set": {"properties": new_prop}})
@manager.command @manager.command
def upgrade_node_types(): def upgrade_node_types():
"""Wipes node_types collection """Wipes node_types collection
@ -410,14 +392,6 @@ def populate_node_types(old_ids={}):
scene_node_type = { scene_node_type = {
'name': 'scene', 'name': 'scene',
'description': 'Scene node type', 'description': 'Scene node type',
'dyn_schema': {
'order': {
'type': 'integer',
}
},
'form_schema': {
'order': {},
},
'parent': { 'parent': {
"node_types": ["act"] "node_types": ["act"]
} }
@ -426,49 +400,11 @@ def populate_node_types(old_ids={}):
act_node_type = { act_node_type = {
'name': 'act', 'name': 'act',
'description': 'Act node type', 'description': 'Act node type',
'dyn_schema': {
'order': {
'type': 'integer',
}
},
'form_schema': {
'order': {},
},
'parent': {} 'parent': {}
} }
comment_node_type = {
'name': 'comment',
'description': 'Comment node type',
'dyn_schema': {
'text': {
'type': 'string',
'maxlength': 256
},
'attachments': {
'type': 'list',
'schema': {
'type': 'objectid',
'data_relation': {
'resource': 'files',
'field': '_id',
'embeddable': True
}
}
}
},
'form_schema': {
'text': {},
'attachments': {
'items': [("File", "name")]
}
},
'parent': {
"node_types": ["shot", "task"]
}
}
project_node_type = { node_type_project = {
'name': 'project', 'name': 'project',
'parent': {}, 'parent': {},
'description': 'The official project type', 'description': 'The official project type',
@ -567,9 +503,17 @@ def populate_node_types(old_ids={}):
} }
}, },
}, },
'permissions': {
'groups': [{
'group': '5596e975ea893b269af85c0e',
'methods': ['GET', 'PUT', 'POST']
}],
'users': [],
'world': ['GET']
}
} }
group_node_type = { node_type_group = {
'name': 'group', 'name': 'group',
'description': 'Generic group node type', 'description': 'Generic group node type',
'parent': {}, 'parent': {},
@ -594,9 +538,17 @@ def populate_node_types(old_ids={}):
'status': {}, 'status': {},
'notes': {}, 'notes': {},
}, },
'permissions': {
'groups': [{
'group': '5596e975ea893b269af85c0e',
'methods': ['GET', 'PUT', 'POST']
}],
'users': [],
'world': ['GET']
}
} }
asset_node_type = { node_type_asset = {
'name': 'asset', 'name': 'asset',
'description': 'Assets for Elephants Dream', 'description': 'Assets for Elephants Dream',
# This data type does not have parent limitations (can be child # This data type does not have parent limitations (can be child
@ -633,6 +585,14 @@ def populate_node_types(old_ids={}):
'status': {}, 'status': {},
'content_type': {}, 'content_type': {},
'file': {}, 'file': {},
},
'permissions': {
'groups': [{
'group': '5596e975ea893b269af85c0e',
'methods': ['GET', 'PUT', 'POST']
}],
'users': [],
'world': ['GET']
} }
} }
@ -666,6 +626,14 @@ def populate_node_types(old_ids={}):
}, },
'parent': { 'parent': {
"node_types": ["group", "project"] "node_types": ["group", "project"]
},
'permissions': {
'groups': [{
'group': '5596e975ea893b269af85c0e',
'methods': ['GET', 'PUT', 'POST']
}],
'users': [],
'world': ['GET']
} }
} }
@ -734,6 +702,14 @@ def populate_node_types(old_ids={}):
}, },
'parent': { 'parent': {
'node_types': ['asset',] 'node_types': ['asset',]
},
'permissions': {
'groups': [{
'group': '5596e975ea893b269af85c0e',
'methods': ['GET', 'PUT', 'POST']
}],
'users': [],
'world': ['GET']
} }
} }
@ -757,10 +733,16 @@ def populate_node_types(old_ids={}):
node_name = node_type['name'] node_name = node_type['name']
if node_name in old_ids: if node_name in old_ids:
node_type = mix_node_type(old_ids[node_name], node_type) node_type = mix_node_type(old_ids[node_name], node_type)
# Remove old node_type node_id = node_type['_id']
db.node_types.remove({'_id': old_ids[node_name]})
# Insert new node_type # Removed internal fields that would cause validation error
db.node_types.insert(node_type) internal_fields = ['_id', '_etag', '_updated', '_created']
for field in internal_fields:
node_type.pop(field, None)
p = put_internal('node_types', node_type, **{'_id': node_id})
print p
else: else:
print("Making the node") print("Making the node")
print(node_type) print(node_type)
@ -770,9 +752,9 @@ def populate_node_types(old_ids={}):
# upgrade(task_node_type, old_ids) # upgrade(task_node_type, old_ids)
# upgrade(scene_node_type, old_ids) # upgrade(scene_node_type, old_ids)
# upgrade(act_node_type, old_ids) # upgrade(act_node_type, old_ids)
# upgrade(comment_node_type, old_ids) upgrade(node_type_project, old_ids)
upgrade(project_node_type, old_ids) upgrade(node_type_group, old_ids)
upgrade(asset_node_type, old_ids) upgrade(node_type_asset, old_ids)
upgrade(node_type_storage, old_ids) upgrade(node_type_storage, old_ids)
upgrade(node_type_comment, old_ids) upgrade(node_type_comment, old_ids)
@ -1037,6 +1019,23 @@ def make_thumbnails():
t = build_thumbnails(file_path=f['path']) t = build_thumbnails(file_path=f['path'])
print t print t
@manager.command
def add_node_permissions():
import codecs
import sys
UTF8Writer = codecs.getwriter('utf8')
sys.stdout = UTF8Writer(sys.stdout)
nodes_collection = app.data.driver.db['nodes']
node_types_collection = app.data.driver.db['node_types']
nodes = nodes_collection.find()
for node in nodes:
print u"{0}".format(node['name'])
if 'permissions' not in node:
node_type = node_types_collection.find_one(node['node_type'])
# nodes_collection.update({'_id': node['_id']},
# {"$set": {'permissions': node_type['permissions']}})
print node['_id']
break
if __name__ == '__main__': if __name__ == '__main__':
manager.run() manager.run()

View File

@ -11,8 +11,6 @@ ITEM_METHODS = ['GET', 'PUT', 'DELETE', 'PATCH']
PAGINATION_LIMIT = 25 PAGINATION_LIMIT = 25
# To be implemented on Eve 0.6
# RETURN_MEDIA_AS_URL = True
users_schema = { users_schema = {
'first_name': { 'first_name': {
@ -147,6 +145,56 @@ organizations_schema = {
} }
} }
permissions_embedded_schema = {
'groups': {
'type': 'list',
'schema': {
'type': 'dict',
'schema': {
'group': {
'type': 'objectid',
'required': True,
'data_relation': {
'resource': 'groups',
'field': '_id',
'embeddable': True
}
},
'methods': {
'type': 'list',
'required': True,
'allowed': ['GET', 'PUT', 'POST', 'DELETE']
}
}
},
},
'users': {
'type': 'list',
'schema': {
'type': 'dict',
'schema': {
'user' : {
'type': 'objectid',
'required': True,
},
'methods': {
'type': 'list',
'required': True,
'allowed': ['GET', 'PUT', 'POST', 'DELETE']
}
}
}
},
'world': {
'type': 'list',
#'required': True,
'allowed': ['GET',]
},
'is_free': {
'type': 'boolean',
}
}
nodes_schema = { nodes_schema = {
'name': { 'name': {
'type': 'string', 'type': 'string',
@ -200,11 +248,15 @@ nodes_schema = {
'embeddable': True 'embeddable': True
}, },
}, },
'properties': { 'properties': {
'type' : 'dict', 'type' : 'dict',
'valid_properties' : True, 'valid_properties' : True,
'required': True, 'required': True,
}, },
'permissions': {
'type': 'dict',
'schema': permissions_embedded_schema
}
} }
node_types_schema = { node_types_schema = {
@ -229,6 +281,11 @@ node_types_schema = {
'parent': { 'parent': {
'type': 'dict', 'type': 'dict',
'required': True, 'required': True,
},
'permissions': {
'type': 'dict',
'required': True,
'schema': permissions_embedded_schema
} }
} }
@ -332,7 +389,6 @@ files_schema = {
} }
} }
groups_schema = { groups_schema = {
'name': { 'name': {
'type': 'string', 'type': 'string',
@ -364,11 +420,15 @@ groups_schema = {
} }
nodes = { nodes = {
'schema': nodes_schema 'schema': nodes_schema,
'public_methods': ['GET'],
'public_item_methods': ['GET']
} }
node_types = { node_types = {
'resource_methods': ['GET', 'POST'], 'resource_methods': ['GET', 'POST'],
'public_methods': ['GET'],
'public_item_methods': ['GET'],
'schema': node_types_schema, 'schema': node_types_schema,
} }
@ -379,7 +439,6 @@ users = {
'cache_control': 'max-age=10,must-revalidate', 'cache_control': 'max-age=10,must-revalidate',
'cache_expires': 10, 'cache_expires': 10,
# most global settings can be overridden at resource level
'resource_methods': ['GET', 'POST'], 'resource_methods': ['GET', 'POST'],
'public_methods': ['GET', 'POST'], 'public_methods': ['GET', 'POST'],
@ -399,7 +458,9 @@ tokens = {
files = { files = {
'resource_methods': ['GET', 'POST'], 'resource_methods': ['GET', 'POST'],
'schema': files_schema, 'public_methods': ['GET'],
'public_item_methods': ['GET'],
'schema': files_schema
} }
@ -423,5 +484,5 @@ DOMAIN = {
} }
if os.environ.get('MONGO_HOST'): MONGO_HOST = os.environ.get('MONGO_HOST', 'localhost')
MONGO_HOST = os.environ.get('MONGO_HOST') MONGO_PORT = os.environ.get('MONGO_PORT', 27017)