Skip to content
Snippets Groups Projects
nimsapiutil.py 9.62 KiB
Newer Older
# @author:  Gunnar Schaefer, Kevin S. Hahn
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
log = logging.getLogger('nimsapi')
logging.getLogger('requests').setLevel(logging.WARNING)                  # silence Requests library logging
INTEGER_ROLES = {
        'anon-read':  0,
        'read-only':  1,
        'read-write': 2,
        'admin':      3,
        }

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.uid = '@public'  # @public is default user
        self.access_token = self.request.headers.get('Authorization', None)
        log.debug('accesstoken: ' + str(self.access_token))

Kevin S. Hahn's avatar
Kevin S. Hahn committed
        if self.access_token and self.app.config['oauth2_id_endpoint']:
            r = requests.request(method='GET', url = self.app.config['oauth2_id_endpoint'], headers={'Authorization': 'Bearer ' + self.access_token})
            if r.status_code == 200:
                oauth_user = json.loads(r.content)
                self.uid = oauth_user['email']
                log.debug('oauth user: ' + oauth_user['email'])
            else:
                #TODO: add handlers for bad tokens.
                log.debug('ERR: ' + str(r.status_code) + ' 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')

        self.user = self.app.db.users.find_one({'uid': self.uid})
Kevin S. Hahn's avatar
Kevin S. Hahn committed
        self.user_is_superuser = self.user.get('superuser', None) if self.user else False

        # p2p request
        self.target_id = self.request.get('iid', None)
        self.p2p_user = self.request.headers.get('X-From', None)
        self.site_id = self.app.config['site_id']
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        self.ssl_key = self.app.config['ssl_key']
        log.debug('X-From: ' + str(self.p2p_user))

        # CORS bare minimum
        self.response.headers.add('Access-Control-Allow-Origin', self.request.headers.get('origin', '*'))
        if not self.request.path.endswith('/nimsapi/log'):
            log.info(self.request.method + ' ' + self.request.path + ' ' + str(self.request.params.mixed()))
    def dispatch(self):
        """dispatching and request forwarding"""
        # dispatch to local instance
        if self.target_id in [None, self.site_id]:
            # request originates from remote instance
            if self.request.user_agent.startswith('NIMS Instance'):
                # is the requester an authorized remote site
                requester = self.request.user_agent.replace('NIMS Instance', '').strip()
                target = self.app.db.remotes.find_one({'_id':requester})
                if not target:
                    log.debug('remote host ' + requester + ' not in auth list. DENIED')
                    self.abort(403, requester + ' is not authorized')
                log.debug('request from ' + self.request.user_agent + ', interNIMS p2p initiated')
Gunnar Schaefer's avatar
Gunnar Schaefer committed
                # assemble msg to be hashed and verify signature
                self.signature = base64.b64decode(self.request.headers.get('X-Signature'))
                msg = self.request.method + self.request.path + str(dict(self.request.params)) + self.request.body + self.request.headers.get('Date')
                key = Crypto.PublicKey.RSA.importKey(target['pubkey'])
                h = Crypto.Hash.SHA.new(msg)
                verifier = Crypto.Signature.PKCS1_v1_5.new(key)
