From e613dbc2a6dd9001abebb22ac8c0c51154c89425 Mon Sep 17 00:00:00 2001 From: "Kevin S. Hahn" <kshahn@stanford.edu> Date: Thu, 23 Jan 2014 14:48:05 -0800 Subject: [PATCH] internims reports user w/ permissions at remotes - internimsclient separated from nimsapi.wsgi - args and config have more consistent naming - using api_url instead of hostname - improve internims error handling - bootstrap.py ensures remotes collection index --- internimsclient.py | 99 ++++++++++++++++++++ nimsapi.py | 37 ++------ nimsapi.wsgi | 227 ++++++++++++--------------------------------- nimsapiutil.py | 9 +- 4 files changed, 170 insertions(+), 202 deletions(-) create mode 100644 internimsclient.py diff --git a/internimsclient.py b/internimsclient.py new file mode 100644 index 00000000..10e03c3a --- /dev/null +++ b/internimsclient.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# @author: Gunnar Schaefer, Kevin S. Hahn + +import json +import base64 +import pymongo +import datetime +import requests +import Crypto.Hash.SHA +import Crypto.PublicKey.RSA +import Crypto.Signature.PKCS1_v1_5 + +import logging +import logging.config +log = logging.getLogger('nimsapi') +logging.getLogger('requests').setLevel(logging.WARNING) + + +def update(db, api_uri, site_id, privkey, internims_url): + """sends is-alive signal to internims central.""" + exp_userlist = [exp['permissions'].viewkeys() for exp in db.experiments.find({}, {'_id': False, 'permissions': True})] + col_userlist = [col['permissions'].viewkeys() for col in db.collections.find({}, {'_id': False, 'permissions': True})] + userlists = exp_userlist + col_userlist + all_users = set([user for experiment in userlists for user in experiment]) + remote_users = filter(lambda u: '#' in u, all_users) + + payload = json.dumps({'iid': site_id, 'api_uri': api_uri, 'users': remote_users}) + h = Crypto.Hash.SHA.new(payload) + 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) + 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({'_id': 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}, '_id': {'$nin': new_remotes.keys()}}, {'$unset': {'remotes': ''}}, multi=True) + else: + log.info((r.status_code, r.reason)) + + +if __name__ == '__main__': + import os + import sys + import time + import argparse + import ConfigParser + + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('configfile', help='path to configuration file') + arg_parser.add_argument('--internims_url', help='https://internims.appspot.com') + arg_parser.add_argument('--db_uri', help='DB URI') + arg_parser.add_argument('--api_uri', help='API URL, without http:// or https://') + arg_parser.add_argument('--site_id', help='instance ID') + arg_parser.add_argument('--sleeptime', default=60, type=int, help='time to sleep between is alive signals') + arg_parser.add_argument('-k', '--ssl_key', help='path to privkey file') + args = arg_parser.parse_args() + + config = ConfigParser.ConfigParser({'here': os.path.dirname(os.path.abspath(args.configfile))}) + config.read(args.configfile) + + logging.config.fileConfig(args.configfile, disable_existing_loggers=False) + log = logging.getLogger('nimsapi') + + privkey_file = args.ssl_key or config.get('nims', 'ssl_key') + if privkey_file: + try: + privkey = Crypto.PublicKey.RSA.importKey(open(privkey_file).read()) + except: + log.warn(privkey_file + ' is not a valid private SSL key file, bailing out.') + sys.exit(1) + else: + log.info('successfully loaded private SSL key from ' + privkey_file) + else: + log.warn('private SSL key not specified, bailing out.') + sys.exit(1) + + db_uri = args.db_uri or config.get('nims', 'db_uri') + db = (pymongo.MongoReplicaSetClient(db_uri) if 'replicaSet' in db_uri else pymongo.MongoClient(db_uri)).get_default_database() + + site_id = args.site_id or config.get('nims', 'site_id') + api_uri = args.api_uri or config.get('nims', 'api_uri') + internims_url = args.internims_url or config.get('nims', 'internims_url') + + while True: + update(db, api_uri, site_id, privkey, internims_url) + time.sleep(args.sleeptime) diff --git a/nimsapi.py b/nimsapi.py index 1d2ea574..6d18beb3 100755 --- a/nimsapi.py +++ b/nimsapi.py @@ -18,14 +18,13 @@ import Crypto.PublicKey.RSA import logging import logging.config +log = logging.getLogger('nimsapi') import experiments import nimsapiutil import collections_ import tempdir as tempfile -log = logging.getLogger('nimsapi') - class NIMSAPI(nimsapiutil.NIMSRequestHandler): @@ -129,6 +128,11 @@ 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)) + class Users(nimsapiutil.NIMSRequestHandler): @@ -362,38 +366,12 @@ class Group(nimsapiutil.NIMSRequestHandler): """Delete an Group.""" -class Remotes(nimsapiutil.NIMSRequestHandler): - - """/nimsapi/remotes """ - - def get(self): - """Return the list of remotes where user has membership""" - # TODO: implement special 'all' case - report ALL available instances, regardless of user permissions - # applies to adding new remote users, need to be able to select from ALL available remote sites - # query, user in userlist, _id does not match this site _id - query = {'users': {'$in': [self.user['_id']]}, '_id': {'$ne': self.app.config['site_id']}} - projection = ['_id'] - # if app has no site-id or pubkey, cannot fetch peer registry, and db.remotes will be empty - remotes = list(self.app.db.remotes.find(query, projection)) - data_remotes = [] # for list buildup - for remote in remotes: - # use own API to dispatch requests (hacky) - response = self.app.get_response('/nimsapi/experiments?user=' + self.user['_id'] + '&iid=' + remote['_id'], headers=[('User-Agent', 'remotes_requestor')]) - xpcount = len(json.loads(response.body)) - if xpcount > 0: - log.debug('%s has access to %s expirements on %s' % (self.user['_id'], xpcount, remote['_id'])) - data_remotes.append(remote['_id']) - - # return json encoded list of remote site '_id's - self.response.write(json.dumps(data_remotes, indent=4, separators=(',', ': '))) - - 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'/remotes', Remotes), + webapp2.Route(r'/remotes', NIMSAPI, handler_method='remotes', 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']), @@ -450,6 +428,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) if args.ssl_key: diff --git a/nimsapi.wsgi b/nimsapi.wsgi index ba24f7f6..6915565d 100644 --- a/nimsapi.wsgi +++ b/nimsapi.wsgi @@ -1,177 +1,68 @@ -#!/usr/bin/env python -# # @author: Gunnar Schaefer, Kevin S. Hahn import os import sys import site import ConfigParser -import logging -import logging.config - - -def apply_config(configfile): - """Return a ConfigParser object""" - config = ConfigParser.ConfigParser() - config.read(configfile) - site.addsitedir(os.path.join(config.get('nims', 'virtualenv'), 'lib', 'python2.7', 'site-packages')) - sys.path.append(config.get('nims', 'here')) - os.environ['PYTHON_EGG_CACHE'] = config.get('nims', 'python_egg_cache') - return config - - -def configure_logger(configfile): - """return a nimsapi configured logger""" - logging.config.fileConfig(configfile, disable_existing_loggers=False) - return logging.getLogger('nimsapi') - - -def read_privkey(privkey_file): - """reads SSL private key. returns key content as RSA.key object""" - try: - privkey = Crypto.PublicKey.RSA.importKey(open(privkey_file).read()) - except: - log.warn(privkey_file + ' is not a valid private SSL key file') - privkey = None - else: - log.info('successfully loaded private SSL key from ' + privkey_file) - return privkey - - -def connect_db(db_uri): - """return mongodb default database""" - kwargs = dict(tz_aware=True) - db_client = pymongo.MongoReplicaSetClient(db_uri, **kwargs) if 'replicaSet' in db_uri else pymongo.MongoClient(db_uri, **kwargs) - db = db_client.get_default_database() - db.remotes.ensure_index('UTC', expireAfterSeconds=120) - return db - - -def internimsclient(db, hostname, site_id, privkey, internims_url): - """sends is alive to internims central. no return""" - # TODO: create a list of non-local users, who have permissions on experiments - users = [users['_id'] for users in list(db.users.find({}, {'_id': True}))] - - payload = json.dumps({'iid': site_id, 'hostname': hostname, 'users': users}) - h = Crypto.Hash.SHA.new(payload) - 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) - if r.status_code == 200: - sites = json.loads(r.content) - for site in sites: - site['UTC'] = datetime.datetime.strptime(site['timestamp'], '%Y-%m-%dT%H:%M:%S.%f') - db.remotes.find_and_modify(query={'_id': site['_id']}, update=site, upsert=True) - log.debug('upserting remote site ' + site['_id']) - else: - log.info((r.status_code, r.reason)) - - -if __name__ == '__main__': - import argparse - - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument('-c', '--configfile', help='path to configuration file') - arg_parser.add_argument('--internims_url', help='https://internims.appspot.com') - arg_parser.add_argument('--db_uri', help='DB URI') - arg_parser.add_argument('--hostname', help='fqdn, without protocol') - arg_parser.add_argument('--site_id', help='instance ID') - arg_parser.add_argument('--sleeptime', default=60, type=int, help='time to sleep between is alive signals') - arg_parser.add_argument('-k', '--privkey', help='path to privkey file') - args = arg_parser.parse_args() - - # read config - config = apply_config(args.configfile) - # import everything else - import json - import time - import base64 - import pymongo - import webapp2 - import nimsapi - import nimsutil - import requests - import datetime - import Crypto.Random - import Crypto.Hash.SHA - import Crypto.PublicKey.RSA - import Crypto.Signature.PKCS1_v1_5 - import signal # not in uwsgi execution - - # configure logger - log = configure_logger(args.configfile) - - # args override configfile - db_uri = args.db_uri or config.get('nims', 'db_uri') - hostname = args.hostname or config.get('nims', 'hostname') - site_id = args.site_id or config.get('nims', 'site_id') - internims_url = args.internims_url or config.get('nims', 'internims_url') - privkey_file = args.privkey or config.get('nims', 'privkey_file') - - # load in the privkey - privkey = read_privkey(privkey_file) - - # connect to db - db = connect_db(db_uri) - - def term_handler(signum, stack): - alive = False - log.debug('Recieved SIGTERM - shuttin down') - - signal.signal(signal.SIGTERM, term_handler) - - alive = True - while alive: - internimsclient(db, hostname, site_id, privkey, internims_url) - time.sleep(args.sleeptime) +configfile = '../production.ini' +config = ConfigParser.ConfigParser() +config.read(configfile) + +site.addsitedir(os.path.join(config.get('nims', 'virtualenv'), 'lib', 'python2.7', 'site-packages')) +sys.path.append(config.get('nims', 'here')) +os.environ['PYTHON_EGG_CACHE'] = config.get('nims', 'python_egg_cache') + +import json +import time +import base64 +import pymongo +import webapp2 +import datetime +import requests +import Crypto.Random +import Crypto.Hash.SHA +import uwsgidecorators +import Crypto.PublicKey.RSA +import Crypto.Signature.PKCS1_v1_5 +import logging +import logging.config +logging.config.fileConfig(configfile, disable_existing_loggers=False) +log = logging.getLogger('nimsapi') + +import nimsapi +import internimsclient + +# read in private key +privkey_file = config.get('nims', 'ssl_key') +try: + privkey = Crypto.PublicKey.RSA.importKey(open(privkey_file).read()) +except: + log.warn(privkey_file + 'is not a valid private SSL key file') + privkey = None else: - # read config - configfile = '../production.ini' - config = apply_config(configfile) - - # import everything else - import json - import time - import base64 - import pymongo - import webapp2 - import nimsapi - import argparse - import datetime - import nimsutil - import requests - import Crypto.Random - import Crypto.Hash.SHA - import Crypto.PublicKey.RSA - import Crypto.Signature.PKCS1_v1_5 - import uwsgidecorators # only in uwsgi execution - - # configure logger - log = configure_logger(configfile) - - db_uri = config.get('nims', 'db_uri') - stage_path = config.get('nims', 'stage_path') - site_id = config.get('nims', 'site_id') - - # load in privkey - privkey = read_privkey(config.get('nims', 'privkey_file')) - - # config uwsgi application - application = nimsapi.app - application.config['stage_path'] = stage_path - application.config['site_id'] = site_id - application.config['privkey'] = privkey - - # connect db - application.db = connect_db(db_uri) - - @uwsgidecorators.postfork - def random_atfork(): - Crypto.Random.atfork() - - @uwsgidecorators.timer(60) - def internimsclient_timer(signum): - internimsclient(application.db, config.get('nims', 'hostname'), config.get('nims', 'site_id'), privkey, config.get('nims', 'internims_url')) + log.info('successfully loaded private SSL key from ' + privkey_file) + +# configure uwsgi application +site_id = config.get('nims', 'site_id') +application = nimsapi.app +application.config['stage_path'] = config.get('nims', 'stage_path') +application.config['site_id'] = site_id +application.config['ssl_key'] = privkey + +# connect to db +db_uri = config.get('nims', 'db_uri') +application.db = (pymongo.MongoReplicaSetClient(db_uri) if 'replicaSet' in db_uri else pymongo.MongoClient(db_uri)).get_default_database() + +# send is-alive signals +api_uri = config.get('nims', 'api_uri') +internims_url = config.get('nims', 'internims_url') + +@uwsgidecorators.timer(60) +def internimsclient_timer(signum): + internimsclient.update(application.db, api_uri, site_id, privkey, internims_url) + +@uwsgidecorators.postfork +def random_atfork(): + Crypto.Random.atfork() diff --git a/nimsapiutil.py b/nimsapiutil.py index 76f737c4..ab6044c8 100644 --- a/nimsapiutil.py +++ b/nimsapiutil.py @@ -2,8 +2,7 @@ import json import base64 -import socket # socket.gethostname() -import logging +import socket import webapp2 import datetime import requests @@ -12,6 +11,7 @@ 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 @@ -93,7 +93,7 @@ class NIMSRequestHandler(webapp2.RequestHandler): elif self.ssl_key is not None and self.site_id is not None: log.debug(socket.gethostname() + ' dispatching to remote ' + self.target_id) # is target registered? - target = self.app.db.remotes.find_one({'_id': self.target_id}, {'_id':False, 'hostname':True}) + 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') @@ -111,8 +111,7 @@ class NIMSRequestHandler(webapp2.RequestHandler): reqheaders['Authorization'] = base64.b64encode(signature) # construct outgoing request - target_api = 'http://' + target['hostname'] + self.request.path # TODO: switch to https - # target_api = 'https://' + target['hostname'] + self.request.path) + target_api = 'https://' + target['api_uri'] + self.request.path.split('/nimsapi')[1] r = requests.request(method=self.request.method, data=reqpayload, url=target_api, params=reqparams, headers=reqheaders, verify=False) # return response content -- GitLab