Exemple #1
0
    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
Exemple #2
0
    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)
Exemple #3
0
 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)
Exemple #4
0
    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)
Exemple #5
0
    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
Exemple #6
0
    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
Exemple #7
0
    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())
Exemple #8
0
 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
Exemple #9
0
 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
Exemple #10
0
    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)
Exemple #11
0
    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
        })
Exemple #12
0
 def debug_hook(*args):
     """debug hook"""
     log("ABAC DEBUG", *args, type="debug")
Exemple #13
0
    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()