Skip to content
Snippets Groups Projects
nimsapiutil.py 12 KiB
Newer Older
# @author:  Gunnar Schaefer, Kevin S. Hahn
Gunnar Schaefer's avatar
Gunnar Schaefer committed
import logging
log = logging.getLogger('nimsapi')
Gunnar Schaefer's avatar
Gunnar Schaefer committed
logging.getLogger('urllib3').setLevel(logging.WARNING) # silence Requests library logging
Gunnar Schaefer's avatar
Gunnar Schaefer committed

import json
import base64
import datetime
import requests
import bson.json_util
import Crypto.Hash.SHA
import Crypto.PublicKey.RSA
import Crypto.Signature.PKCS1_v1_5
ROLES = [
        {
            'rid': 'anon-read',
            'name': 'Anonymized',
            'sort': 0,
            'public': True,
            },
        {
            'rid': 'read-only',
            'name': 'Read-Only',
            'sort': 1,
            'public': True,
            },
        {
            'rid': 'read-write',
            'name': 'Read-Write',
            'sort': 2,
            },
        {
            'rid': 'admin',
            'name': 'Admin',
            'sort': 3,
            },
        ]

INTEGER_ROLES = {r['rid']: r['sort'] for r in ROLES}
class NIMSRequestHandler(webapp2.RequestHandler):

    """fetches pubkey from own self.db.remotes. needs to be aware of OWN site uid"""

    json_schema = None

    file_schema = {
        'title': 'File',
        'type': 'object',
        'properties': {
            'type': {
                'title': 'Type',
                'type': 'array',
            },
            'filename': {
                'title': 'File Name',
                'type': 'string',
            },
            'ext': {
                'title': 'File Name Extension',
                'type': 'string',
            },
            'md5': {
                'title': 'MD5',
                'type': 'string',
            },
            'size': {
                'title': 'Size',
                'type': 'integer',
            },
        }
    }

    def __init__(self, request=None, response=None):
        self.initialize(request, response)
        self.target_site = self.request.get('site', None)
        self.access_token = self.request.headers.get('Authorization', None)
        self.source_site = None       # requesting remote site; gets set if request from remote
Kevin S. Hahn's avatar
Kevin S. Hahn committed

        # CORS header
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        if 'Origin' in self.request.headers and self.request.headers['Origin'].startswith('https://'):
            self.response.headers['Access-Control-Allow-Origin'] = self.request.headers['Origin']
Kevin S. Hahn's avatar
Kevin S. Hahn committed
        if self.access_token and self.app.config['oauth2_id_endpoint']:
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            r = requests.get(self.app.config['oauth2_id_endpoint'], headers={'Authorization': 'Bearer ' + self.access_token})
            if r.status_code == 200:
Kevin S. Hahn's avatar
Kevin S. Hahn committed
                self.uid = json.loads(r.content)['email']
Kevin S. Hahn's avatar
Kevin S. Hahn committed
                # TODO: add handlers for bad tokens
                # inform app of expired token, app will try to get new token, or ask user to log in again
                log.debug('ERR: ' + str(r.status_code) + r.reason + ' bad token')
        elif self.app.config['insecure'] and 'X-Requested-With' not in self.request.headers and self.request.get('user', None):
            self.uid = self.request.get('user')
Kevin S. Hahn's avatar
Kevin S. Hahn committed
        else:
            self.uid = '@public'
            self.user_is_superuser = False

        if self.uid != '@public':
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            user = self.app.db.users.find_one({'_id': self.uid}, ['superuser'])
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            if user:
                self.user_is_superuser = user.get('superuser', None)
            else:
