def connect(self): """Wrap MySQL connect to handle idle connections""" # Mysql closes idle connections, but we have no easy way of knowing # until we try to use it, so set our idle time to be less than the # setting on the server. Sync this time with the settings. if self.expired(): self.close() while not self.dbc: try: # use_pure=False is asserting to use the native-c build self.dbc = mysql.connector.connect(use_pure=False, **self.master.config) except Exception as err: # pylint: disable=broad-except if self.do_DEBUG('db'): log("Connect Problem, waiting...", traceback=traceback.format_exc(), type="error") else: log("Connect Problem, waiting...", error=str(err), type="error") time.sleep(1) self.dbc.autocommit = True self._last_used = time.time() return self
def access(self): """log access""" request = cherrypy.serving.request remote = request.remote response = cherrypy.serving.response outheaders = response.headers inheaders = request.headers if response.output_status is None: status = "-" else: status = response.output_status.split(" ".encode(), 1)[0] remaddr = inheaders.get('X-Forwarded-For', None) or \ remote.name or remote.ip if isinstance(status, bytes): status = status.decode() # this is set in abac.py login = cherrypy.serving.request.login kwargs = dict() if login and login.token_name: kwargs['token'] = login.token_name # Notes: insert other auth attributes? log("type=http status=" + str(status), query=request.request_line, remote=remaddr, len=outheaders.get('Content-Length', '') or '-', reqid=cherrypy.serving.request.reqid, **kwargs)
def emit(self, record): """Overriding emit""" try: msg = record.msg.strip() log(msg) except (KeyboardInterrupt, SystemExit): raise except: # pylint: disable=bare-except self.handleError(record)
def error(self, msg='', context='', severity=logging.INFO, traceback=False): """log error""" kwargs = {} if traceback: # pylint: disable=protected-access kwargs['traceback'] = cherrypy._cperror.format_exc() if not msg: msg = "error" if isinstance(msg, bytes): msg = msg.decode() log(error=msg, type="error", context=context, severity=severity, **kwargs)
def do(self, stmt, *args): """Run a statement, return the cursor.""" try: cursor, stmt = self.prepare(stmt) except mysql.connector.errors.InternalError as err: # bad coding, but this avoids blowing out future connections if str(err) == "Unread result found": self.close() raise except mysql.connector.errors.OperationalError as err: log("db error", error=str(err), type="error") self.close() raise cursor.execute(stmt, args) return cursor
def allowed(self, attrs, debug=False, base=None): """ process an ABAC policy expression. Context is provided as a dict to add to the namespace of the expression. policy is always added to the context >>> p=Policy() >>> attrs=attrs_skeleton(token_nbr=1, token_name='sinatra') >>> p.compile("__import__('os').system('echo oops')").allowed({}) Traceback (most recent call last): ... NameError: name '__import__' is not defined >>> p.compile("token_name == 'frank'").allowed(attrs) Traceback (most recent call last): ... exceptions.PolicyFailed: Policy failed to evaluate (token_name == 'frank') >>> p.compile("re('^sin', token_name)").allowed(attrs) True >>> p.compile("token_name == 'sinatra'").allowed(attrs) True >>> p.compile("token_nbr == 0").allowed(attrs) Traceback (most recent call last): ... exceptions.PolicyFailed: Policy failed to evaluate (token_nbr == 0) >>> p.compile("token_nbr > 0").allowed(attrs) True """ if not isinstance(attrs, dict): raise exceptions.InvalidContext("Context is not a dictionary") try: # pylint: disable=eval-used if eval(self.policy_ast, abac_context(), attrs): return True except KeyError as err: log("policy failure id={} missing key={}".format( self.policy_id, err), type="error") except Exception as err: # pylint: disable=broad-except if debug and base: base.DEBUG("abac error={}, traceback={}".format( err, traceback.format_exc()), module="abac") return False
def monitor(self): """ internal heartbeat from Cherrypy.process.plugins.Monitor """ self.stat.heartbeat.last = time.time() alive = 0 dead = 0 pool = self.dbm.pool.copy() # thread changes it for dbi_id in pool: if self.dbm.pool[dbi_id].alive(): alive += 1 else: dead += 1 self.stat.dbm.alive = alive self.stat.dbm.dead = dead if self.stat.next_report < self.stat.heartbeat.last: log("type=status-report", **self.status_report())
def pwsin(hdrs, ingrp): """closure for multiple passwords input in as X-Passwords""" # pylint: disable=too-many-nested-blocks try: grp = groups.get(ingrp) hdr = hdrs.get('X-Passwords') if not hdr: log("Missing X-Passwords header, try adding --password --password", type="error") return False pwds = json2data(base64.b64decode(hdr)) pwdset = set(pwds) if len(pwds) != len(pwdset): raise Exception( "Multiple passwords provided, but some are duplicates") if not grp: log("policy cannot find group={}".format(grp), type="error") else: matches = 0 for pwhash in grp: pwhash = pwhash.encode() for pwd in pwds: pwd = pwd.encode() try: if nacl.pwhash.verify_scryptsalsa208sha256( pwhash, pwd): matches += 1 if matches == len(pwds): return True except nacl.exceptions.InvalidkeyError: pass except Exception as err: # pylint: disable=broad-except log("pwsin() error=" + str(err), type="error") return False
def pwin(hdrs, ingrp): """closure for single password input in as X-Password""" try: grp = groups.get(ingrp) hdr = hdrs.get('X-Password') if not hdr: log("Missing X-Password header, try adding --password", type="error") return False pwd = base64.b64decode(hdr) if not grp: log("policy cannot find group={}".format(grp), type="error") else: for pwhash in grp: pwhash = pwhash.encode() try: if nacl.pwhash.verify_scryptsalsa208sha256( pwhash, pwd): return True except nacl.exceptions.InvalidkeyError: pass except Exception as err: # pylint: disable=broad-except log("pwin() error=" + str(err), type="error") return False
def _rest_crud(self, method, *args, **kwargs): """Called by the relevant method when content should be posted""" cherrypy.serving.request.reqid = self.reqid = next(self.reqgen) do_abac_log = False if kwargs.get('abac') == "log": if set_DEBUG('abac', True): do_abac_log = True try: return getattr(self, method)(*args, **kwargs) except exceptions.AuthFailed as err: log("authfail", reason=err.args[1]) cherrypy.response.status = 401 return {"status": "failed", "message": "Unauthorized"} except exceptions.PolicyFailed as err: if do_DEBUG('auth'): log("forbidden", traceback=json2data(traceback.format_exc())) else: log("forbidden", reason=err.args[0]) cherrypy.response.status = 403 return {"status": "failed", "message": "Forbidden"} except (ValueError, exceptions.InvalidParameter, Error) as err: status = {"status": "failed"} cherrypy.response.status = 400 if type(err) in (list, tuple, Error): # pylint: disable=unidiomatic-typecheck cherrypy.response.status = err.args[1] if isinstance(err.args[0], dict): status = err.args[0] status.update({'status': 'failed'}) # pylint: disable=no-member else: status['message'] = err.args[0] else: status['message'] = str(err) return status except Exception as err: log("error", traceback=json2data(traceback.format_exc())) raise finally: if do_abac_log: set_DEBUG('abac', False)
def rest_read(self, *args, **kwargs): """Receive an Apikey and give a Session Token""" # trace("Token read") # authorize if 'X-ApiKey' not in cherrypy.request.headers: return self.respond_failure("Unauthorized", status=401) try: jwt_apikey = cherrypy.request.headers['X-ApiKey'] token_id = get_jti(jwt_apikey) try: token = dbo.Apikey(master=self.server.dbm).get(token_id, True) except dbo.ObjectNotFound: return self.auth_fail("Apikey not found") # validate base on array of secrets jwt_data = None for secret_raw in token.obj.get('secrets', []): # there is a problem with binary secrets across language bases for secret in (secret_raw, base64.b64decode(secret_raw)): try: # pylint: disable=no-member jwt_data = jwt.decode(jwt_apikey, secret) break except jwt.exceptions.DecodeError: continue except jwt.exceptions.ExpiredSignatureError: # pylint: disable=no-member self.auth_fail("JWT expired") if jwt_data: break if not jwt_data: self.auth_fail("JWT cannot be decoded") if not jwt_data.get('exp'): self.auth_fail("JWT missing expiration") # 36 chars is a UUID4 if not jwt_data.get('seed') or len(jwt_data.get('seed')) < 36: self.auth_fail("JWT seed missing") if time.time() - jwt_data['exp'] > self.server.conf.auth.expires: self.auth_fail("JWT bad expiration (too great)") # the signature matches, it is good expires_at = time.time() + self.server.conf.auth.expires auth_session = dbo.AuthSession(master=self.server.dbm) auth_session.new_session(token, expires_at, { 'token_id': token.obj['id'], 'token_name': token.obj['name'] }) cookie = cherrypy.response.cookie cookie['sid'] = auth_session.obj['session_id'] cookie['sid']['path'] = self.server.conf.server.route_base cookie['sid']['max-age'] = self.server.conf.auth.expires or 300 cookie['sid']['version'] = 1 log("type=auth", apikey=token.obj['id'], token=token.obj['name']) except Exception as err: # pylint: disable=broad-except if self.server: if self.server.do_DEBUG(): log(type="error", traceback=json2data(traceback.format_exc())) else: log(type="error", traceback=json2data(traceback.format_exc())) return self.auth_fail(str(err)) # finally: # trace("Token read DONE") return self.respond({ "status": "success", "session": auth_session.obj['session_id'], "secret": auth_session.obj['secret_encoded'], "jti": auth_session.obj['token_id'], "expires_at": expires_at })
def debug_hook(*args): """debug hook""" log("ABAC DEBUG", *args, type="debug")
def start(self, test=True): """ Startup script for webhook routing. Called from agent start """ cherrypy.log = CherryLog() cherrypy.config.update({ 'log.screen': False, 'log.access_file': '', 'log.error_file': '' }) cherrypy.engine.unsubscribe('graceful', cherrypy.log.reopen_files) logging.config.dictConfig({ 'version': 1, 'formatters': { 'custom': { '()': 'rfxengine.server.cherry.Logger' } }, 'handlers': { 'console': { 'level':'INFO', 'class':'rfxengine.server.cherry.Logger', #logging.StreamHandler', 'formatter': 'custom', 'stream': 'ext://sys.stdout' } }, 'loggers': { '': { 'handlers': ['console'], 'level': 'INFO' }, 'cherrypy.access': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False }, 'cherrypy.error': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False }, } }) defaults = { 'deploy_ver': 0, # usable for deployment tools 'server': { 'route_base': '/api/v1', 'port': 54000, 'host': '0.0.0.0' }, 'heartbeat': 10, 'status_report': 3600, # every hour 'requestid': False, 'refresh_maps': 300, 'cache': { 'housekeeper': 60, 'policies': 300, 'sessions': 300, 'groups': 300 }, 'crypto': { # pylint: disable=bad-continuation # '000': { # dd if=/dev... # 'key': "", # 'default': True, # } }, 'db': { 'database': 'reflex_engine', 'user': '******' }, 'auth': { 'expires': 300 } } cfgin = None # try docker secrets if os.path.exists("/run/secrets/REFLEX_ENGINE_CONFIG"): with open("/run/secrets/REFLEX_ENGINE_CONFIG") as infile: cfgin = infile.read() # try environ if not cfgin: cfgin = os.environ.get('REFLEX_ENGINE_CONFIG') if cfgin: try: cfgin = json2data(base64.b64decode(cfgin)) except: # pylint: disable=bare-except try: cfgin = json2data(cfgin) except Exception as err: # pylint: disable=broad-except traceback.print_exc() self.ABORT("Cannot process REFLEX_ENGINE_CONFIG: " + str(err) + " from " + cfgin) conf = dictlib.Obj(dictlib.union(defaults, cfgin)) else: self.NOTIFY("Unable to find configuration, using defaults!") conf = dictlib.Obj(defaults) # cherry py global cherry_conf = { 'server.socket_port': 9000, 'server.socket_host': '0.0.0.0' } if dictlib.dig_get(conf, 'server.port'): # .get('port'): cherry_conf['server.socket_port'] = int(conf.server.port) if dictlib.dig_get(conf, 'server.host'): # .get('host'): cherry_conf['server.socket_host'] = conf.server.host # if production mode if test: log("Test mode enabled", type="notice") conf['test_mode'] = True else: cherry_conf['environment'] = 'production' conf['test_mode'] = False # db connection self.dbm = mxsql.Master(config=conf.db, base=self, crypto=conf.get('crypto')) # configure the cache self.dbm.cache = rfxengine.memstate.Cache(**conf.cache.__export__()) self.dbm.cache.start_housekeeper(conf.cache.housekeeper) # schema schema = dbo.Schema(master=self.dbm) schema.initialize(verbose=False, reset=False) sys.stdout.flush() cherrypy.config.update(cherry_conf) endpoint_conf = { '/': { 'response.headers.server': "stack", 'tools.secureheaders.on': True, 'request.dispatch': cherrypy.dispatch.MethodDispatcher(), 'request.method_with_bodies': ('PUT', 'POST', 'PATCH'), } } cherrypy.config.update({'engine.autoreload.on': False}) self.conf = conf # startup cleaning interval def clean_keys(dbm): """periodically called to purge expired auth keys from db""" dbo.AuthSession(master=dbm).clean_keys() timeinterval.start(conf.auth.expires * 1000, clean_keys, self.dbm) # recheck policymaps every so often def check_policymaps(dbm): """ periodically remap policy maps, incase somebody was fidgeting where they shoudln't be """ dbo.Policyscope(master=dbm).remap_all() timeinterval.start(conf.refresh_maps * 1000, check_policymaps, self.dbm) # mount routes cherrypy.tree.mount(endpoints.Health(conf, server=self), conf.server.route_base + "/health", endpoint_conf) cherrypy.tree.mount(endpoints.Token(conf, server=self), conf.server.route_base + "/token", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="config"), conf.server.route_base + "/config", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="service"), conf.server.route_base + "/service", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="pipeline"), conf.server.route_base + "/pipeline", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="instance"), conf.server.route_base + "/instance", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="build"), conf.server.route_base + "/build", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="group"), conf.server.route_base + "/group", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="apikey"), conf.server.route_base + "/apikey", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="policy"), conf.server.route_base + "/policy", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="policyscope"), conf.server.route_base + "/policyscope", endpoint_conf) cherrypy.tree.mount(endpoints.Object(conf, server=self, obj="state"), conf.server.route_base + "/state", endpoint_conf) cherrypy.tree.mount(endpoints.InstancePing(conf, server=self), conf.server.route_base + "/instance-ping", endpoint_conf) # cherrypy.tree.mount(endpoints.Compose(conf, server=self), # conf.server.route_base + "/compose", # endpoint_conf) # setup our heartbeat monitor int_mon = cherrypy.process.plugins.Monitor(cherrypy.engine, self.monitor, frequency=conf.heartbeat/2) int_mon.start() log("Base path={}".format(conf.server.route_base), type="notice") cherrypy.engine.start() cherrypy.engine.block()