예제 #1
0
def after_request(resp):
    """
    Adds HTTP Headers to the response

    Arguments
    ----------
    resp : flask.Response object
      the Flask response object
    """
    resp.headers['Cache-Control'] = 'no-cache'

    if app.config['DEV']:
        resp.headers['Access-Control-Allow-Origin'] = '*'
        resp.headers['Access-Control-Allow-Methods'] = 'GET, POST'
        resp.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept'

        source_ip = get_real_source_ip()

        structured_log(
            level='info',
            msg="HTTP Request",
            method=request.method,
            uri=request.path,
            status=resp.status_code,
            src_ip=source_ip,
            protocol=request.environ['SERVER_PROTOCOL'],
            user_agent=request.environ.get('HTTP_USER_AGENT', '-')
        )

    return resp
예제 #2
0
파일: config.py 프로젝트: mieitza/beesly
    def __init__(self):
        self.prefix = __app__.lower()
        self.host = os.environ.get("STATSD_HOST", "localhost")

        try:
            self.port = int(os.environ.get('STATSD_PORT', 8125))
        except ValueError:
            self.port = 8125

        # check that STATSD_HOST is a valid hostname or IPv4 address
        if self.host != "localhost":
            try:
                socket.gethostbyname(self.host)
            except:
                try:
                    socket.inet_aton(self.host)
                except:
                    structured_log(level='error', msg="Invalid value provided for STATSD_HOST. Defaulting to localhost")
                    self.host = "localhost"

        self.client = StatsClient(host=self.host, port=self.port, prefix=self.prefix)

        structured_log(
            level='info',
            msg="Statsd client configured to export metrics to %s:%s" % (self.host, self.port)
        )
예제 #3
0
def exception_handler(err):
    """
    Catches any exceptions raised by this microservice and responds with
    HTTP 500 and a custom error message.
    """
    tb = traceback.format_exc()
    formatted_traceback = ' '.join(tb.splitlines()[1:]).strip()

    structured_log(level='error', msg="Encountered an exception", traceback=formatted_traceback)
    return jsonify(error="The application failed to handle the request"), 500
예제 #4
0
def verify_endpoint():
    """
    Verifies if a JWT is valid.
    """
    if request.method == 'POST':

        if not app.config["JWT"]:
            return jsonify(message="JWT verification is not enabled"), 501

        request_json = request.get_json(force=True, cache=False)

        token = request_json.get('jwt', None)

        if token is None:
            return jsonify(message="No JWT provided"), 400

        try:
            claims = jwt.get_unverified_claims(token)
        except:
            return jsonify(message="Invalid JWT"), 400

        master_key  = app.config["JWT_MASTER_KEY"]
        algorithm   = app.config["JWT_ALGORITHM"]
        issuer      = app.config['APP_NAME']

        try:
            subject = claims["sub"].encode('utf-8')
            salt    = claims["x"].encode('utf-8')
        except KeyError:
            return jsonify(message="Invalid claims in JWT", valid=False), 401

        # jwt.decode() requires secret_key to be a str, so it must be decoded
        secret_key = blake2b(b'', key=master_key, salt=salt, person=subject).decode('utf-8')

        # exception is raised if token has expired, signature verification fails, etc.
        try:
            jwt.decode(token=token, key=secret_key, algorithms=algorithm, issuer=issuer)
        except Exception as err:
            structured_log(level='info', msg="Failed to verify JWT", error=err)
            return jsonify(message="Failed to verify JWT", valid=False), 401

        statsd.client.incr("jwt_verified")
        structured_log(level='info', msg="JWT successfully verified", user=f"'{subject}'")
        return jsonify(message="JWT successfully verified", valid=True), 200
예제 #5
0
파일: __init__.py 프로젝트: bincyber/beesly
def create_app():
    """
    Initializes the Flask application.
    """

    structured_log(level='info', msg="Starting beesly...")

    try:
        settings = initialize_config()
    except ConfigError:
        structured_log(level='critical', msg="Failed to load configuration. Exiting...")
        sys.exit(4)

    # enable Swagger UI if running in DEV mode
    if settings["DEV"]:
        app.static_folder = os.path.dirname(os.path.realpath(__file__)) + "/swagger-ui"
        app.add_url_rule("/service/docs/<path:filename>", endpoint="/service/docs", view_func=app.send_static_file)

    app.config.update(settings)
    structured_log(level='info', msg="Successfully loaded configuration")

    rlimiter.init_app(app)

    return app