Gunnar Schaefer's avatar
Gunnar Schaefer committed
                self.abort(403, 'user ' + self.uid + ' does not exist')
        if self.target_site not in [None, self.app.config['site_id']]:
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            self.rtype = 'to_remote'
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            if not self.app.config['site_id']:
                self.abort(500, 'api site_id is not configured')
            if not self.app.config['ssl_key']:
                self.abort(500, 'api ssl_key is not configured')
            target = self.app.db.remotes.find_one({'_id': self.target_site}, {'_id': False, 'api_uri': True})
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            if not target:
                self.abort(402, 'remote host ' + self.target_site + ' is not an authorized remote')
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            # adjust headers
            self.headers = self.request.headers
            self.headers['User-Agent'] = 'NIMS Instance ' + self.app.config['site_id']
            self.headers['X-From'] = (self.uid + '#' + self.app.config['site_id']) if self.uid != '@public' else self.uid
            self.headers['Content-Length'] = len(self.request.body)
            self.headers['Date'] = str(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))   # Nonce for msg
            del self.headers['Host']
            if self.headers.get('Authorization'): del self.headers['Authorization']

            # adjust params
            self.params = self.request.params.mixed()
            if self.params.get('user'): del self.params['user']
            del self.params['site']
Kevin S. Hahn's avatar
Kevin S. Hahn committed

            # assemble msg, hash, and signature
            msg = self.request.method + self.request.path + str(self.params) + self.request.body + self.headers.get('Date')
            signature = Crypto.Signature.PKCS1_v1_5.new(self.app.config['ssl_key']).sign(Crypto.Hash.SHA.new(msg))
            self.headers['X-Signature'] = base64.b64encode(signature)

            # prepare delegated request URI
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            self.target_uri = target['api_uri'] + self.request.path.split('/nimsapi')[1]
Kevin S. Hahn's avatar
Kevin S. Hahn committed

        elif self.request.user_agent.startswith('NIMS Instance'):
            self.rtype = 'from_remote'

            # store remote user info into self.uid and self.source_site
            self.uid, self.source_site = self.request.headers.get('X-From').split('#')
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            self.user_is_superuser = False

            remote_instance = self.request.user_agent.replace('NIMS Instance', '').strip()
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            requester = self.app.db.remotes.find_one({'_id': remote_instance})
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            if not requester:
Gunnar Schaefer's avatar
Gunnar Schaefer committed
                self.abort(402, remote_instance + ' is not authorized')
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            # assemble msg, hash, and verify received signature
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            signature = base64.b64decode(self.request.headers.get('X-Signature'))
            msg = self.request.method + self.request.path + str(self.request.params.mixed()) + self.request.body + self.request.headers.get('Date')
            verifier = Crypto.Signature.PKCS1_v1_5.new(Crypto.PublicKey.RSA.importKey(requester['pubkey']))
            if not verifier.verify(Crypto.Hash.SHA.new(msg), signature):
Gunnar Schaefer's avatar
Gunnar Schaefer committed
                self.abort(402, 'remote message/signature is not authentic')
Kevin S. Hahn's avatar
Kevin S. Hahn committed
        else:
            self.rtype = 'local'

    def dispatch(self):
        """dispatching and request forwarding"""
        log.debug('%s %s %s %s %s %s' % (self.rtype, self.uid, self.source_site, self.request.method, self.request.path, str(self.request.params.mixed())))
Kevin S. Hahn's avatar
Kevin S. Hahn committed
        if self.rtype in ['local', 'from_remote']:
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            return super(NIMSRequestHandler, self).dispatch()
Kevin S. Hahn's avatar
Kevin S. Hahn committed
        else:
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            if self.request.method == 'OPTIONS':
                return self.options()
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            r = requests.request(self.request.method, self.target_uri, params=self.params, data=self.request.body, headers=self.headers)
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            if r.status_code != 200:
                self.abort(r.status_code, 'InterNIMS p2p err: ' + r.reason)
            self.response.write(r.content)
