Skip to content
Snippets Groups Projects
Commit c518a637 authored by Kevin S. Hahn's avatar Kevin S. Hahn
Browse files

revisions to nimsapi URL routes

parent c31d68f0
No related branches found
No related tags found
No related merge requests found
......@@ -9,17 +9,17 @@ import nimsapiutil
class Epochs(nimsapiutil.NIMSRequestHandler):
def count(self):
def count(self, iid):
"""Return the number of Epochs."""
self.response.write('epochs count\n')
def post(self):
"""Create a new Epoch"""
def post(self, iid):
"""Create a new Epoch."""
self.response.write('epochs post\n')
def get(self, sess_id):
def get(self, iid, sid):
"""Return the list of Session Epochs."""
session = self.app.db.sessions.find_one({'_id': bson.objectid.ObjectId(sess_id)})
session = self.app.db.sessions.find_one({'_id': bson.objectid.ObjectId(sid)})
if not session:
self.abort(404)
experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(session['experiment'])})
......@@ -27,21 +27,21 @@ class Epochs(nimsapiutil.NIMSRequestHandler):
self.abort(500)
if not self.user_is_superuser and self.userid not in experiment['permissions']:
self.abort(403)
query = {'session': bson.objectid.ObjectId(sess_id)}
query = {'session': bson.objectid.ObjectId(sid)}
projection = ['timestamp', 'series', 'acquisition', 'description', 'datatype']
epochs = list(self.app.db.epochs.find(query, projection))
self.response.write(json.dumps(epochs, default=bson.json_util.default))
def put(self):
def put(self, iid):
"""Update many Epochs."""
self.response.write('epochs put\n')
class Epoch(nimsapiutil.NIMSRequestHandler):
def get(self, epoch_id):
def get(self, iid, eid):
"""Return one Epoch, conditionally with details."""
epoch = self.app.db.epochs.find_one({'_id': bson.objectid.ObjectId(epoch_id)})
epoch = self.app.db.epochs.find_one({'_id': bson.objectid.ObjectId(eid)})
if not epoch:
self.abort(404)
session = self.app.db.sessions.find_one({'_id': epoch['session']})
......@@ -54,10 +54,10 @@ class Epoch(nimsapiutil.NIMSRequestHandler):
self.abort(403)
self.response.write(json.dumps(epoch, default=bson.json_util.default))
def put(self, _id):
def put(self, iid, eid):
"""Update an existing Epoch."""
self.response.write('epoch %s put, %s\n' % (_id, self.request.params))
self.response.write('epoch %s put, %s\n' % (epoch_id, self.request.params))
def delete(self, _id):
def delete(self, iid, eid):
"""Delete an Epoch."""
self.response.write('epoch %s delete, %s\n' % (_id, self.request.params))
self.response.write('epoch %s delete, %s\n' % (epoch_id, self.request.params))
......@@ -9,15 +9,15 @@ import nimsapiutil
class Experiments(nimsapiutil.NIMSRequestHandler):
def count(self):
def count(self, iid):
"""Return the number of Experiments."""
self.response.write('%d experiments\n' % self.app.db.experiments.count())
def post(self):
"""Create a new Experiment"""
def post(self, iid):
"""Create a new Experiment."""
self.response.write('experiments post\n')
def get(self):
def get(self, iid):
"""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]
......@@ -31,20 +31,20 @@ class Experiments(nimsapiutil.NIMSRequestHandler):
exp['timestamp'] = timestamps[exp['_id']]
self.response.write(json.dumps(experiments, default=bson.json_util.default))
def put(self):
def put(self, iid):
"""Update many Experiments."""
self.response.write('experiments put\n')
class Experiment(nimsapiutil.NIMSRequestHandler):
def get(self, exp_id):
def get(self, iid, xid):
"""Return one Experiment, conditionally with details."""
experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(exp_id)})
experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(xid)})
if not experiment:
self.abort(404)
experiment['timestamp'] = self.app.db.sessions.aggregate([
{'$match': {'experiment': bson.objectid.ObjectId(exp_id)}},
{'$match': {'experiment': bson.objectid.ObjectId(xid)}},
{'$group': {'_id': '$experiment', 'timestamp': {'$max': '$timestamp'}}},
])['result'][0]['timestamp']
if not self.user_is_superuser:
......@@ -54,10 +54,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, exp_id):
def put(self, iid, xid):
"""Update an existing Experiment."""
self.response.write('experiment %s put, %s\n' % (exp_id, self.request.params))
def delete(self, exp_id):
def delete(self, iid, xid):
"""Delete an Experiment."""
self.response.write('experiment %s delete, %s\n' % (exp_id, self.request.params))
......@@ -10,9 +10,11 @@ import logging
import pymongo
import tarfile
import webapp2
import requests
import zipfile
import argparse
import bson.json_util
import webapp2_extras.routes
import nimsutil
......@@ -26,8 +28,14 @@ log = logging.getLogger('nimsapi')
class NIMSAPI(nimsapiutil.NIMSRequestHandler):
def head(self):
"""Return 200 OK."""
self.response.set_status(200)
def get(self):
self.response.write('nimsapi\n')
"""Return API documentation"""
self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
self.response.write('nimsapi - {0}\n'.format(self.app.config['site_id']))
def upload(self):
# TODO add security: either authenticated user or machine-to-machine CRAM
......@@ -47,7 +55,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
self.abort(400, 'Content-MD5 mismatch.')
if not tarfile.is_tarfile(upload_filepath) and not zipfile.is_zipfile(upload_filepath):
self.abort(415)
os.rename(upload_filepath, os.path.join(stage_path, str(uuid.uuid1()) + '_' + filename)) # add UUID to prevent clobbering files
os.rename(upload_filepath, os.path.join(stage_path, str(uuid.uuid1()) + '_' + fid)) # add UUID to prevent clobbering files
def download(self):
paths = []
......@@ -64,33 +72,33 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler):
class Users(nimsapiutil.NIMSRequestHandler):
def count(self):
def count(self, iid):
"""Return the number of Users."""
self.response.write('%d users\n' % self.app.db.users.count())
def post(self):
def post(self, iid):
"""Create a new User"""
self.response.write('users post\n')
def get(self):
def get(self, iid):
"""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):
def put(self, iid):
"""Update many Users."""
self.response.write('users put\n')
class User(nimsapiutil.NIMSRequestHandler):
def get(self, uid):
def get(self, iid, 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, uid):
def put(self, iid, uid):
"""Update an existing User."""
user = self.app.db.users.find_one({'_id': uid})
if not user:
......@@ -112,80 +120,127 @@ class User(nimsapiutil.NIMSRequestHandler):
self.abort(403)
self.response.write(json.dumps(user, default=bson.json_util.default) + '\n')
def delete(self, uid):
def delete(self, iid, uid):
"""Delete an User."""
self.response.write('user %s delete, %s\n' % (uid, self.request.params))
class Groups(nimsapiutil.NIMSRequestHandler):
def count(self):
def count(self, iid):
"""Return the number of Groups."""
self.response.write('%d groups\n' % self.app.db.groups.count())
def post(self):
def post(self, iid):
"""Create a new Group"""
self.response.write('groups post\n')
def get(self):
def get(self, iid):
"""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):
def put(self, iid):
"""Update many Groups."""
self.response.write('groups put\n')
class Group(nimsapiutil.NIMSRequestHandler):
def get(self, gid):
def get(self, iid, 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, gid):
def put(self, iid, gid):
"""Update an existing Group."""
self.response.write('group %s put, %s\n' % (gid, self.request.params))
def delete(self, gid):
def delete(self, iid, gid):
"""Delete an Group."""
class Remotes(nimsapiutil.NIMSRequestHandler):
def get(self):
"""Return Remote NIMS sites"""
logging.info(self.user)
# TODO: remotes by default will show all registered remote site
if self.request.get('all'):
projection = ['_id', 'hostname', 'ip4']
sites = list(self.app.db.remotes.find({}, projection))
self.response.write(json.dumps(sites, default=bson.json_util.default))
# if 'all' not specificed, then user MUST be speficied
else:
"""Return the list of remotes where user has membership"""
logging.info(self.user['_id'])
# projection = ['_id', 'hostname', 'ip4']
projection = ['_id', 'hostname', 'ip4', 'users']
remotes = list(self.app.db.remotes.find({'users': {'$in': [self.user['_id']]}}, projection))
self.response.write(json.dumps(remotes, default=bson.json_util.default))
# TODO: IMPORTANT; refine how remotes are returned from pymongo queries
# in the list of remotes; in which sites, are there experiments, which the person has access to?
# return ONLY remote site names.
# if not superuser, does person have permissions to what is being queried
# query = {'permissions.' + self.userid: {'$in': 'true'}} if not self.user_is_superuser else None
# projection = ['_id', 'users', 'hostname', 'ip4']
# remotes = list(self.app.db.remotes.find(query, projection))
# session_aggregates = self.app.db.sessions.aggregate([
# # {'$match': {'experiment': {'$in': [exp['_id'] for exp in experiments]}}},
# {'$group': {'_id': '$experiment', 'timestamp': {'$max': '$timestamp'}}},
# ])['result']
# timestamps = {sa['_id']: sa['timestamp'] for sa in session_aggregates}
# for exp in experiments:
# exp['timestamp'] = timestamps[exp['_id']]
# self.response.write(json.dumps(experiments, default=bson.json_util.default))
class ArgumentParser(argparse.ArgumentParser):
def __init__(self):
super(ArgumentParser, self).__init__()
self.add_argument('uri', help='NIMS DB URI')
self.add_argument('stage_path', help='path to staging area')
self.add_argument('--pubkey', default='internims/NIMSpubkey.pub', help='path to ssl pubkey')
self.add_argument('-u', '--uid', default='local', help='site uid')
self.add_argument('-f', '--logfile', help='path to log file')
self.add_argument('-l', '--loglevel', default='info', help='path to log file')
self.add_argument('-q', '--quiet', action='store_true', default=False, help='disable console logging')
routes = [
webapp2.Route(r'/nimsapi', NIMSAPI),
webapp2.Route(r'/nimsapi/upload', NIMSAPI, handler_method='upload', methods=['PUT']),
webapp2.Route(r'/nimsapi/download', NIMSAPI, handler_method='download', methods=['GET']),
webapp2.Route(r'/nimsapi/dump', NIMSAPI, handler_method='dump', methods=['GET']),
webapp2.Route(r'/nimsapi/users', Users),
webapp2.Route(r'/nimsapi/users/count', Users, handler_method='count', methods=['GET']),
webapp2.Route(r'/nimsapi/users/<:.+>', User),
webapp2.Route(r'/nimsapi/groups', Groups),
webapp2.Route(r'/nimsapi/groups/count', Groups, handler_method='count', methods=['GET']),
webapp2.Route(r'/nimsapi/groups/<:.+>', Group),
webapp2.Route(r'/nimsapi/experiments', experiments.Experiments),
webapp2.Route(r'/nimsapi/experiments/count', experiments.Experiments, handler_method='count', methods=['GET']),
webapp2.Route(r'/nimsapi/experiments/<:[0-9a-f]+>', experiments.Experiment),
webapp2.Route(r'/nimsapi/experiments/<:[0-9a-f]+>/sessions', sessions.Sessions),
webapp2.Route(r'/nimsapi/sessions/count', sessions.Sessions, handler_method='count', methods=['GET']),
webapp2.Route(r'/nimsapi/sessions/<:[0-9a-f]+>', sessions.Session),
webapp2.Route(r'/nimsapi/sessions/<:[0-9a-f]+>/move', sessions.Session, handler_method='move'),
webapp2.Route(r'/nimsapi/sessions/<:[0-9a-f]+>/epochs', epochs.Epochs),
webapp2.Route(r'/nimsapi/epochs/count', epochs.Epochs, handler_method='count', methods=['GET']),
webapp2.Route(r'/nimsapi/epochs/<:[0-9a-f]+>', epochs.Epoch),
]
webapp2.Route(r'/nimsapi', NIMSAPI),
webapp2_extras.routes.PathPrefixRoute(r'/nimsapi', [
webapp2.Route(r'/download', NIMSAPI, handler_method='download', methods=['GET']),
webapp2.Route(r'/dump', NIMSAPI, handler_method='dump', methods=['GET']),
webapp2.Route(r'/upload/<fid>', NIMSAPI, handler_method='upload', methods=['PUT']),
webapp2.Route(r'/remotes', Remotes),
]),
# webapp2_extras.routes.PathPrefixRoute has bug, variable MUST have regex
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/<uid>', User),
webapp2.Route(r'/groups', Groups),
webapp2.Route(r'/groups/count', Groups, handler_method='count', methods=['GET']),
webapp2.Route(r'/groups/<gid>', Group),
webapp2.Route(r'/experiments', experiments.Experiments),
webapp2.Route(r'/experiments/count', experiments.Experiments, handler_method='count', methods=['GET']),
webapp2.Route(r'/experiments/<xid:[0-9a-f]{24}>', experiments.Experiment),
webapp2.Route(r'/experiments/<xid:[0-9a-f]{24}>/sessions', sessions.Sessions),
webapp2.Route(r'/sessions/count', sessions.Sessions, handler_method='count', methods=['GET']),
webapp2.Route(r'/sessions/<sid:[0-9a-f]{24}>', sessions.Session),
webapp2.Route(r'/sessions/<sid:[0-9a-f]{24}>/move', sessions.Session, handler_method='move'),
webapp2.Route(r'/sessions/<sid:[0-9a-f]{24}>/epochs', epochs.Epochs),
webapp2.Route(r'/epochs/count', epochs.Epochs, handler_method='count', methods=['GET']),
webapp2.Route(r'/epochs/<eid:[0-9a-f]{24}>', epochs.Epoch),
]),
]
if __name__ == '__main__':
......@@ -193,37 +248,6 @@ if __name__ == '__main__':
nimsutil.configure_log(args.logfile, not args.quiet, args.loglevel)
from paste import httpserver
app = webapp2.WSGIApplication(routes, debug=True, config=dict(stage_path=args.stage_path))
app = webapp2.WSGIApplication(routes, debug=True, config=dict(stage_path=args.stage_path, site_id=args.uid, pubkey=args.pubkey))
app.db = (pymongo.MongoReplicaSetClient(args.uri) if 'replicaSet' in args.uri else pymongo.MongoClient(args.uri)).get_default_database()
httpserver.serve(app, host=httpserver.socket.gethostname(), port='8080')
#API = NIMSAPI
#APIResource = experiments.Experiments
#routes = [
# webapp2.Route(r'/', API),
# webapp2.Route(r'/resource', APIResource),
# ]
#
#from webapp2_extras.routes import PathPrefixRoute
#
#if __name__ == '__main__':
# from paste import httpserver
# nimsapi = webapp2.WSGIApplication([PathPrefixRoute('/', routes)], debug=True)
# httpserver.serve(nimsapi, host='127.0.0.1', port='8080')
#else:
# from webapp2_extras.routes import PathPrefixRoute
# nimsapi = webapp2.WSGIApplication([PathPrefixRoute('/nimsapi', routes)], debug=True)
# /experiments experiment info for all experiments
# /experiments/ID/sessions experiment info with embedded sessions for one experiment
# /experiments/ID/epochs experiment info with embedded sessions and embedded epochs for one experiment
# /sessions/ID/epochs experiment info with embedded sessions and embedded epochs for one session
# /sessions experiment info with embedded sessions for all experiments
# /epochs experiment info with embedded sessions and embedded epochs for all sessions
# /experiments/ID experiment details for one experiment
# /sessions/ID sessions details for one session
# /epochs/ID epoch details for one epoch
# @author: Gunnar Schaefer
# @author: Kevin S. Hahn
import base64
import datetime
import json
import logging
import requests
import webapp2
import socket # socket.gethostname()
from Crypto.Hash import HMAC
from Crypto.Random import random
logging.basicConfig(level=logging.INFO)
class NIMSRequestHandler(webapp2.RequestHandler):
"""fetches pubkey from own self.db.remotes. needs to be aware of OWN site uid"""
def __init__(self, request=None, response=None):
webapp2.RequestHandler.__init__(self, request, response)
# call initialize, isntead of __init__
self.request.remote_user = self.request.get('user', None) # FIXME: auth system should set REMOTE_USER
self.userid = self.request.remote_user or '@public'
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.useragent = self.request.headers['user-agent'] # browser or internimsP2P request
self.site_id = self.app.config['site_id'] # what is THIS site
self.pubkey = open(self.app.config['pubkey']).read()
# requests coming from another NIMS instance are dealt with differently
if self.useragent.startswith('NIMS Instance'):
logging.info("request from '{0}', interNIMS p2p initiated".format(self.useragent))
try:
authinfo = self.request.headers['authorization']
challenge_id, digest = base64.b64decode(authinfo).split()
user, remote_site = challenge_id.split(':')
# logging.info('{0} {1} {2}'.format(user, remote_site, digest))
projection = {'_id': False, 'pubkey': True}
remote_pubkey = self.app.db.remotes.find_one({'_id': remote_site}, projection)['pubkey']
# get the challenge from db.challenges
projection = {'_id': False, 'challenge': True}
challenge = self.app.db.challenges.find_one({'_id': challenge_id}, projection)['challenge']
# purge challenge (challenges are single use)
self.app.db.challenges.remove({'_id': challenge_id})
# verify
h = HMAC.new(remote_pubkey, challenge)
self.expected = base64.b64encode('%s %s' % (challenge_id, h.hexdigest()))
if self.expected == authinfo:
logging.info('CRAM successful')
else:
self.abort(403, 'Not Authorized: cram failed')
except KeyError, e:
# send a 401 with a fresh challenge
cid = self.request.get('cid')
if not cid: self.abort(403, 'challenge_id not in payload')
challenge = {'_id': cid,
'challenge': str(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)
# send 401 + challenge in 'www-authenticate' header
self.response.headers['www-authenticate'] = base64.b64encode(challenge['challenge'])
self.response.set_status(401)
def dispatch(self):
"""dispatching and request forwarding"""
if self.target_id in ['local', self.site_id]:
logging.info('{0} delegating to local {1}'.format(socket.gethostname(), self.request.url))
super(NIMSRequestHandler, self).dispatch()
else:
logging.info('{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:
logging.info('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)
# adjust the request, pass as much of orig request as possible
reqheaders['User-Agent'] = 'NIMS Instance {0}'.format(self.site_id)
del reqheaders['Host']
target_api = 'http://{0}{1}?{2}'.format(target['hostname'], self.request.path, self.request.query_string)
reqparams = {'cid': self.cid}
# 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)
if r.status_code == 401:
challenge = base64.b64decode(r.headers['www-authenticate'])
# logging.info('challenge {0} recieved'.format(challenge))
h = HMAC.new(self.pubkey, challenge)
response = base64.b64encode('%s %s' % (self.cid, h.hexdigest()))
# logging.info('sending: {0} {1}'.format(self.cid, h.hexdigest()))
#adjust the request and try again
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)
self.response.write(r.content)
......@@ -9,36 +9,36 @@ import nimsapiutil
class Sessions(nimsapiutil.NIMSRequestHandler):
def count(self):
def count(self, iid):
"""Return the number of Sessions."""
self.response.write('sessions count\n')
def post(self):
def post(self, iid):
"""Create a new Session"""
self.response.write('sessions post\n')
def get(self, exp_id):
def get(self, iid, xid):
"""Return the list of Experiment Sessions."""
experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(exp_id)})
experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(xid)})
if not experiment:
self.abort(404)
if not self.user_is_superuser and self.userid not in experiment['permissions']:
self.abort(403)
query = {'experiment': bson.objectid.ObjectId(exp_id)}
query = {'experiment': bson.objectid.ObjectId(xid)}
projection = ['timestamp', 'subject']
sessions = list(self.app.db.sessions.find(query, projection))
self.response.write(json.dumps(sessions, default=bson.json_util.default))
def put(self):
def put(self, iid):
"""Update many Sessions."""
self.response.write('sessions put\n')
class Session(nimsapiutil.NIMSRequestHandler):
def get(self, sess_id):
def get(self, iid, sid):
"""Return one Session, conditionally with details."""
session = self.app.db.sessions.find_one({'_id': bson.objectid.ObjectId(sess_id)})
session = self.app.db.sessions.find_one({'_id': bson.objectid.ObjectId(sid)})
if not session:
self.abort(404)
experiment = self.app.db.experiments.find_one({'_id': bson.objectid.ObjectId(session['experiment'])})
......@@ -48,19 +48,19 @@ class Session(nimsapiutil.NIMSRequestHandler):
self.abort(403)
self.response.write(json.dumps(session, default=bson.json_util.default))
def put(self, _id):
def put(self, iid, sid):
"""Update an existing Session."""
self.response.write('session %s put, %s\n' % (_id, self.request.params))
self.response.write('session %s put, %s\n' % (sid, self.request.params))
def delete(self, _id):
def delete(self, iid, sid):
"""Delete an Session."""
self.response.write('session %s delete, %s\n' % (_id, self.request.params))
self.response.write('session %s delete, %s\n' % (sid, self.request.params))
def move(self, _id):
def move(self, iid, sid):
"""
Move a Session to another Experiment.
Usage:
/nimsapi/sessions/123/move?dest=456
"""
self.response.write('session %s move, %s\n' % (_id, self.request.params))
self.response.write('session %s move, %s\n' % (sid, self.request.params))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment