diff --git a/internimsclient.py b/internimsclient.py index fc10c17b2ca2a4f461145d5dcbf3d0a9e09f179b..1aad9f46420320f8f0a520dea971e3c2023e8abb 100755 --- a/internimsclient.py +++ b/internimsclient.py @@ -30,7 +30,7 @@ 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 @@ -40,12 +40,12 @@ def update(db, api_uri, site_id, privkey, internims_url): 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}, 'uid': {'$nin': response['users'].keys()}}, {'$unset': {'remotes': ''}}, multi=True) + 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({'uid': uid}, {'$set': {'remotes': remotes}}) + db.users.update({'_id': uid}, {'$set': {'remotes': remotes}}) else: # r.reason contains generic description for the specific error code # need the part of the error response body that contains the detailed explanation diff --git a/nimsapi.py b/nimsapi.py index f066d4f00aab4432648c4c250304c52f2d226d70..7131fd3a97add4c85705743f2f2f24c11eaf7d3a 100755 --- a/nimsapi.py +++ b/nimsapi.py @@ -33,7 +33,7 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): """Return 200 OK.""" self.response.set_status(200) - def get(self): + def get(self, *args): """Return API documentation""" resources = """ Resource | Description @@ -100,6 +100,15 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): self.response.write('</body>\n') self.response.write('</html>\n') + def login(self): + """Return details for the current User.""" + log.info(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.""" + return [r['_id'] for r in self.app.db.remotes.find()] + def upload(self): # TODO add security: either authenticated user or machine-to-machine CRAM if 'Content-MD5' not in self.request.headers: @@ -129,29 +138,22 @@ class NIMSAPI(nimsapiutil.NIMSRequestHandler): paths += _idpaths symlinks += _idsymlinks - def remotes(self): - """Return the list of all remote sites.""" - return list(self.app.db.remotes.find(None, [])) - def log(self): """Return logs.""" try: 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) + 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 for line in reversed(logs) if re.match('[\d\s:-]{17}[\s]+nimsapi:[.]*', line)][:n] + return [line.strip() for line in reversed(logs) if re.match('[-:0-9 ]{18} +nimsapi:(?!.*[/a-z]*/log )', line)][:n] class Users(nimsapiutil.NIMSRequestHandler): @@ -252,13 +254,6 @@ class User(nimsapiutil.NIMSRequestHandler): 'required': ['_id'], } - def current(self): - """Return details for the current User.""" - if self.request.method == 'GET': - return self.get(self.uid) - elif self.request.method == 'PUT': - return self.put(self.uid) - def get(self, uid): """Return User details.""" projection = [] @@ -266,14 +261,11 @@ class User(nimsapiutil.NIMSRequestHandler): projection += ['remotes'] if self.request.get('status') in ('1', 'true'): projection += ['status'] - if self.request.get('login') in ('1', 'true'): - projection += ['firstname', 'lastname', 'superuser'] - self.app.db.users.update({'uid': uid}, {'$inc': {'logins': 1}}) - return self.app.db.users.find_one({'uid': uid}, projection or None) + return self.app.db.users.find_one({'_id': uid}, projection or None) def put(self, uid): """Update an existing User.""" - user = self.app.db.users.find_one({'uid': uid}) + 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 @@ -288,7 +280,7 @@ class User(nimsapiutil.NIMSRequestHandler): 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({'uid': uid}, updates) + self.app.db.users.update({'_id': uid}, updates) else: self.abort(403) @@ -396,17 +388,16 @@ class Group(nimsapiutil.NIMSRequestHandler): routes = [ - 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'/login', NIMSAPI, handler_method='login', methods=['GET', 'POST']), webapp2.Route(r'/remotes', NIMSAPI, handler_method='remotes', methods=['GET']), + webapp2.Route(r'/upload', NIMSAPI, handler_method='upload', methods=['PUT']), + webapp2.Route(r'/download', NIMSAPI, handler_method='download', methods=['GET']), webapp2.Route(r'/log', NIMSAPI, handler_method='log', methods=['GET']), webapp2.Route(r'/users', Users), webapp2.Route(r'/users/count', Users, handler_method='count', methods=['GET']), webapp2.Route(r'/users/listschema', Users, handler_method='schema', methods=['GET']), webapp2.Route(r'/users/schema', User, handler_method='schema', methods=['GET']), - webapp2.Route(r'/users/current', User, handler_method='current', methods=['GET', 'PUT']), webapp2.Route(r'/users/<uid>', User), webapp2.Route(r'/groups', Groups), webapp2.Route(r'/groups/count', Groups, handler_method='count', methods=['GET']), @@ -437,16 +428,18 @@ routes = [ 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'/nimsapi', NIMSAPI), + webapp2.Route(r'/nimsapi/<:.*>', NIMSAPI), ] def dispatcher(router, request, response): rv = router.default_dispatcher(request, response) if rv is not None: - return webapp2.Response(json.dumps(rv, default=bson.json_util.default)) + return response.write(json.dumps(rv, default=bson.json_util.default)) app = webapp2.WSGIApplication(routes, debug=True) app.router.set_dispatcher(dispatcher) -app.config = dict(stage_path='', site_id=None, ssl_key=None, insecure=False, log_path='') +app.config = dict(stage_path='', site_id='local', ssl_key=None, insecure=False, log_path='') if __name__ == '__main__': @@ -482,9 +475,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 + 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/nimsapiutil.py b/nimsapiutil.py index fd4ffa32e7e519d896e483072768e16f4e9ed02e..f00747e8b76defca46158618d9cca3633a1df8f6 100644 --- a/nimsapiutil.py +++ b/nimsapiutil.py @@ -61,13 +61,13 @@ class NIMSRequestHandler(webapp2.RequestHandler): self.access_token = self.request.headers.get('Authorization', None) # CORS header - self.response.headers.add('Access-Control-Allow-Origin', self.request.headers.get('origin', '*')) + 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: self.uid = json.loads(r.content)['email'] - log.debug('oauth user: ' + self.uid) else: # 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 @@ -79,11 +79,11 @@ class NIMSRequestHandler(webapp2.RequestHandler): self.user_is_superuser = False if self.uid != '@public': - user = self.app.db.users.find_one({'_id': self.uid}) + 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.abort(403, 'user ' + self.uid + ' does not exist') if self.target_id not in [None, self.app.config['site_id']]: self.rtype = 'to_remote' @@ -95,8 +95,7 @@ class NIMSRequestHandler(webapp2.RequestHandler): 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 + ' is not an authorized remote.') - self.abort(403, 'remote host ' + self.target_id + ' is not an authorized remote.') + self.abort(402, 'remote host ' + self.target_id + ' is not an authorized remote') # adjust headers self.headers = self.request.headers @@ -118,7 +117,7 @@ class NIMSRequestHandler(webapp2.RequestHandler): self.headers['X-Signature'] = base64.b64encode(signature) # prepare delegated request URI - self.target_api = target['api_uri'] + self.request.path.split('/nimsapi')[1] + self.target_uri = target['api_uri'] + self.request.path.split('/nimsapi')[1] elif self.request.user_agent.startswith('NIMS Instance'): self.rtype = 'from_remote' @@ -127,35 +126,41 @@ class NIMSRequestHandler(webapp2.RequestHandler): 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}) + requester = self.app.db.remotes.find_one({'_id': remote_instance}) if not requester: - log.debug('remote host ' + remote_instance + ' not in auth list. DENIED') - self.abort(403, remote_instance + ' is not authorized') + self.abort(402, remote_instance + ' is not authorized') - # assemble msg, hash, and verify recieved signature + # 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): - log.debug('remote message/signature is not authentic') - self.abort(403, 'remote message/signature is not authentic') + self.abort(402, 'remote message/signature is not authentic') else: self.rtype = 'local' - # TODO: question: okay to move this logging block into dispatch? - if not self.request.path.endswith('/nimsapi/log'): - log.info(self.rtype + ' ' + self.request.method + ' ' + self.request.path + ' ' + str(self.request.params.mixed())) - def dispatch(self): """dispatching and request forwarding""" + log.info(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: - r = requests.request(method=self.request.method, data=self.request.body, url=self.target_api, params=self.params, headers=self.headers, verify=False) + r = requests.request(self.request.method, self.target_uri, params=self.params, data=self.request.body, headers=self.headers, verify=False) if not r.status_code == 200: self.abort(r.status_code, 'internims p2p err: ' + r.reason) self.response.write(r.content) + def abort(self, code, *args, **kwargs): + log.debug(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'] = 'POST, GET, PUT, DELETE, OPTIONS' + self.response.headers['Access-Control-Allow-Headers'] = 'Authorization' + def schema(self): return self.json_schema