# @author: Gunnar Schaefer, Kevin S. Hahn import logging log = logging.getLogger('nimsapi') logging.getLogger('requests').setLevel(logging.WARNING) # silence Requests library logging import copy import json import base64 import webapp2 import datetime import requests import bson.json_util ROLES = [ { 'rid': 'view', 'name': 'View-Only', }, { 'rid': 'download', 'name': 'Download', }, { 'rid': 'modify', 'name': 'Modify', }, { 'rid': 'admin', 'name': 'Admin', }, ] INTEGER_ROLES = {r['rid']: i for i, r in enumerate(ROLES)} FILE_SCHEMA = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'File', 'type': 'object', 'properties': { 'name': { 'title': 'Name', 'type': 'string', }, 'ext': { 'title': 'Extension', 'type': 'string', }, 'size': { 'title': 'Size', 'type': 'integer', }, 'sha1': { 'title': 'SHA-1', 'type': 'string', }, 'type': { 'title': 'Type', 'type': 'string', }, 'kinds': { 'title': 'Kinds', 'type': 'array', }, 'state': { 'title': 'State', 'type': 'array', }, }, 'required': ['name', 'ext', 'size', 'sha1', 'type', 'kinds', 'state'], 'additionalProperties': False } def mongo_dict(d): def _mongo_list(d, pk=''): pk = pk and pk + '.' return sum([_mongo_list(v, pk+k) if isinstance(v, dict) else [(pk+k, v)] for k, v in d.iteritems()], []) return dict(_mongo_list(d)) class RequestHandler(webapp2.RequestHandler): """fetches pubkey from own self.db.remotes. needs to be aware of OWN site uid""" json_schema = None file_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'File', 'type': 'object', 'properties': { 'name': { 'title': 'Name', 'type': 'string', }, 'ext': { 'title': 'Extension', 'type': 'string', }, 'size': { 'title': 'Size', 'type': 'integer', }, 'sha1': { 'title': 'SHA-1', 'type': 'string', }, 'type': { 'title': 'Type', 'type': 'string', }, 'kinds': { 'title': 'Kinds', 'type': 'array', }, 'state': { 'title': 'State', 'type': 'array', }, }, 'required': ['state', 'datatype', 'filetype'], #FIXME 'additionalProperties': False } def __init__(self, request=None, response=None): self.initialize(request, response) self.debug = self.app.config['insecure'] # set uid, source_site, public_request, and superuser self.uid = None self.source_site = None access_token = self.request.headers.get('Authorization', None) if access_token and self.app.config['oauth2_id_endpoint']: r = requests.get(self.app.config['oauth2_id_endpoint'], headers={'Authorization': 'Bearer ' + access_token}) if r.status_code == 200: self.uid = json.loads(r.content)['email'] else: headers = {'WWW-Authenticate': 'Bearer realm="%s", error="invalid_token", error_description="Invalid OAuth2 token."' % self.app.config['site_id']} self.abort(401, 'invalid oauth2 token', headers=headers) elif self.debug and self.request.get('user'): self.uid = self.request.get('user') elif self.request.user_agent.startswith('NIMS Instance'): self.uid = self.request.headers.get('X-User') self.source_site = self.request.headers.get('X-Site') if self.request.environ['SSL_CLIENT_VERIFY'] != 'SUCCESS': self.abort(401, 'no valid SSL client certificate') remote_instance = self.request.user_agent.replace('NIMS Instance', '').strip() if not self.app.db.remotes.find_one({'_id': remote_instance}): self.abort(402, remote_instance + ' is not authorized') self.public_request = not bool(self.uid) if self.public_request or self.source_site: self.superuser_request = False else: user = self.app.db.users.find_one({'_id': self.uid}, ['root', 'wheel']) if not user: self.abort(403, 'user ' + self.uid + ' does not exist') self.superuser_request = user.get('root') and user.get('wheel') def dispatch(self): """dispatching and request forwarding""" target_site = self.request.get('site', self.app.config['site_id']) if target_site == self.app.config['site_id']: log.debug('from %s %s %s %s %s' % (self.source_site, self.uid, self.request.method, self.request.path, str(self.request.params.mixed()))) return super(RequestHandler, self).dispatch() else: if not self.app.config['site_id']: self.abort(500, 'api site_id is not configured') if not self.app.config['ssl_cert']: self.abort(500, 'api ssl_cert is not configured') target = self.app.db.remotes.find_one({'_id': target_site}, ['api_uri']) if not target: self.abort(402, 'remote host ' + target_site + ' is not an authorized remote') # adjust headers self.headers = self.request.headers self.headers['User-Agent'] = 'NIMS Instance ' + self.app.config['site_id'] self.headers['X-User'] = self.uid self.headers['X-Site'] = self.app.config['site_id'] self.headers['Content-Length'] = len(self.request.body) del self.headers['Host'] if 'Authorization' in self.headers: del self.headers['Authorization'] # adjust params self.params = self.request.params.mixed() if 'user' in self.params: del self.params['user'] del self.params['site'] log.debug(' for %s %s %s %s %s' % (target_site, self.uid, self.request.method, self.request.path, str(self.request.params.mixed()))) target_uri = target['api_uri'] + self.request.path.split('/nimsapi')[1] r = requests.request(self.request.method, target_uri, params=self.params, data=self.request.body, headers=self.headers, cert=self.app.config['ssl_cert']) if r.status_code != 200: self.abort(r.status_code, 'InterNIMS p2p err: ' + r.reason) self.response.write(r.content) def abort(self, code, *args, **kwargs): log.warning(str(code) + ' ' + '; '.join(args)) webapp2.abort(code, *args, **kwargs) def schema(self, updates={}): json_schema = copy.deepcopy(self.json_schema) json_schema['properties'].update(updates) return json_schema class ContainerList(RequestHandler): def _get(self, query, projection, admin_only=False): if self.public_request: query['public'] = True else: projection['permissions'] = {'$elemMatch': {'_id': self.uid, 'site': self.source_site}} if not self.superuser_request: if admin_only: query['permissions'] = {'$elemMatch': {'_id': self.uid, 'site': self.source_site, 'access': 'admin'}} else: query['permissions'] = {'$elemMatch': {'_id': self.uid, 'site': self.source_site}} containers = list(self.dbc.find(query, projection)) for container in containers: container['_id'] = str(container['_id']) return containers class Container(RequestHandler): def _get(self, _id, min_role=None): # TODO: take projection arg for added effiency; use empty projection for access checks container = self.dbc.find_one({'_id': _id}) if not container: self.abort(404, 'no such ' + self.__class__.__name__) if self.uid is None: if not container.get('public', False): self.abort(403, 'this ' + self.__class__.__name__ + 'is not public') del container['permissions'] elif not self.superuser_request: user_perm = None for perm in container['permissions']: if perm['_id'] == self.uid and perm.get('site') == self.source_site: user_perm = perm break else: self.abort(403, self.uid + ' does not have permissions on this ' + self.__class__.__name__) if min_role and INTEGER_ROLES[user_perm['access']] < INTEGER_ROLES[min_role]: self.abort(403, self.uid + ' does not have at least ' + min_role + ' permissions on this ' + self.__class__.__name__) if user_perm['access'] not in ['admin', 'modify']: # if not admin or modify, mask permissions of other users container['permissions'] = [user_perm] if self.request.get('paths').lower() in ('1', 'true'): for file_info in container['files']: file_info['path'] = str(_id)[-3:] + '/' + str(_id) + '/' + file_info['name'] + file_info['ext'] container['_id'] = str(container['_id']) return container class AcquisitionAccessChecker(object): def check_acq_list(self, acq_ids): if not self.superuser_request: for a_id in acq_ids: agg_res = self.app.db.acquisitions.aggregate([ {'$match': {'_id': a_id}}, {'$project': {'permissions': 1}}, {'$unwind': '$permissions'}, ])['result'] if not agg_res: self.abort(404, 'Acquisition %s does not exist' % a_id) for perm_doc in agg_res: if perm_doc['permissions']['_id'] == self.uid: break else: self.abort(403, self.uid + ' does not have permissions on Acquisition %s' % a_id)