diff --git a/epochs.py b/epochs.py index 34629c898fd07d2b40238a8ac9a998953dd7241e..14ef8273928d057c24548d4a92e8701f802e635d 100644 --- a/epochs.py +++ b/epochs.py @@ -9,6 +9,8 @@ import nimsapiutil class Epochs(nimsapiutil.NIMSRequestHandler): + """/nimsapi/epochs """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Epoch List', @@ -43,15 +45,15 @@ class Epochs(nimsapiutil.NIMSRequestHandler): } } - def count(self, iid): + def count(self): """Return the number of Epochs.""" self.response.write(json.dumps(self.app.db.epochs.count())) - def post(self, iid): + def post(self): """Create a new Epoch.""" self.response.write('epochs post\n') - def get(self, iid, sid): + def get(self, sid): """Return the list of Session Epochs.""" session = self.app.db.sessions.find_one({'_id': bson.objectid.ObjectId(sid)}) if not session: @@ -66,13 +68,15 @@ class Epochs(nimsapiutil.NIMSRequestHandler): epochs = list(self.app.db.epochs.find(query, projection)) self.response.write(json.dumps(epochs, default=bson.json_util.default)) - def put(self, iid): + def put(self): """Update many Epochs.""" self.response.write('epochs put\n') class Epoch(nimsapiutil.NIMSRequestHandler): + """/nimsapi/epochs/<eid> """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Epoch', @@ -209,7 +213,7 @@ class Epoch(nimsapiutil.NIMSRequestHandler): 'required': ['_id'], } - def get(self, iid, eid): + def get(self, eid): """Return one Epoch, conditionally with details.""" epoch = self.app.db.epochs.find_one({'_id': bson.objectid.ObjectId(eid)}) if not epoch: @@ -224,10 +228,10 @@ class Epoch(nimsapiutil.NIMSRequestHandler): self.abort(403) self.response.write(json.dumps(epoch, default=bson.json_util.default)) - def put(self, iid, eid): + def put(self, eid): """Update an existing Epoch.""" self.response.write('epoch %s put, %s\n' % (epoch_id, self.request.params)) - def delete(self, iid, eid): + def delete(self, eid): """Delete an Epoch.""" self.response.write('epoch %s delete, %s\n' % (epoch_id, self.request.params)) diff --git a/experiments.py b/experiments.py index e617c338c2ad24416204a91c4dcdbad53b9db279..c59785790ed8412b910a51779f2c3445b4903368 100644 --- a/experiments.py +++ b/experiments.py @@ -9,6 +9,8 @@ import nimsapiutil class Experiments(nimsapiutil.NIMSRequestHandler): + """/experiments """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Experiment List', @@ -35,15 +37,15 @@ class Experiments(nimsapiutil.NIMSRequestHandler): } } - def count(self, iid): + def count(self): """Return the number of Experiments.""" self.response.write(json.dumps(self.app.db.experiments.count())) - def post(self, iid): + def post(self): """Create a new Experiment.""" self.response.write('experiments post\n') - def get(self, iid): + def get(self): """Return the list of Experiments.""" query = {'permissions.' + self.userid: {'$exists': 'true'}} if not self.user_is_superuser else None projection = ['group', 'name', 'permissions.'+self.userid] @@ -57,13 +59,15 @@ class Experiments(nimsapiutil.NIMSRequestHandler): exp['timestamp'] = timestamps[exp['_id']] self.response.write(json.dumps(experiments, default=bson.json_util.default)) - def put(self, iid): + def put(self): """Update many Experiments.""" self.response.write('experiments put\n') class Experiment(nimsapiutil.NIMSRequestHandler): + """/experiments/<xid> """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Experiment', @@ -99,7 +103,7 @@ class Experiment(nimsapiutil.NIMSRequestHandler): 'required': ['_id', 'group', 'name'], } - def get(self, iid, xid): + def get(self, xid): """Return one Experiment, conditionally with details.""" experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(xid)}) if not experiment: @@ -115,10 +119,10 @@ class Experiment(nimsapiutil.NIMSRequestHandler): experiment['permissions'] = {self.userid: experiment['permissions'][self.userid]} self.response.write(json.dumps(experiment, default=bson.json_util.default)) - def put(self, iid, xid): + def put(self, xid): """Update an existing Experiment.""" self.response.write('experiment %s put, %s\n' % (exp_id, self.request.params)) - def delete(self, iid, xid): + def delete(self, xid): """Delete an Experiment.""" self.response.write('experiment %s delete, %s\n' % (exp_id, self.request.params)) diff --git a/nimsapi.py b/nimsapi.py index 030afccd00346ddfeb7cb28512ef3888155a05a3..7dbf0145d3f72f27b048f49144bc6f2d7819ad0d 100755 --- a/nimsapi.py +++ b/nimsapi.py @@ -28,6 +28,8 @@ log = logging.getLogger('nimsapi') class NIMSAPI(nimsapiutil.NIMSRequestHandler): + """/nimsapi """ + def head(self): """Return 200 OK.""" self.response.set_status(200) @@ -63,7 +65,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): self.response.write('</style>\n') self.response.write('</head>\n') self.response.write('<body>\n') - self.response.write('<p>nimsapi - ' + self.app.config['site_id'] + '</p>\n') + self.response.write('<h1>nimsapi - ' + self.app.config['site_id'] + '</h1>\n') self.response.write('<table class="tftable" border="1">\n') self.response.write('<tr><th>Resource</th><th>Description</th></tr>\n') for r, d in resources: @@ -108,6 +110,8 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): class Users(nimsapiutil.NIMSRequestHandler): + """/nimsapi/users """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'User List', @@ -144,27 +148,29 @@ class Users(nimsapiutil.NIMSRequestHandler): } } - def count(self, iid): + def count(self): """Return the number of Users.""" self.response.write('%d users\n' % self.app.db.users.count()) - def post(self, iid): + def post(self): """Create a new User""" self.response.write('users post\n') - def get(self, iid): + def get(self): """Return the list of Users.""" projection = ['firstname', 'lastname', 'email_hash'] users = list(self.app.db.users.find({}, projection)) self.response.write(json.dumps(users, default=bson.json_util.default)) - def put(self, iid): + def put(self): """Update many Users.""" self.response.write('users put\n') class User(nimsapiutil.NIMSRequestHandler): + """/nimsapi/users/<uid> """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'User', @@ -202,12 +208,12 @@ class User(nimsapiutil.NIMSRequestHandler): 'required': ['_id'], } - def get(self, iid, uid): + def get(self, uid): """Return User details.""" user = self.app.db.users.find_one({'_id': uid}) self.response.write(json.dumps(user, default=bson.json_util.default)) - def put(self, iid, uid): + def put(self, uid): """Update an existing User.""" user = self.app.db.users.find_one({'_id': uid}) if not user: @@ -229,13 +235,15 @@ class User(nimsapiutil.NIMSRequestHandler): self.abort(403) self.response.write(json.dumps(user, default=bson.json_util.default) + '\n') - def delete(self, iid, uid): + def delete(self, uid): """Delete an User.""" self.response.write('user %s delete, %s\n' % (uid, self.request.params)) class Groups(nimsapiutil.NIMSRequestHandler): + """/nimsapi/groups """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Group List', @@ -252,27 +260,29 @@ class Groups(nimsapiutil.NIMSRequestHandler): } } - def count(self, iid): + def count(self): """Return the number of Groups.""" self.response.write('%d groups\n' % self.app.db.groups.count()) - def post(self, iid): + def post(self): """Create a new Group""" self.response.write('groups post\n') - def get(self, iid): + def get(self): """Return the list of Groups.""" projection = ['_id'] groups = list(self.app.db.groups.find({}, projection)) self.response.write(json.dumps(groups, default=bson.json_util.default)) - def put(self, iid): + def put(self): """Update many Groups.""" self.response.write('groups put\n') class Group(nimsapiutil.NIMSRequestHandler): + """/nimsapi/groups/<gid>""" + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Group', @@ -313,16 +323,16 @@ class Group(nimsapiutil.NIMSRequestHandler): 'required': ['_id'], } - def get(self, iid, gid): + def get(self, gid): """Return Group details.""" group = self.app.db.groups.find_one({'_id': gid}) self.response.write(json.dumps(group, default=bson.json_util.default)) - def put(self, iid, gid): + def put(self, gid): """Update an existing Group.""" self.response.write('group %s put, %s\n' % (gid, self.request.params)) - def delete(self, iid, gid): + def delete(self, gid): """Delete an Group.""" @@ -385,8 +395,6 @@ routes = [ webapp2.Route(r'/dump', NIMSAPI, handler_method='dump', methods=['GET']), webapp2.Route(r'/upload', NIMSAPI, handler_method='upload', methods=['PUT']), webapp2.Route(r'/remotes', Remotes), - ]), - webapp2_extras.routes.PathPrefixRoute(r'/nimsapi/<iid:[^/]+>', [ webapp2.Route(r'/users', Users), webapp2.Route(r'/users/count', Users, handler_method='count', methods=['GET']), webapp2.Route(r'/users/listschema', Users, handler_method='schema', methods=['GET']), @@ -422,6 +430,7 @@ app = webapp2.WSGIApplication(routes, debug=True) if __name__ == '__main__': args = ArgumentParser().parse_args() nimsutil.configure_log(args.logfile, not args.quiet, args.loglevel) + # TODO: if pubkey not specified, throw log.warning from paste import httpserver app.config = dict(stage_path=args.stage_path, site_id=args.uid, pubkey=args.pubkey) diff --git a/nimsapiutil.py b/nimsapiutil.py index 231134761e95fa325ed45785d96b6a7b45827a0e..c45c41d66a462deaadb830bed5e5ee03b3cbf9d3 100644 --- a/nimsapiutil.py +++ b/nimsapiutil.py @@ -8,11 +8,12 @@ import webapp2 import datetime import requests import bson.json_util -from Crypto.Hash import HMAC -from Crypto.Random import random +import Crypto.Hash.HMAC +import Crypto.Random.random log = logging.getLogger('nimsapi') - +requests_log = logging.getLogger('requests') # configure Requests logging +requests_log.setLevel(logging.WARNING) # set level to WARNING (default is INFO) class NIMSRequestHandler(webapp2.RequestHandler): @@ -62,16 +63,12 @@ class NIMSRequestHandler(webapp2.RequestHandler): self.user = self.app.db.users.find_one({'_id': self.userid}) self.user_is_superuser = self.user.get('superuser') self.response.headers['Content-Type'] = 'application/json' - try: - self.target_id = request.route_kwargs['iid'] # for what site is the request meant - except KeyError: - self.target_id = 'local' # change to site_id? - self.site_id = self.app.config['site_id'] # what is THIS site - self.pubkey = open(self.app.config['pubkey']).read() if self.app.config['pubkey'] is not None else None + self.target_id = self.request.get('iid', None) + self.site_id = self.app.config['site_id'] # requests coming from another NIMS instance are dealt with differently if self.request.user_agent.startswith('NIMS Instance'): - log.info("request from '{0}', interNIMS p2p initiated".format(self.request.user_agent)) + log.debug("request from '{0}', interNIMS p2p initiated".format(self.request.user_agent)) try: authinfo = self.request.headers['authorization'] challenge_id, digest = base64.b64decode(authinfo).split() @@ -85,36 +82,44 @@ class NIMSRequestHandler(webapp2.RequestHandler): # purge challenge (challenges are single use) self.app.db.challenges.remove({'_id': challenge_id}) # verify - h = HMAC.new(remote_pubkey, challenge) + h = Crypto.Hash.HMAC.new(remote_pubkey, challenge) self.expected = base64.b64encode('%s %s' % (challenge_id, h.hexdigest())) + log.debug('recieved: %s' % authinfo) + log.debug('expected: %s' % self.expected) if self.expected == authinfo: - log.info('CRAM successful') + log.debug('CRAM response accepted - %s authenticated' % challenge_id) else: self.abort(403, 'Not Authorized: cram failed') - except KeyError, e: + except KeyError as e: # send a 401 with a fresh challenge cid = self.request.get('cid') - if not cid: self.abort(403, 'challenge_id not in payload') + if not cid: self.abort(403, 'cid, challenge_id, required') challenge = {'_id': cid, - 'challenge': str(random.getrandbits(128)), + 'challenge': str(Crypto.Random.random.getrandbits(128)), 'timestamp': datetime.datetime.now()} # upsert challenge with time of creation - spam = self.app.db.challenges.find_and_modify(query={'_id': cid}, update=challenge, upsert=True, new=True) + self.app.db.challenges.find_and_modify(query={'_id': cid}, update=challenge, upsert=True, new=True) # send 401 + challenge in 'www-authenticate' header self.response.headers['www-authenticate'] = base64.b64encode(challenge['challenge']) self.response.set_status(401) + log.debug('issued challenge to %s; %s' % (cid, challenge['challenge'])) def dispatch(self): """dispatching and request forwarding""" - if self.target_id in ['local', self.site_id]: - log.info('{0} delegating to local {1}'.format(socket.gethostname(), self.request.url)) + if self.target_id in [None, self.site_id]: + log.debug('{0} delegating to local {1}'.format(socket.gethostname(), self.request.url)) super(NIMSRequestHandler, self).dispatch() else: - log.info('{0} delegating to remote {1}'.format(socket.gethostname(), self.target_id)) + # WORK ON THIS SPOT + # capture error, and log. + self.pubkey = open(self.app.config['pubkey']).read() + # check if pubkey specified, throw error + + log.debug('{0} delegating to remote {1}'.format(socket.gethostname(), self.target_id)) # is target registered? target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id':False, 'hostname':True}) if not target: - log.info('remote host {0} not in auth list. DENIED'.format(self.target_id)) + log.debug('remote host {0} not in auth list. DENIED'.format(self.target_id)) self.abort(403, 'forbidden: site is not registered with interNIMS') self.cid = self.userid + ':' + self.site_id reqheaders = dict(self.request.headers) @@ -125,16 +130,17 @@ class NIMSRequestHandler(webapp2.RequestHandler): target_api = 'http://{0}{1}?{2}'.format(target['hostname'], self.request.path, self.request.query_string) reqparams = {'cid': self.cid} + # TODO: error handling for host-down/host-unreachable # first attempt, expect 401, send as little as possible... - r = requests.request(method=self.request.method, url=target_api, params=reqparams, headers=reqheaders, cookies=self.request.cookies) + r = requests.request(method=self.request.method, url=target_api, params=reqparams, headers=reqheaders) if r.status_code == 401: challenge = base64.b64decode(r.headers['www-authenticate']) - # log.info('challenge {0} recieved'.format(challenge)) - h = HMAC.new(self.pubkey, challenge) + log.debug('Authorization requested - challenge: %s' % challenge) + h = Crypto.Hash.HMAC.new(self.pubkey, challenge) response = base64.b64encode('%s %s' % (self.cid, h.hexdigest())) - # log.info('sending: {0} {1}'.format(self.cid, h.hexdigest())) - #adjust the request and try again + log.debug('response: %s %s' % (self.cid, h.hexdigest())) + log.debug('b4encoded: %s' % response) reqheaders['authorization'] = response r = requests.request(method=self.request.method, url=target_api, params=reqparams, data=self.request.body, headers=reqheaders, cookies=self.request.cookies) diff --git a/sessions.py b/sessions.py index 4b69170606bcffedb8f60fcd357bbc247ca5beda..d0c43ce497be85b932dffe2af7c40a3c25300e09 100644 --- a/sessions.py +++ b/sessions.py @@ -9,6 +9,8 @@ import nimsapiutil class Sessions(nimsapiutil.NIMSRequestHandler): + """/sessions """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Session List', @@ -31,15 +33,15 @@ class Sessions(nimsapiutil.NIMSRequestHandler): } } - def count(self, iid): + def count(self): """Return the number of Sessions.""" self.response.write(json.dumps(self.app.db.sessions.count())) - def post(self, iid): + def post(self): """Create a new Session""" self.response.write('sessions post\n') - def get(self, iid, xid): + def get(self, xid): """Return the list of Experiment Sessions.""" experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(xid)}) if not experiment: @@ -51,13 +53,15 @@ class Sessions(nimsapiutil.NIMSRequestHandler): sessions = list(self.app.db.sessions.find(query, projection)) self.response.write(json.dumps(sessions, default=bson.json_util.default)) - def put(self, iid): + def put(self): """Update many Sessions.""" self.response.write('sessions put\n') class Session(nimsapiutil.NIMSRequestHandler): + """/sessions/<sid> """ + json_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'title': 'Session', @@ -106,7 +110,7 @@ class Session(nimsapiutil.NIMSRequestHandler): 'required': ['_id', 'experiment', 'uid', 'patient_id', 'subject'], } - def get(self, iid, sid): + def get(self, sid): """Return one Session, conditionally with details.""" session = self.app.db.sessions.find_one({'_id': bson.objectid.ObjectId(sid)}) if not session: @@ -118,15 +122,15 @@ class Session(nimsapiutil.NIMSRequestHandler): self.abort(403) self.response.write(json.dumps(session, default=bson.json_util.default)) - def put(self, iid, sid): + def put(self, sid): """Update an existing Session.""" self.response.write('session %s put, %s\n' % (sid, self.request.params)) - def delete(self, iid, sid): + def delete(self, sid): """Delete an Session.""" self.response.write('session %s delete, %s\n' % (sid, self.request.params)) - def move(self, iid, sid): + def move(self, sid): """ Move a Session to another Experiment.