diff --git a/collections_.py b/collections_.py index 16a574dc4c82787d44dca99cb1d431f52ad23739..856a8e105613d232a377474c61098c910a9018a1 100644 --- a/collections_.py +++ b/collections_.py @@ -1,13 +1,10 @@ # @author: Gunnar Schaefer -import re -import json -import webapp2 -import bson.json_util - import logging log = logging.getLogger('nimsapi') +import bson.json_util + import nimsapiutil @@ -48,7 +45,9 @@ class Collections(nimsapiutil.NIMSRequestHandler): def count(self): """Return the number of Collections.""" - self.response.write(json.dumps(self.app.db.collections.count())) + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.collections.count()) def post(self): """Create a new Collection.""" @@ -63,8 +62,7 @@ class Collections(nimsapiutil.NIMSRequestHandler): """Return the list of Collections.""" query = {'permissions.uid': self.uid} if not self.user_is_superuser else None projection = {'curator': 1, 'name': 1, 'notes': 1, 'permissions': {'$elemMatch': {'uid': self.uid}}} - collections = list(self.app.db.collections.find(query, projection)) - self.response.write(json.dumps(collections, default=bson.json_util.default)) + return list(self.app.db.collections.find(query, projection)) def put(self): """Update many Collections.""" @@ -114,8 +112,7 @@ class Collection(nimsapiutil.NIMSRequestHandler): def get(self, cid): """Return one Collection, conditionally with details.""" cid = bson.ObjectId(cid) - collection = self.get_collection(cid) - self.response.write(json.dumps(collection, default=bson.json_util.default)) + return self.get_collection(cid) def put(self, cid): """Update an existing Collection.""" @@ -171,7 +168,9 @@ class Sessions(nimsapiutil.NIMSRequestHandler): def count(self): """Return the number of Sessions.""" - self.response.write(json.dumps(self.app.db.sessions.count())) + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.sessions.count()) def post(self): """Create a new Session""" @@ -190,7 +189,7 @@ class Sessions(nimsapiutil.NIMSRequestHandler): sessions = list(self.app.db.sessions.find(query, projection)) for sess in sessions: sess['site'] = self.app.config['site_id'] - self.response.write(json.dumps(sessions, default=bson.json_util.default)) + return sessions def put(self): """Update many Sessions.""" @@ -230,7 +229,9 @@ class Epochs(nimsapiutil.NIMSRequestHandler): def count(self): """Return the number of Epochs.""" - self.response.write(json.dumps(self.app.db.epochs.count())) + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.epochs.count()) def post(self): """Create a new Epoch.""" @@ -247,9 +248,7 @@ class Epochs(nimsapiutil.NIMSRequestHandler): elif sid != '': self.abort(400, sid + ' is not a valid ObjectId') projection = ['name', 'description', 'datatype', 'notes'] - print query - epochs = list(self.app.db.epochs.find(query, projection)) - self.response.write(json.dumps(epochs, default=bson.json_util.default)) + return list(self.app.db.epochs.find(query, projection)) def put(self): """Update many Epochs.""" diff --git a/experiments.py b/experiments.py index 18752fd5a0be175c20cc7850f9b7059dd1930730..dab607b27ead38b0b035ca04c4b4f9c633e84042 100644 --- a/experiments.py +++ b/experiments.py @@ -1,12 +1,10 @@ # @author: Gunnar Schaefer -import json -import webapp2 -import bson.json_util - import logging log = logging.getLogger('nimsapi') +import bson.json_util + import nimsdata import nimsapiutil @@ -51,7 +49,9 @@ class Experiments(nimsapiutil.NIMSRequestHandler): def count(self): """Return the number of Experiments.""" - self.response.write(json.dumps(self.app.db.experiments.count())) + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.experiments.count()) def post(self): """Create a new Experiment.""" @@ -64,7 +64,7 @@ class Experiments(nimsapiutil.NIMSRequestHandler): experiments = list(self.app.db.experiments.find(query, projection)) for exp in experiments: exp['site'] = self.app.config['site_id'] - self.response.write(json.dumps(experiments, default=bson.json_util.default)) + return experiments def put(self): """Update many Experiments.""" @@ -117,8 +117,7 @@ class Experiment(nimsapiutil.NIMSRequestHandler): def get(self, xid): """Return one Experiment, conditionally with details.""" xid = bson.ObjectId(xid) - experiment = self.get_experiment(xid) - self.response.write(json.dumps(experiment, default=bson.json_util.default)) + return self.get_experiment(xid) def put(self, xid): """Update an existing Experiment.""" @@ -168,7 +167,9 @@ class Sessions(nimsapiutil.NIMSRequestHandler): def count(self): """Return the number of Sessions.""" - self.response.write(json.dumps(self.app.db.sessions.count())) + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.sessions.count()) def post(self): """Create a new Session""" @@ -180,8 +181,7 @@ class Sessions(nimsapiutil.NIMSRequestHandler): self.get_experiment(xid) # ensure permissions query = {'experiment': xid} projection = ['name', 'subject', 'notes'] - sessions = list(self.app.db.sessions.find(query, projection)) - self.response.write(json.dumps(sessions, default=bson.json_util.default)) + return list(self.app.db.sessions.find(query, projection)) def put(self): """Update many Sessions.""" @@ -222,16 +222,17 @@ class Session(nimsapiutil.NIMSRequestHandler): } def schema(self, *args, **kwargs): + if self.request.method == 'OPTIONS': + return self.options() import copy json_schema = copy.deepcopy(self.json_schema) json_schema['properties'].update(nimsdata.NIMSData.session_properties) - self.response.write(json.dumps(json_schema, default=bson.json_util.default)) + return json_schema def get(self, sid): """Return one Session, conditionally with details.""" sid = bson.ObjectId(sid) - session = self.get_session(sid) - self.response.write(json.dumps(session, default=bson.json_util.default)) + return self.get_session(sid) def put(self, sid): """Update an existing Session.""" @@ -247,15 +248,6 @@ class Session(nimsapiutil.NIMSRequestHandler): """Delete a Session.""" self.abort(501) - def move(self, sid): - """ - Move a Session to another Experiment. - - Usage: - /nimsapi/sessions/123/move?dest=456 - """ - self.response.write('session %s move, %s\n' % (sid, self.request.params)) - class Epochs(nimsapiutil.NIMSRequestHandler): @@ -290,7 +282,9 @@ class Epochs(nimsapiutil.NIMSRequestHandler): def count(self): """Return the number of Epochs.""" - self.response.write(json.dumps(self.app.db.epochs.count())) + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.epochs.count()) def post(self): """Create a new Epoch.""" @@ -302,8 +296,7 @@ class Epochs(nimsapiutil.NIMSRequestHandler): self.get_session(sid) # ensure permissions query = {'session': sid} projection = ['name', 'description', 'datatype', 'notes'] - epochs = list(self.app.db.epochs.find(query, projection)) - self.response.write(json.dumps(epochs, default=bson.json_util.default)) + return list(self.app.db.epochs.find(query, projection)) def put(self): """Update many Epochs.""" @@ -340,16 +333,17 @@ class Epoch(nimsapiutil.NIMSRequestHandler): } def schema(self, *args, **kwargs): + if self.request.method == 'OPTIONS': + return self.options() import copy json_schema = copy.deepcopy(self.json_schema) json_schema['properties'].update(nimsdata.nimsdicom.NIMSDicom.epoch_properties) - self.response.write(json.dumps(json_schema, default=bson.json_util.default)) + return json_schema def get(self, eid): """Return one Epoch, conditionally with details.""" eid = bson.ObjectId(eid) - epoch = self.get_epoch(eid) - self.response.write(json.dumps(epoch, default=bson.json_util.default)) + return self.get_epoch(eid) def put(self, eid): """Update an existing Epoch.""" diff --git a/internimsclient.py b/internimsclient.py index e8ff3e4fe1c1977181d2b0d5bfda488d79d6724d..a576fe691419446e4b758e37b3a232a45bb94b91 100755 --- a/internimsclient.py +++ b/internimsclient.py @@ -2,6 +2,12 @@ # # @author: Gunnar Schaefer, Kevin S. Hahn +import logging +import logging.config +log = logging.getLogger('internims') +logging.getLogger('requests').setLevel(logging.WARNING) + +import re import json import base64 import datetime @@ -10,15 +16,10 @@ import Crypto.Hash.SHA import Crypto.PublicKey.RSA import Crypto.Signature.PKCS1_v1_5 -import logging -import logging.config -log = logging.getLogger('internims') -logging.getLogger('requests').setLevel(logging.WARNING) - def update(db, api_uri, site_id, privkey, internims_url): """sends is-alive signal to internims central.""" - db.remotes.ensure_index('UTC', expireAfterSeconds=120) + db.remotes.ensure_index('timestamp', expireAfterSeconds=120) exp_userlist = [e['permissions'] for e in db.experiments.find(None, {'_id': False, 'permissions.uid': True})] col_userlist = [c['permissions'] for c in db.collections.find(None, {'_id': False, 'permissions.uid': True})] @@ -29,25 +30,27 @@ def update(db, api_uri, site_id, privkey, internims_url): signature = Crypto.Signature.PKCS1_v1_5.new(privkey).sign(h) headers = {'Authorization': base64.b64encode(signature)} - r = requests.post(url=internims_url, data=payload, headers=headers, verify=True) + r = requests.post(internims_url, data=payload, headers=headers) if r.status_code == 200: response = (json.loads(r.content)) # update remotes entries for site in response['sites']: - site['UTC'] = datetime.datetime.strptime(site['timestamp'], '%Y-%m-%dT%H:%M:%S.%f') - db.remotes.find_and_modify({'_id': site['_id']}, update=site, upsert=True) - log.debug('upserting remote: ' + site['_id']) - - # update, add remotes to users - new_remotes = response['users'] - log.debug('users w/ remotes: ' + str(new_remotes)) - for user in response['users']: - db.users.update({'uid': user}, {'$set': {'remotes': new_remotes.get(user, [])}}) - - # cannot use new_remotes.viewkeys(). leads to 'bson.errors.InvalidDocument: Cannot encode object: dict_keys([])' - db.users.update({'remotes': {'$exists':True}, 'uid': {'$nin': new_remotes.keys()}}, {'$unset': {'remotes': ''}}, multi=True) + site['timestamp'] = datetime.datetime.strptime(site['timestamp'], '%Y-%m-%dT%H:%M:%S.%fZ') + db.remotes.update({'_id': site['_id']}, site, upsert=True) + log.debug('updating remotes: ' + ', '.join((r['_id'] for r in response['sites']))) + + # delete remotes from users, who no longer have remotes + db.users.update({'remotes': {'$exists':True}, '_id': {'$nin': response['users'].keys()}}, {'$unset': {'remotes': ''}}, multi=True) + + # add remotes to users + log.debug('users w/ remotes: ' + ', '.join(response['users'])) + for uid, remotes in response['users'].iteritems(): + db.users.update({'_id': uid}, {'$set': {'remotes': remotes}}) else: - log.warning((r.status_code, r.reason)) + # r.reason contains generic description for the specific error code + # need the part of the error response body that contains the detailed explanation + reason = re.search('<br /><br />\n(.*)\n\n\n </body>\n</html>', r.content) + log.warning((r.status_code, reason.group(1))) if __name__ == '__main__': diff --git a/nimsapi.py b/nimsapi.py index fe4a708f5dacb09ef3c4d5490d6be6adcbb985e8..a3ec983c842cf31bc7f022696a8668ac025b0090 100755 --- a/nimsapi.py +++ b/nimsapi.py @@ -2,6 +2,10 @@ # # @author: Gunnar Schaefer, Kevin S. Hahn +import logging +import logging.config +log = logging.getLogger('nimsapi') + import os import re import json @@ -15,15 +19,23 @@ import bson.json_util import webapp2_extras.routes import Crypto.PublicKey.RSA -import logging -import logging.config -log = logging.getLogger('nimsapi') - +import users import experiments import nimsapiutil import collections_ import tempdir as tempfile +def hrsize(size): + if size < 1000: + return '%d%s' % (size, 'B') + for suffix in 'KMGTPEZY': + size /= 1024. + if size < 10.: + return '%.1f%s' % (size, suffix) + if size < 1000.: + return '%.0f%s' % (size, suffix) + return '%.0f%s' % (size, 'Y') + class NIMSAPI(nimsapiutil.NIMSRequestHandler): @@ -38,12 +50,12 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): resources = """ Resource | Description :---------------------------------------------------|:----------------------- - nimsapi/download | download + nimsapi/login | user login + [(nimsapi/remotes)] | list of remote instances nimsapi/upload | upload - nimsapi/remotes | list of remote instances - [(nimsapi/log)] | list of uwsgi log messages + nimsapi/download | download + [(nimsapi/log)] | log messages [(nimsapi/users)] | list of users - nimsapi/users/current | details for currently logged in user [(nimsapi/users/count)] | count of users [(nimsapi/users/listschema)] | schema for user list [(nimsapi/users/schema)] | schema for single user @@ -100,7 +112,22 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): self.response.write('</body>\n') self.response.write('</html>\n') + def login(self): + """Return details for the current User.""" + if self.request.method == 'OPTIONS': + return self.options() + log.debug(self.uid + ' has logged in') + return self.app.db.users.find_and_modify({'_id': self.uid}, {'$inc': {'logins': 1}}, fields=['firstname', 'lastname', 'superuser']) + + def remotes(self): + """Return the list of all remote sites.""" + if self.request.method == 'OPTIONS': + return self.options() + return [r['_id'] for r in self.app.db.remotes.find()] + def upload(self): + if self.request.method == 'OPTIONS': + return self.options() # TODO add security: either authenticated user or machine-to-machine CRAM if 'Content-MD5' not in self.request.headers: self.abort(400, 'Request must contain a valid "Content-MD5" header.') @@ -109,7 +136,6 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): with tempfile.TemporaryDirectory(prefix='.tmp', dir=stage_path) as tempdir_path: hash_ = hashlib.md5() upload_filepath = os.path.join(tempdir_path, filename) - log.info('upload from ' + self.request.user_agent + ': ' + os.path.basename(upload_filepath)) with open(upload_filepath, 'wb') as upload_file: for chunk in iter(lambda: self.request.body_file.read(2**20), ''): hash_.update(chunk) @@ -118,9 +144,12 @@ 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) + log.info('upload from %s: %s [%s]' % (self.request.user_agent, os.path.basename(upload_filepath), hrsize(self.request.content_length))) os.rename(upload_filepath, os.path.join(stage_path, str(uuid.uuid1()) + '_' + filename)) # add UUID to prevent clobbering files def download(self): + if self.request.method == 'OPTIONS': + return self.options() paths = [] symlinks = [] for js_id in self.request.get('id', allow_multiple=True): @@ -129,326 +158,89 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): paths += _idpaths symlinks += _idsymlinks - def remotes(self): - """Return the list of remotes where user has membership""" - remotes = [remote['_id'] for remote in list(self.app.db.remotes.find({}, []))] - self.response.write(json.dumps(remotes, default=bson.json_util.default)) - def log(self): - """Return logs""" - # TODO: don't hardcode log path. - logfile = '/var/log/uwsgi/app/nims.log' + """Return logs.""" + if self.request.method == 'OPTIONS': + return self.options() try: - logs = open(logfile).readlines() + logs = open(app.config['log_path']).readlines() except IOError as e: - log.debug(e) if 'Permission denied' in e: - # specify body format to print details separate from comment body_template = '${explanation}<br /><br />${detail}<br /><br />${comment}' comment = 'To fix permissions, run the following command: chmod o+r ' + logfile self.abort(500, detail=str(e), comment=comment, body_template=body_template) else: - # file does not exist - self.abort(500, e) - else: - logs.reverse() - numlines = self.request.get('n', None) - - trimmed = [] - for line in logs: - match = re.search('^[\d\s:-]{17}[\s]+nimsapi:[.]*', line) - if match: - trimmed.append(line) - - if not numlines: numlines = len(trimmed) - - self.response.headers['Content-Type'] = 'application/json' - self.response.write(json.dumps(trimmed[:int(numlines)])) - - -class Users(nimsapiutil.NIMSRequestHandler): - - """/nimsapi/users """ - - json_schema = { - '$schema': 'http://json-schema.org/draft-04/schema#', - 'title': 'User List', - 'type': 'array', - 'items': { - 'title': 'User', - 'type': 'object', - 'properties': { - '_id': { - 'title': 'Database ID', - 'type': 'string', - }, - 'firstname': { - 'title': 'First Name', - 'type': 'string', - 'default': '', - }, - 'lastname': { - 'title': 'Last Name', - 'type': 'string', - 'default': '', - }, - 'email': { - 'title': 'Email', - 'type': 'string', - 'format': 'email', - 'default': '', - }, - 'email_hash': { - 'type': 'string', - 'default': '', - }, - } - } - } - - def count(self): - """Return the number of Users.""" - self.response.write('%d users\n' % self.app.db.users.count()) - - def current(self): - """Return the current User.""" - # FIXME: trim this down to not use the self.user object, and only send relevant info - self.response.write(json.dumps(self.user, default=bson.json_util.default)) - - def post(self): - """Create a new User""" - self.response.write('users post\n') - - 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): - """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', - 'type': 'object', - 'properties': { - '_id': { - 'title': 'Database ID', - 'type': 'string', - }, - 'firstname': { - 'title': 'First Name', - 'type': 'string', - 'default': '', - }, - 'lastname': { - 'title': 'Last Name', - 'type': 'string', - 'default': '', - }, - 'email': { - 'title': 'Email', - 'type': 'string', - 'format': 'email', - 'default': '', - }, - 'email_hash': { - 'type': 'string', - 'default': '', - }, - 'superuser': { - 'title': 'Superuser', - 'type': 'boolean', - }, - }, - 'required': ['_id'], - } - - def get(self, uid): - """Return User details.""" - user = self.app.db.users.find_one({'uid': uid}) - self.response.write(json.dumps(user, default=bson.json_util.default)) - - def put(self, uid): - """Update an existing User.""" - user = self.app.db.users.find_one({'user_info': uid}) - if not user: - self.abort(404) - if uid == self.uid or self.user_is_superuser: # users can only update their own info - updates = {'$set': {}, '$unset': {}} - for k, v in self.request.params.iteritems(): - if k != 'superuser' and k in []:#user_fields: - updates['$set'][k] = v # FIXME: do appropriate type conversion - elif k == 'superuser' and uid == self.uid and self.user_is_superuser is not None: # toggle superuser for requesting user - updates['$set'][k] = v.lower() in ('1', 'true') - elif k == 'superuser' and uid != self.uid and self.user_is_superuser: # enable/disable superuser for other user - if v.lower() in ('1', 'true') and user.get('superuser') is None: - updates['$set'][k] = False # superuser is tri-state: False indicates granted, but disabled, superuser privileges - elif v.lower() not in ('1', 'true'): - updates['$unset'][k] = '' - self.app.db.users.update({'user_info': uid}, updates) - else: - self.abort(403) - - 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', - 'type': 'array', - 'items': { - 'title': 'Group', - 'type': 'object', - 'properties': { - '_id': { - 'title': 'Database ID', - 'type': 'string', - }, - } - } - } - - def count(self): - """Return the number of Groups.""" - self.response.write('%d groups\n' % self.app.db.groups.count()) - - def post(self): - """Create a new Group""" - self.response.write('groups post\n') - - 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): - """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', - 'type': 'object', - 'properties': { - '_id': { - 'title': 'Database ID', - 'type': 'string', - }, - 'name': { - 'title': 'Name', - 'type': 'string', - 'maxLength': 32, - }, - 'pis': { - 'title': 'PIs', - 'type': 'array', - 'default': [], - 'items': { - 'type': 'string', - }, - 'uniqueItems': True, - }, - 'admins': { - 'title': 'Admins', - 'type': 'array', - 'default': [], - 'items': { - 'type': 'string', - }, - 'uniqueItems': True, - }, - 'memebers': { - 'title': 'Members', - 'type': 'array', - 'default': [], - 'items': { - 'type': 'string', - }, - 'uniqueItems': True, - }, - }, - 'required': ['_id'], - } - - 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, gid): - """Update an existing Group.""" - self.response.write('group %s put, %s\n' % (gid, self.request.params)) - - def delete(self, gid): - """Delete an Group.""" + self.abort(500, e) # file does not exist + try: + n = int(self.request.get('n', 10000)) + except: + self.abort(400, 'n must be an integer') + return [line.strip() for line in reversed(logs) if re.match('[-:0-9 ]{18} +nimsapi:(?!.*[/a-z]*/log )', line)][:n] routes = [ - webapp2.Route(r'/nimsapi', NIMSAPI), + webapp2.Route(r'/nimsapi', NIMSAPI), webapp2_extras.routes.PathPrefixRoute(r'/nimsapi', [ - webapp2.Route(r'/download', NIMSAPI, handler_method='download', methods=['GET']), - webapp2.Route(r'/upload', NIMSAPI, handler_method='upload', methods=['PUT']), - webapp2.Route(r'/remotes', NIMSAPI, handler_method='remotes', methods=['GET']), - webapp2.Route(r'/log', NIMSAPI, handler_method='log', methods=['GET']), - webapp2.Route(r'/users', Users), - webapp2.Route(r'/users/current', Users, handler_method='current', methods=['GET']), - webapp2.Route(r'/users/count', Users, handler_method='count', methods=['GET']), - webapp2.Route(r'/users/listschema', Users, handler_method='schema', methods=['GET']), - webapp2.Route(r'/users/schema', User, handler_method='schema', 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/listschema', Groups, handler_method='schema', methods=['GET']), - webapp2.Route(r'/groups/schema', Group, handler_method='schema', 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/listschema', experiments.Experiments, handler_method='schema', methods=['GET']), - webapp2.Route(r'/experiments/schema', experiments.Experiment, handler_method='schema', methods=['GET']), - webapp2.Route(r'/experiments/<xid:[0-9a-f]{24}>', experiments.Experiment), - webapp2.Route(r'/experiments/<xid:[0-9a-f]{24}>/sessions', experiments.Sessions), - webapp2.Route(r'/sessions/count', experiments.Sessions, handler_method='count', methods=['GET']), - webapp2.Route(r'/sessions/listschema', experiments.Sessions, handler_method='schema', methods=['GET']), - webapp2.Route(r'/sessions/schema', experiments.Session, handler_method='schema', methods=['GET']), - webapp2.Route(r'/sessions/<sid:[0-9a-f]{24}>', experiments.Session), - webapp2.Route(r'/sessions/<sid:[0-9a-f]{24}>/move', experiments.Session, handler_method='move'), - webapp2.Route(r'/sessions/<sid:[0-9a-f]{24}>/epochs', experiments.Epochs), - webapp2.Route(r'/epochs/count', experiments.Epochs, handler_method='count', methods=['GET']), - webapp2.Route(r'/epochs/listschema', experiments.Epochs, handler_method='schema', methods=['GET']), - webapp2.Route(r'/epochs/schema', experiments.Epoch, handler_method='schema', methods=['GET']), - webapp2.Route(r'/epochs/<eid:[0-9a-f]{24}>', experiments.Epoch), - webapp2.Route(r'/collections', collections_.Collections), - webapp2.Route(r'/collections/count', collections_.Collections, handler_method='count', methods=['GET']), - webapp2.Route(r'/collections/listschema', collections_.Collections, handler_method='schema', methods=['GET']), - webapp2.Route(r'/collections/schema', collections_.Collection, handler_method='schema', methods=['GET']), - webapp2.Route(r'/collections/<cid:[0-9a-f]{24}>', collections_.Collection), - webapp2.Route(r'/collections/<cid:[0-9a-f]{24}>/sessions', collections_.Sessions), - webapp2.Route(r'/collections/<cid:[0-9a-f]{24}>/epochs', collections_.Epochs), + webapp2.Route(r'/login', NIMSAPI, handler_method='login', methods=['OPTIONS', 'GET', 'POST']), + webapp2.Route(r'/remotes', NIMSAPI, handler_method='remotes', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/upload', NIMSAPI, handler_method='upload', methods=['OPTIONS', 'PUT']), + webapp2.Route(r'/download', NIMSAPI, handler_method='download', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/log', NIMSAPI, handler_method='log', methods=['OPTIONS', 'GET']), + ]), + webapp2.Route(r'/nimsapi/users', users.Users), + webapp2_extras.routes.PathPrefixRoute(r'/nimsapi/users', [ + webapp2.Route(r'/count', users.Users, handler_method='count', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/listschema', users.Users, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/schema', users.User, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/<uid>', users.User), + ]), + webapp2.Route(r'/nimsapi/groups', users.Groups), + webapp2_extras.routes.PathPrefixRoute(r'/nimsapi/groups', [ + webapp2.Route(r'/count', users.Groups, handler_method='count', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/listschema', users.Groups, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/schema', users.Group, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/<gid>', users.Group), + ]), + webapp2.Route(r'/nimsapi/experiments', experiments.Experiments), + webapp2_extras.routes.PathPrefixRoute(r'/nimsapi/experiments', [ + webapp2.Route(r'/count', experiments.Experiments, handler_method='count', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/listschema', experiments.Experiments, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/schema', experiments.Experiment, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/<xid:[0-9a-f]{24}>', experiments.Experiment), + webapp2.Route(r'/<xid:[0-9a-f]{24}>/sessions', experiments.Sessions), + ]), + webapp2.Route(r'/nimsapi/collections', collections_.Collections), + webapp2_extras.routes.PathPrefixRoute(r'/nimsapi/collections', [ + webapp2.Route(r'/count', collections_.Collections, handler_method='count', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/listschema', collections_.Collections, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/schema', collections_.Collection, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/<cid:[0-9a-f]{24}>', collections_.Collection), + webapp2.Route(r'/<cid:[0-9a-f]{24}>/sessions', collections_.Sessions), + webapp2.Route(r'/<cid:[0-9a-f]{24}>/epochs', collections_.Epochs), + ]), + webapp2_extras.routes.PathPrefixRoute(r'/nimsapi/sessions', [ + webapp2.Route(r'/count', experiments.Sessions, handler_method='count', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/listschema', experiments.Sessions, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/schema', experiments.Session, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/<sid:[0-9a-f]{24}>', experiments.Session), + webapp2.Route(r'/<sid:[0-9a-f]{24}>/epochs', experiments.Epochs), + ]), + webapp2_extras.routes.PathPrefixRoute(r'/nimsapi/epochs', [ + webapp2.Route(r'/count', experiments.Epochs, handler_method='count', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/listschema', experiments.Epochs, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/schema', experiments.Epoch, handler_method='schema', methods=['OPTIONS', 'GET']), + webapp2.Route(r'/<eid:[0-9a-f]{24}>', experiments.Epoch), ]), ] +def dispatcher(router, request, response): + rv = router.default_dispatcher(request, response) + if rv is not None: + return response.write(json.dumps(rv, default=bson.json_util.default)) + app = webapp2.WSGIApplication(routes, debug=True) -app.config = dict(stage_path='', site_id=None, ssl_key=None, insecure=False) +app.router.set_dispatcher(dispatcher) +app.config = dict(stage_path='', site_id='local', ssl_key=None, insecure=False, log_path='') if __name__ == '__main__': @@ -462,6 +254,7 @@ if __name__ == '__main__': arg_parser.add_argument('config_file', help='path to config file') arg_parser.add_argument('--db_uri', help='NIMS DB URI') arg_parser.add_argument('--stage_path', help='path to staging area') + arg_parser.add_argument('--log_path', help='path to API log file') arg_parser.add_argument('--ssl_key', help='path to private SSL key file') arg_parser.add_argument('--site_id', help='InterNIMS site ID') arg_parser.add_argument('--oauth2_id_endpoint', help='OAuth2 provider ID endpoint') @@ -470,6 +263,7 @@ if __name__ == '__main__': config = ConfigParser.ConfigParser({'here': os.path.dirname(os.path.abspath(args.config_file))}) config.read(args.config_file) logging.config.fileConfig(args.config_file, disable_existing_loggers=False) + logging.getLogger('paste.httpserver').setLevel(logging.INFO) # silence paste logging if args.ssl_key: try: @@ -483,8 +277,9 @@ if __name__ == '__main__': else: log.warning('private SSL key not specified, internims functionality disabled') - app.config['site_id'] = args.site_id or 'local' + app.config['site_id'] = args.site_id or app.config['site_id'] app.config['stage_path'] = args.stage_path or config.get('nims', 'stage_path') + app.config['log_path'] = args.log_path or app.config['log_path'] app.config['oauth2_id_endpoint'] = args.oauth2_id_endpoint or config.get('oauth2', 'id_endpoint') app.config['insecure'] = config.getboolean('nims', 'insecure') diff --git a/nimsapi.wsgi b/nimsapi.wsgi index 994240d52d6a8c444d27d8389d4e63573e56a6f7..8dcbc569e72d8eac159537354e85c25553482b21 100644 --- a/nimsapi.wsgi +++ b/nimsapi.wsgi @@ -48,6 +48,7 @@ else: site_id = config.get('nims', 'site_id') application = nimsapi.app application.config['stage_path'] = config.get('nims', 'stage_path') +application.config['log_path'] = config.get('nims', 'log_path') application.config['site_id'] = site_id application.config['ssl_key'] = privkey application.config['oauth2_id_endpoint'] = config.get('oauth2', 'id_endpoint') diff --git a/nimsapiutil.py b/nimsapiutil.py index c063a0d3ac6c3416a5067df52ef1fa5baf16be4f..b3cf4fa5391a2d8f560ffb52358cb0d86b5fba5b 100644 --- a/nimsapiutil.py +++ b/nimsapiutil.py @@ -1,5 +1,9 @@ # @author: Gunnar Schaefer, Kevin S. Hahn +import logging +log = logging.getLogger('nimsapi') +logging.getLogger('requests').setLevel(logging.WARNING) # silence Requests library logging + import json import base64 import webapp2 @@ -10,10 +14,6 @@ import Crypto.Hash.SHA import Crypto.PublicKey.RSA import Crypto.Signature.PKCS1_v1_5 -import logging -log = logging.getLogger('nimsapi') -logging.getLogger('requests').setLevel(logging.WARNING) # silence Requests library logging - INTEGER_ROLES = { 'anon-read': 0, 'read-only': 1, @@ -57,174 +57,175 @@ class NIMSRequestHandler(webapp2.RequestHandler): def __init__(self, request=None, response=None): self.initialize(request, response) - self.uid = '@public' # @public is default user + self.target_id = self.request.get('iid', None) self.access_token = self.request.headers.get('Authorization', None) - log.debug('accesstoken: ' + str(self.access_token)) + + # CORS header + if 'Origin' in self.request.headers and self.request.headers['Origin'].startswith('https://'): + self.response.headers['Access-Control-Allow-Origin'] = self.request.headers['Origin'] 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}) + r = requests.get(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']) + self.uid = json.loads(r.content)['email'] else: - #TODO: add handlers for bad tokens. - log.debug('ERR: ' + str(r.status_code) + ' bad token') + # 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') + else: + self.uid = '@public' + self.user_is_superuser = False + + if self.uid != '@public': + user = self.app.db.users.find_one({'_id': self.uid}, ['superuser']) + if user: + self.user_is_superuser = user.get('superuser', None) + else: + self.abort(403, 'user ' + self.uid + ' does not exist') - self.user = self.app.db.users.find_one({'uid': self.uid}) - self.user_is_superuser = self.user.get('superuser', None) if self.user else False + if self.target_id not in [None, self.app.config['site_id']]: + self.rtype = 'to_remote' - # 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'] - self.ssl_key = self.app.config['ssl_key'] - log.debug('X-From: ' + str(self.p2p_user)) + 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') - # CORS bare minimum - self.response.headers.add('Access-Control-Allow-Origin', self.request.headers.get('origin', '*')) + target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id': False, 'api_uri': True}) + if not target: + self.abort(402, 'remote host ' + self.target_id + ' is not an authorized remote') - if not self.request.path.endswith('/nimsapi/log'): - log.info(self.request.method + ' ' + self.request.path + ' ' + str(self.request.params.mixed())) + # 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['iid'] + + # 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 + self.target_uri = target['api_uri'] + self.request.path.split('/nimsapi')[1] + + elif self.request.user_agent.startswith('NIMS Instance'): + self.rtype = 'from_remote' + + self.uid = self.request.headers.get('X-From') + self.user_is_superuser = False + + remote_instance = self.request.user_agent.replace('NIMS Instance', '').strip() + requester = self.app.db.remotes.find_one({'_id': remote_instance}) + if not requester: + self.abort(402, remote_instance + ' is not authorized') + + # assemble msg, hash, and verify received signature + 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): + self.abort(402, 'remote message/signature is not authentic') + else: + self.rtype = 'local' 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') - # verify signature - self.signature = base64.b64decode(self.request.headers.get('X-Signature')) - - # assemble msg to be hased - 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) - if verifier.verify(h, self.signature): - super(NIMSRequestHandler, self).dispatch() - else: - log.debug('message/signature is not authentic') - self.abort(403, 'authentication failed') - # request originates from self - else: - super(NIMSRequestHandler, self).dispatch() - - # dispatch to remote instance - 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 - try: - del headers['Authorization'] # delete access_token - except KeyError: - pass # not all requests will have access_token - - # 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) - 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 - # TODO: think about: are the headers even useful? + log.debug(self.rtype + ' ' + self.uid + ' ' + self.request.method + ' ' + self.request.path + ' ' + str(self.request.params.mixed())) + if self.rtype in ['local', 'from_remote']: + return super(NIMSRequestHandler, self).dispatch() + else: + if self.request.method == 'OPTIONS': + return self.options() + r = requests.request(self.request.method, self.target_uri, params=self.params, data=self.request.body, headers=self.headers, verify=False) + if r.status_code != 200: + self.abort(r.status_code, 'internims p2p err: ' + r.reason) self.response.write(r.content) - elif self.ssl_key is None or self.site_id is None: - log.debug('ssl key or site id undefined, cannot dispatch to remote') + def abort(self, code, *args, **kwargs): + log.warning(str(code) + ' ' + '; '.join(args)) + 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): + self.response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, POST, PUT, DELETE, OPTIONS' + self.response.headers['Access-Control-Allow-Headers'] = 'Authorization' + self.response.headers['Access-Control-Max-Age'] = '151200' - def schema(self, *args, **kwargs): - self.response.write(json.dumps(self.json_schema, default=bson.json_util.default)) + def schema(self): + if self.request.method == 'OPTIONS': + return self.options() + return self.json_schema - def get_collection(self, cid, min_role='anon-read'): + def get_collection(self, cid, min_role=None): collection = self.app.db.collections.find_one({'_id': cid}) if not collection: - self.abort(404) + self.abort(404, 'no such Collection') 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']}] + coll = self.app.db.collections.find_one({'_id': cid, 'permissions.uid': self.uid}, ['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'] return collection - def get_experiment(self, xid, min_role='anon-read'): + def get_experiment(self, xid, min_role=None): experiment = self.app.db.experiments.find_one({'_id': xid}) if not experiment: - self.abort(404) + self.abort(404, 'no such Experiment') 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']}] + exp = self.app.db.experiments.find_one({'_id': xid, 'permissions.uid': self.uid}, ['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'] return experiment - def get_session(self, sid, min_role='anon-read'): - #FIXME: implement min_role logic + def get_session(self, sid, min_role=None): 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) + self.abort(404, 'no such Session') 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') + experiment = self.app.db.experiments.find_one({'_id': session['experiment'], 'permissions.uid': self.uid}, ['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='anon-read'): - #FIXME: implement min_role logic + def get_epoch(self, eid, min_role=None): 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) + self.abort(404, 'no such Epoch') 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') + 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.uid': self.uid}, ['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 diff --git a/users.py b/users.py new file mode 100644 index 0000000000000000000000000000000000000000..932bb0ba6620079b92c2ab6f46f81d0dad65f443 --- /dev/null +++ b/users.py @@ -0,0 +1,240 @@ +# @author: Gunnar Schaefer + +import logging +log = logging.getLogger('nimsapi') + +import bson.json_util + +import nimsapiutil + + +class Users(nimsapiutil.NIMSRequestHandler): + + """/nimsapi/users """ + + json_schema = { + '$schema': 'http://json-schema.org/draft-04/schema#', + 'title': 'User List', + 'type': 'array', + 'items': { + 'title': 'User', + 'type': 'object', + 'properties': { + '_id': { + 'title': 'Database ID', + 'type': 'string', + }, + 'firstname': { + 'title': 'First Name', + 'type': 'string', + 'default': '', + }, + 'lastname': { + 'title': 'Last Name', + 'type': 'string', + 'default': '', + }, + 'email': { + 'title': 'Email', + 'type': 'string', + 'format': 'email', + 'default': '', + }, + 'email_hash': { + 'type': 'string', + 'default': '', + }, + } + } + } + + def count(self): + """Return the number of Users.""" + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.users.count()) + + def post(self): + """Create a new User""" + self.response.write('users post\n') + + def get(self): + """Return the list of Users.""" + return list(self.app.db.users.find({}, ['firstname', 'lastname', 'email_hash'])) + + 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', + 'type': 'object', + 'properties': { + '_id': { + 'title': 'Database ID', + 'type': 'string', + }, + 'firstname': { + 'title': 'First Name', + 'type': 'string', + 'default': '', + }, + 'lastname': { + 'title': 'Last Name', + 'type': 'string', + 'default': '', + }, + 'email': { + 'title': 'Email', + 'type': 'string', + 'format': 'email', + 'default': '', + }, + 'email_hash': { + 'type': 'string', + 'default': '', + }, + 'superuser': { + 'title': 'Superuser', + 'type': 'boolean', + }, + }, + 'required': ['_id'], + } + + def get(self, uid): + """Return User details.""" + projection = [] + if self.request.get('remotes') in ('1', 'true'): + projection += ['remotes'] + if self.request.get('status') in ('1', 'true'): + projection += ['status'] + user = self.app.db.users.find_one({'_id': uid}, projection or None) + if not user: + self.abort(404, 'no such User') + return user + + def put(self, uid): + """Update an existing User.""" + user = self.app.db.users.find_one({'_id': uid}) + if not user: + self.abort(404) + if uid == self.uid or self.user_is_superuser: # users can only update their own info + updates = {'$set': {}, '$unset': {}} + for k, v in self.request.params.iteritems(): + if k != 'superuser' and k in []:#user_fields: + updates['$set'][k] = v # FIXME: do appropriate type conversion + elif k == 'superuser' and uid == self.uid and self.user_is_superuser is not None: # toggle superuser for requesting user + updates['$set'][k] = v.lower() in ('1', 'true') + elif k == 'superuser' and uid != self.uid and self.user_is_superuser: # enable/disable superuser for other user + if v.lower() in ('1', 'true') and user.get('superuser') is None: + updates['$set'][k] = False # superuser is tri-state: False indicates granted, but disabled, superuser privileges + elif v.lower() not in ('1', 'true'): + updates['$unset'][k] = '' + self.app.db.users.update({'_id': uid}, updates) + else: + self.abort(403) + + 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', + 'type': 'array', + 'items': { + 'title': 'Group', + 'type': 'object', + 'properties': { + '_id': { + 'title': 'Database ID', + 'type': 'string', + }, + } + } + } + + def count(self): + """Return the number of Groups.""" + if self.request.method == 'OPTIONS': + return self.options() + self.response.write(self.app.db.groups.count()) + + def post(self): + """Create a new Group""" + self.response.write('groups post\n') + + def get(self): + """Return the list of Groups.""" + return list(self.app.db.groups.find({}, [])) + + 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', + 'type': 'object', + 'properties': { + '_id': { + 'title': 'Database ID', + 'type': 'string', + }, + 'name': { + 'title': 'Name', + 'type': 'string', + 'maxLength': 32, + }, + 'roles': { + 'title': 'Roles', + 'type': 'array', + 'default': [], + 'items': { + 'type': 'object', + 'properties': { + 'uid': { + 'type': 'string', + }, + 'role': { + 'type': 'string', + 'enum': [k for k, v in sorted(nimsapiutil.INTEGER_ROLES.iteritems(), key=lambda (k, v): v)], + }, + }, + }, + 'uniqueItems': True, + }, + }, + 'required': ['_id'], + } + + def get(self, gid): + """Return Group details.""" + group = self.app.db.groups.find_one({'_id': gid}) + if not group: + self.abort(404, 'no such Group') + return group + + def put(self, gid): + """Update an existing Group.""" + self.response.write('group %s put, %s\n' % (gid, self.request.params)) + + def delete(self, gid): + """Delete an Group."""