diff --git a/epochs.py b/epochs.py index 18d46f690229a0246e51423a236131f957087161..945e06fc794b973a92ec24f83e17a16e7dc01aaa 100644 --- a/epochs.py +++ b/epochs.py @@ -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)) diff --git a/experiments.py b/experiments.py index 7a5d85d23f96747d253de64429ab504e43decd76..8c0acfd767577866b7a4fd4a06bf678738a198dd 100644 --- a/experiments.py +++ b/experiments.py @@ -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)) diff --git a/nimsapi.py b/nimsapi.py index 460dfb3ff9b5d1b0d626750dffecf3eb6f005d19..717f5f081a8cdc637c66de78b83dfdfebf0a9139 100755 --- a/nimsapi.py +++ b/nimsapi.py @@ -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 diff --git a/nimsapiutil.py b/nimsapiutil.py index c4acbcafc0b725f6c21bcefb392ebd9f76de8adc..08d817be4766edbd25a068f25b2cbdcc8b362ace 100644 --- a/nimsapiutil.py +++ b/nimsapiutil.py @@ -1,14 +1,105 @@ # @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) diff --git a/sessions.py b/sessions.py index d79f6bb454cf18d37e24728ffee0fd8b0c9afd87..3be07e3efc38f461c872931c080ca6b34a05a7d8 100644 --- a/sessions.py +++ b/sessions.py @@ -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))