Gunnar Schaefer's avatar
Gunnar Schaefer committed
                if not verifier.verify(h, self.signature):
                    log.debug('remote message/signature is not authentic')
                    self.abort(403, 'remote message/signature is not authentic')
            return super(NIMSRequestHandler, self).dispatch()
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        # delegate to remote instance
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        elif self.ssl_key is not None and self.site_id is not None:
            log.debug('dispatching to remote ' + self.target_id)
            # is target registered?
            target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id':False, 'api_uri':True})
            if not target:
                log.debug('remote host ' + self.target_id + ' not in auth list. DENIED')
                self.abort(403, self.target_id + 'is not authorized')

            # adjust headers
            headers = self.request.headers
            headers['User-Agent'] = 'NIMS Instance ' + self.site_id
            headers['X-From'] = self.uid
            headers['Content-Length'] = len(self.request.body)
            del headers['Host']                                                 # delete old host destination
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            if 'Authorization' in headers:
                del headers['Authorization']                                    # delete access_token, if present
            # assemble msg to be hashed
            nonce = str(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
            headers['Date'] = nonce
            msg = self.request.method + self.request.path + str(dict(self.request.params)) + self.request.body + nonce
            # create a signature
            h = Crypto.Hash.SHA.new(msg)
Gunnar Schaefer's avatar
Gunnar Schaefer committed
            signature = Crypto.Signature.PKCS1_v1_5.new(self.ssl_key).sign(h)
            headers['X-Signature'] = base64.b64encode(signature)

            # construct outgoing request
            target_api = 'https://' + target['api_uri'] + self.request.path.split('/nimsapi')[1]
            r = requests.request(method=self.request.method, data=self.request.body, url=target_api, params=self.request.params, headers=headers, verify=False)

            # return response content
Kevin S. Hahn's avatar
Kevin S. Hahn committed
            # TODO: think about: are the headers even useful?
            self.response.write(r.content)
Gunnar Schaefer's avatar
Gunnar Schaefer committed
        elif self.ssl_key is None or self.site_id is None:
            log.debug('ssl key or site id undefined, cannot dispatch to remote')
Gunnar Schaefer's avatar
Gunnar Schaefer committed
    def schema(self):
        return self.json_schema
    def get_collection(self, cid, min_role='anon-read'):
        collection = self.app.db.collections.find_one({'_id': cid})
        if not collection:
            self.abort(404)
        if not self.user_is_superuser:
            for perm in collection['permissions']:
                if perm['uid'] == self.uid:
                    break
            else:
                self.abort(403, self.uid + ' does not have permission to this Collection')
            if INTEGER_ROLES[perm['role']] < INTEGER_ROLES[min_role]:
                self.abort(403, self.uid + ' does not have at least ' + min_role + ' on this Collection')
            if perm['role'] != 'admin': # if not admin, mask all other permissions
                    collection['permissions'] = [{'uid': self.uid, 'role': perm['role']}]
        return collection

    def get_experiment(self, xid, min_role='anon-read'):
        experiment = self.app.db.experiments.find_one({'_id': xid})
        if not experiment:
            self.abort(404)
        if not self.user_is_superuser:
            for perm in experiment['permissions']:
                if perm['uid'] == self.uid:
                    break
            else:
                self.abort(403, self.uid + ' does not have permission to this Experiment')
            if INTEGER_ROLES[perm['role']] < INTEGER_ROLES[min_role]:
                self.abort(403, self.uid + ' does not have at least ' + min_role + ' on this Experiment')
            if perm['role'] != 'admin': # if not admin, mask all other permissions
                    experiment['permissions'] = [{'uid': self.uid, 'role': perm['role']}]
        return experiment

    def get_session(self, sid, min_role='anon-read'):
        #FIXME: implement min_role logic
        session = self.app.db.sessions.find_one({'_id': sid})
        if not session:
            self.abort(404)
        experiment = self.app.db.experiments.find_one({'_id': session['experiment']})
        if not experiment:
            self.abort(500)
        if not self.user_is_superuser:
            for perm in experiment['permissions']:
                if perm['uid'] == self.uid:
                    break
            else:
                self.abort(403, 'user does not have permission to this Session')
        return session

    def get_epoch(self, eid, min_role='anon-read'):
        #FIXME: implement min_role logic
        epoch = self.app.db.epochs.find_one({'_id': eid})
        if not epoch:
            self.abort(404)
        session = self.app.db.sessions.find_one({'_id': epoch['session']})
        if not session:
            self.abort(500)
        experiment = self.app.db.experiments.find_one({'_id': session['experiment']})
        if not experiment:
            self.abort(500)
        if not self.user_is_superuser:
            for perm in experiment['permissions']:
                if perm['uid'] == self.uid:
                    break
            else:
                self.abort(403, 'user does not have permission to this Epoch')
        return epoch