예제 #6
0
def renew_endpoint():
    """
    Renews a JWT that has not expired.
    """
    if request.method == 'POST':

        if not app.config["JWT"]:
            return jsonify(message="JWT renewal is not enabled"), 501

        request_json = request.get_json(force=True, cache=False)

        token       = request_json.get('jwt', None)
        username    = request_json.get('username', None)

        if token is None or username is None:
            return jsonify(message="No JWT or username provided"), 400

        sanitized_username = str(escape(username))

        if not validate_username(sanitized_username):
            structured_log(level='warning', msg="Invalid username provided", user=f"'{sanitized_username}'")
            return jsonify(message="Invalid username provided"), 400

        try:
            claims = jwt.get_unverified_claims(token)
        except JWTError:
            return jsonify(message="Invalid JWT"), 400

        try:
            subject = claims["sub"].encode('utf-8')
            salt    = claims["x"].encode('utf-8')
        except KeyError:
            return jsonify(message="Invalid claims in JWT"), 401

        if sanitized_username != claims["sub"]:
            return jsonify(message="Invalid subject in JWT claim"), 400

        master_key  = app.config["JWT_MASTER_KEY"]
        algorithm   = app.config["JWT_ALGORITHM"]
        issuer      = app.config['APP_NAME']

        # jwt.decode() requires secret_key to be a str, so it must be decoded
        secret_key = blake2b(b'', key=master_key, salt=salt, person=subject).decode('utf-8')

        # exception is raised if token has expired, signature verification fails, etc.
        try:
            payload = jwt.decode(token=token, key=secret_key, algorithms=algorithm, issuer=issuer)
        except Exception as err:
            structured_log(level='info', msg="Failed to renew JWT", error=err)
            return jsonify(message="Failed to renew invalid JWT"), 401

        issue_time  = time.time()
        expiry_time = issue_time + app.config['JWT_VALIDITY_PERIOD']

        salt = URLSafeBase64Encoder.encode(nacl.utils.random(12))

        payload['iat']  = issue_time
        payload['exp']  = expiry_time
        payload['x']    = salt.decode('utf-8')

        # compute a new secret key for the regenerated JWT
        new_secret_key  = blake2b(b'', key=master_key, salt=salt, person=subject).decode('utf-8')
        new_token       = jwt.encode(claims=payload, key=new_secret_key, algorithm=algorithm)

        statsd.client.incr("jwt_renewed")
        structured_log(level='info', msg="JWT successfully renewed", user="******".format(sanitized_username))
        return jsonify(message="JWT successfully renewed", jwt=new_token), 200
예제 #7
0
def auth_endpoint():
    """
    Authenticates users using PAM.
    If authentication is successful, returns a list of groups the user is a member of.
    Returns a short-lived JSON Web Token if JWT_MASTER_KEY is set.
    """
    if request.method == 'POST':
        request_json = request.get_json(force=True, cache=False)

        username = request_json.get('username', None)
        password = request_json.get('password', None)

        if username is None or password is None:
            return jsonify(message="No username or password provided"), 400

        sanitized_username = str(escape(username))

        if not validate_username(sanitized_username):
            structured_log(level='warning', msg="Invalid username provided", user=f"'{sanitized_username}'")
            return jsonify(message="Invalid username provided"), 400

        pam_service = app.config['PAM_SERVICE']

        with statsd.client.timer("pam_auth"):
            if pam().authenticate(sanitized_username, password, service=pam_service):
                authenticated = True
                auth_message = "Authentication successful"
                statsd.client.incr("auth_success")
            else:
                authenticated = False
                auth_message = "Authentication failed"
                statsd.client.incr("auth_failed")

        structured_log(level='info', msg=auth_message, user=f"'{sanitized_username}'")

        if authenticated:
            groups = get_group_membership(sanitized_username)

            token = None

            if app.config["JWT"]:
                issuer      = app.config['APP_NAME']
                issue_time  = time.time()
                expiry_time = issue_time + app.config['JWT_VALIDITY_PERIOD']
                subject     = sanitized_username.encode('utf-8')

                # generate a unique salt for each JWT, needs to be encoded to include as a claim
                salt = URLSafeBase64Encoder.encode(nacl.utils.random(12))

                claims = {
                    "iss": issuer,
                    "iat": issue_time,
                    "exp": expiry_time,
                    "sub": sanitized_username,
                    "groups": groups,
                    "x": salt.decode('utf-8')
                }

                master_key  = app.config["JWT_MASTER_KEY"]
                algorithm   = app.config["JWT_ALGORITHM"]

                # generate a unique secret key for each JWT
                secret_key = blake2b(b'', key=master_key, salt=salt, person=subject).decode('utf-8')

                token = jwt.encode(claims=claims, key=secret_key, algorithm=algorithm)
                statsd.client.incr("jwt_generated")

            return jsonify(message=f"{auth_message}", auth=True, groups=groups, jwt=token), 200
        else:
            return jsonify(message=f"{auth_message}", auth=False), 401