Gunnar Schaefer's avatar
Gunnar Schaefer committed
    def abort(self, code, *args, **kwargs):
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        log.warning(str(code) + ' ' + '; '.join(args))
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        if 'Access-Control-Allow-Origin' in self.response.headers:
            headers = kwargs.setdefault('headers', {})
            headers['Access-Control-Allow-Origin'] = self.response.headers['Access-Control-Allow-Origin']
        webapp2.abort(code, *args, **kwargs)

    def options(self, *args, **kwargs):
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        self.response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, POST, PUT, DELETE, OPTIONS'
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        self.response.headers['Access-Control-Allow-Headers'] = 'Authorization'
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        self.response.headers['Access-Control-Max-Age'] = '151200'
Gunnar Schaefer's avatar
Gunnar Schaefer committed
    def schema(self):
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        if self.request.method == 'OPTIONS':
            return self.options()
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        return self.json_schema
    def get_collection(self, cid, min_role=None):
        collection = self.app.db.collections.find_one({'_id': cid})
        if not collection:
            self.abort(404, 'no such Collection')
        if not self.user_is_superuser:
            coll = self.app.db.collections.find_one({'_id': cid, 'permissions': {'$elemMatch': {'uid': self.uid, 'site': self.source_site}}}, ['permissions.$'])
            if not coll:
                self.abort(403, self.uid + ' does not have permissions on this Collection')
            if min_role and INTEGER_ROLES[coll['permissions'][0]['role']] < INTEGER_ROLES[min_role]:
                self.abort(403, self.uid + ' does not have at least ' + min_role + ' permissions on this Collection')
            if coll['permissions'][0]['role'] != 'admin': # if not admin, mask permissions of other users
                collection['permissions'] = coll['permissions']
        for i, perm in enumerate(collection['permissions']):
            if perm['uid'] == '@public':
                collection['public'] = perm['role']
                collection['permissions'].pop(i)
                break
        return collection

    def get_experiment(self, xid, min_role=None):
        experiment = self.app.db.experiments.find_one({'_id': xid})
        if not experiment:
            self.abort(404, 'no such Experiment')
        if not self.user_is_superuser:
            exp = self.app.db.experiments.find_one({'_id': xid, 'permissions': {'$elemMatch': {'uid': self.uid, 'site': self.source_site}}}, ['permissions.$'])
            if not exp:
                self.abort(403, self.uid + ' does not have permissions on this Experiment')
            if min_role and INTEGER_ROLES[exp['permissions'][0]['role']] < INTEGER_ROLES[min_role]:
                self.abort(403, self.uid + ' does not have at least ' + min_role + ' permissions on this Experiment')
            if exp['permissions'][0]['role'] != 'admin': # if not admin, mask permissions of other users
                experiment['permissions'] = exp['permissions']
        for i, perm in enumerate(experiment['permissions']):
            if perm['uid'] == '@public':
                experiment['public'] = perm['role']
                experiment['permissions'].pop(i)
                break
        return experiment

    def get_session(self, sid, min_role=None):
        session = self.app.db.sessions.find_one({'_id': sid})
        if not session:
            self.abort(404, 'no such Session')
        if not self.user_is_superuser:
            experiment = self.app.db.experiments.find_one({'_id': session['experiment'], 'permissions': {'$elemMatch': {'uid': self.uid, 'site': self.source_site}}}, ['permissions.$'])
            if not experiment:
                if not self.app.db.experiments.find_one({'_id': session['experiment']}, []):
                    self.abort(500)
                else:
                    self.abort(403, self.uid + ' does not have permissions to this Session')
            if min_role and INTEGER_ROLES[experiment['permissions'][0]['role']] < INTEGER_ROLES[min_role]:
                self.abort(403, self.uid + ' does not have at least ' + min_role + ' permissions on this Session')
        return session

    def get_epoch(self, eid, min_role=None):
        epoch = self.app.db.epochs.find_one({'_id': eid})
        if not epoch:
            self.abort(404, 'no such Epoch')
        if not self.user_is_superuser:
            session = self.app.db.sessions.find_one({'_id': epoch['session']}, ['experiment'])
            if not session:
                self.abort(500)
            experiment = self.app.db.experiments.find_one({'_id': session['experiment'], 'permissions': {'$elemMatch': {'uid': self.uid, 'site': self.source_site}}}, ['permissions.$'])
            if not experiment:
                if not self.app.db.experiments.find_one({'_id': session['experiment']}, []):
                    self.abort(500)
                else:
                    self.abort(403, self.uid + ' does not have permissions on this Epoch')
            if min_role and INTEGER_ROLES[experiment['permissions'][0]['role']] < INTEGER_ROLES[min_role]:
                self.abort(403, self.uid + ' does not have at least ' + min_role + ' permissions on this Epoch')
        return epoch