예제 #8
0
파일: config.py 프로젝트: mieitza/beesly
def initialize_config():
    """
    Initializes the application's configuration by reading settings from
    environment variables and validating them. Returns a dictionary containing
    application configuration if validation passes, otherwise ConfigError exception is raised.
    """
    settings = {}

    # ensure that all dependencies used exist
    dependencies = ['id']
    for binary in dependencies:
        if find_executable(binary) is None:
            structured_log(level='critical', msg="Failed to locate required dependency", dependency=binary)
            raise ConfigError()

    settings['APP_NAME']    = __app__
    settings['APP_VERSION'] = __version__
    settings['DEV']         = strtobool(os.environ.get("DEV", 'False'))

    settings["RATELIMIT_ENABLED"]       = strtobool(os.environ.get("RATELIMIT_ENABLED", 'True'))
    settings["RATELIMIT_STRATEGY"]      = os.environ.get("RATELIMIT_STRATEGY", 'fixed-window')
    settings["RATELIMIT_STORAGE_URL"]   = os.environ.get("RATELIMIT_STORAGE_URL", 'memory://')

    if not settings["RATELIMIT_ENABLED"]:
        structured_log(level='info', msg="Rate limiting disabled")

    if settings["RATELIMIT_ENABLED"]:

        rate_limit_strategies       = ['fixed-window', 'fixed-window-elastic-expiry', 'moving-window']
        rate_limit_storage_schemes  = ['memory', 'memcached', 'redis', 'rediss', 'redis+sentinel', 'redis_cluster']

        storage_scheme = urlparse(settings["RATELIMIT_STORAGE_URL"]).scheme

        if settings["RATELIMIT_STRATEGY"] not in rate_limit_strategies:
            structured_log(level='error', msg="Invalid value provided for RATELIMIT_STRATEGY")
            raise ConfigError()

        if storage_scheme not in rate_limit_storage_schemes:
            structured_log(level='error', msg="Invalid value provided for RATELIMIT_STORAGE_URL")
            raise ConfigError()

        if settings["RATELIMIT_STRATEGY"] == 'moving-window' and storage_scheme == 'memcached':
            structured_log(level='error', msg="Invalid value provided for RATELIMIT_STORAGE_URL. moving-window can't be used with memcached")
            raise ConfigError()

    # python-pam module allows specfiying which PAM service by name to authenticate against
    settings['PAM_SERVICE'] = os.environ.get("PAM_SERVICE", 'login')

    pam_file = '/etc/pam.d/' + settings['PAM_SERVICE']
    if not os.path.exists(pam_file):
        structured_log(level='error', msg="Invalid value provided for PAM_SERVICE. The pam configuration file '%s' does not exist" % pam_file)
        raise ConfigError()

    # configure JWT, by default it's disabled
    settings["JWT"] = False

    settings["JWT_MASTER_KEY"]  = os.environ.get('JWT_MASTER_KEY', None)
    settings["JWT_ALGORITHM"]   = os.environ.get('JWT_ALGORITHM', 'HS256')

    try:
        settings["JWT_VALIDITY_PERIOD"] = int(os.environ.get('JWT_VALIDITY_PERIOD', 900))
    except ValueError:
        settings["JWT_VALIDITY_PERIOD"] = 900

    if settings["JWT_MASTER_KEY"] is not None:
        if len(settings["JWT_MASTER_KEY"]) < 10:
            structured_log(level='error', msg="Invalid value provided for JWT_MASTER_KEY. Must be at least 10 characters long")
            raise ConfigError()

        if settings["JWT_ALGORITHM"] not in ['HS256', 'HS384', 'HS512']:
            structured_log(level='error', msg="Invalid value provided for JWT_ALGORITHM. Defaulting to HS256")
            settings["JWT_ALGORITHM"] = 'HS256'

        settings["JWT"] = True

    return settings