Skip to content

Commit

Permalink
Separate filter from rules
Browse files Browse the repository at this point in the history
The IPFilter class now contains logic for interacting with Flask. The
Whitelist class contains logic for permitting hosts and networks and
checking whether an IP address should be allowed. This separation will
allow other types of rules to be created in the future.

This is actually a complete rewrite using a TDD approach.
  • Loading branch information
douganger committed Mar 19, 2019
1 parent 3f2aeee commit 30569b0
Show file tree
Hide file tree
Showing 15 changed files with 361 additions and 105 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ clean:
rm -rf docs/_build/*
rm -rf .env

distribution:
python setup.py sdist bdist_wheel

documentation:
python -m sphinx -M html "docs" "docs/_build"

Expand All @@ -22,5 +25,5 @@ pylint:
pylint test/*.py

unittest:
py.test test --cov=flask_ipfilter
py.test test --disable-pytest-warnings --cov=flask_ipfilter

12 changes: 6 additions & 6 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# Flask-IPFilter

[![Build Status](https://travis-ci.org/douganger/flask-ipfilter.svg?branch=master)](https://travis-ci.org/douganger/flask-ipfilter)
[![Coverage
Status](https://coveralls.io/repos/github/douganger/flask-ipfilter/badge.svg?branch=master)](https://coveralls.io/github/douganger/flask-ipfilter?branch=master)
[![Documentation
Status](https://readthedocs.org/projects/flask-ipfilter/badge/?version=latest)](https://flask-ipfilter.readthedocs.io/en/latest/?badge=latest)
[![Codacy
Badge](https://api.codacy.com/project/badge/Grade/fbff22f2f804412790ee10601e8b6949)](https://www.codacy.com/app/douganger/flask-ipfilter)
[![Coverage Status](https://coveralls.io/repos/github/douganger/flask-ipfilter/badge.png?branch=master)](https://coveralls.io/github/douganger/flask-ipfilter?branch=master)
[![Documentation Status](https://readthedocs.org/projects/flask-ipfilter/badge/?version=latest)](https://flask-ipfilter.readthedocs.io/en/latest/?badge=latest)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/fbff22f2f804412790ee10601e8b6949)](https://www.codacy.com/app/douganger/flask-ipfilter)

Flask-IPFilter is a simple Flask extension to limit access to a site to certain
IP addresses. The current implementation is a minimal proof of concept with some
Expand All @@ -27,12 +24,12 @@ in your application.

```python
from flask import Flask
from flask_ipfilter import Whitelist
from flask_ipfilter import IPFilter, Whitelist

app = Flask(__name__)
ip_filter = Whitelist(app)
ip_filter = IPFilter(app, ruleset=Whitelist())

ip_filter.whitelist("127.0.0.1")
ip_filter.ruleset.permit("127.0.0.1")

@app.route("/")
def route_test():
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
copyright = '2019, Douglas Anger'
author = 'Douglas Anger'
version = '0.0'
release = '0.0.3'
release = '0.0.4'

extensions = [
'sphinx.ext.autodoc',
Expand Down
10 changes: 5 additions & 5 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Flask-IPFilter Documentation
============================
Flask-IPFilter
==============

Flask-IPFilter is a simple Flask extension to limit access to a site to certain
IP addresses. The current implementation is a minimal proof of concept with some
Expand All @@ -23,12 +23,12 @@ in your application.
.. code-block:: python
from flask import Flask
from flask_ipfilter import Whitelist
from flask_ipfilter import IPFilter, Whitelist
app = Flask(__name__)
ip_filter = Whitelist(app)
ip_filter = IPFilter(app, ruleset=Whitelist())
ip_filter.whitelist("127.0.0.1")
ip_filter.ruleset.permit("127.0.0.1")
@app.route("/")
def route_test():
Expand Down
7 changes: 7 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ Modules

.. automodule:: flask_ipfilter

ipfilter
--------

.. automodule:: flask_ipfilter.ipfilter
:members:
:show-inheritance:

whitelist
---------

Expand Down
6 changes: 3 additions & 3 deletions example_app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from flask import Flask
from flask_ipfilter import Whitelist
from flask_ipfilter import IPFilter, Whitelist

app = Flask(__name__)
ip_filter = Whitelist(app)
ip_filter = IPFilter(app, ruleset=Whitelist())

ip_filter.whitelist("127.0.0.1")
ip_filter.ruleset.permit("127.0.0.1")

@app.route("/")
def route_test():
Expand Down
1 change: 1 addition & 0 deletions flask_ipfilter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
load balancer.
"""

from flask_ipfilter.ipfilter import IPFilter
from flask_ipfilter.whitelist import Whitelist
55 changes: 55 additions & 0 deletions flask_ipfilter/ipfilter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Restrict access to Flask applications by requestor IP address."""

from flask import request
from werkzeug.exceptions import Forbidden


class IPFilter:
"""An IP address filter for Flask applications."""

def __init__(self, flask_app=None, ruleset=None):
"""
Initialize the IP filter.
:parameter app: The Flask application may or may not exist when
__init__ is called. If app is passed, the filter will
be applied to the Flask application immediately. The
filter can be applied to a Flask application later by
calling init_app with the app object as the parameter.
"""
self.app = flask_app
self.ruleset = ruleset
if flask_app:
self.init_app(flask_app)

def init_app(self, flask_app):
"""
Connect the whitelist filter to a Flask application.
This is called in the :func:`__init__` function if the app
parameter is passed at that time, but can be called later in order to
allow a whitelist to be set up before the Flask app is created.
:parameter app: Required reference to the Flask application to which we
will apply the filter.
"""
flask_app.before_request(self)

def __call__(self):
"""
Validate an IP before processing a request.
When :func:`init_app` is called, it will be registered to run before
each request. This depends on the Flask request object.
:returns: Nothing, but raises an exception for Flask to catch if the
ruleset determines that the IP address should be blocked.
"""
x_forwarded_for = request.headers.get('X-Forwarded-For')
if x_forwarded_for:
ip_address = x_forwarded_for.split(',')[-1].strip()
else:
ip_address = request.remote_addr

if not self.ruleset.evaluate(ip_address):
raise Forbidden()
90 changes: 30 additions & 60 deletions flask_ipfilter/whitelist.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,54 @@
"""Code for filtering Flask requests based on an IP address whitelist."""
"""IPFilter Whitelist."""

import ipaddress
from flask import request
from werkzeug.exceptions import Forbidden


class Whitelist:
"""An IP address whitelist filter for Flask."""
"""
A ruleset that denies requests by default.
def __init__(self, app=None):
"""Initialize the whitelist filter."""
self.addresses = set()
self.networks = set()
if app:
self.init_app(app)
Hosts and networks will be allowed only if they have been explicitly
permitted with the :func:`permit` function.
"""

def init_app(self, app):
"""
Connect the whitelist filter to a Flask application.
This is called in the :func:`__init__` function if the app
parameter is passed at that time, but can be called later in order to
allow a whitelist to be set up before the Flask app is created.
"""
self.app = app
self.app.before_request(self.validate_ip)

def whitelist(self, address):
"""
Add a new host or network to the whitelist.
:parameter address: The IP address to allow. The address parameter
accepts anything that the :mod:`ipaddress` module
can take as a parameter to :class:`ip_address` or
:class:`ip_network`.
"""
try:
host = ipaddress.ip_address(address)
self.addresses.add(host)
except ValueError:
net = ipaddress.ip_network(address)
self.networks.add(net)
def __init__(self):
"""Initialize the ruleset."""
self.permitted_hosts = set()
self.permitted_networks = set()

def check(self, address):
def evaluate(self, ip_address):
"""
Check an IP address against the whitelist.
Determine whether an IP address is allowed.
:parameter address: The IP address to check.
:parameter ip_address: The IP address to check. This must be something
that :mod:`ipaddress` can convert into an
:class:`ip_address`.
:returns: True if access should be allowed and False otherwise.
"""
ip_addr = ipaddress.ip_address(address)
ip_addr = ipaddress.ip_address(ip_address)

if ip_addr in self.addresses:
if ip_addr in self.permitted_hosts:
return True

for net in self.networks:
for net in self.permitted_networks:
if ip_addr in net:
return True

return False

def validate_ip(self):
def permit(self, ip_address):
"""
Validate an IP before processing a request.
When :func:`init_app` is called, it will be registered to run before
each request.
This depends on the Flask request object.
Add a new host or network to the whitelist.
:returns: A denied message with HTTP status 403 if the IP address
associated with the request object is not allowed.
Otherwise, nothing.
:parameter ip_address: The IP address to allow. The address parameter
accepts anything that the :mod:`ipaddress` module
can take as a parameter to :class:`ip_address` or
:class:`ip_network`.
"""
x_forwarded_for = request.headers.get('X-Forwarded-For')
if x_forwarded_for:
ip_addr = x_forwarded_for.split(',')[-1].strip()
else:
ip_addr = request.remote_addr

if not self.check(ip_addr):
raise Forbidden()
try:
host = ipaddress.ip_address(ip_address)
self.permitted_hosts.add(host)
except ValueError:
net = ipaddress.ip_network(ip_address)
self.permitted_networks.add(net)
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ more-itertools==6.0.0 ; python_version > '2.7'
pluggy==0.9.0
py==1.8.0
pytest-cov==2.6.1
pytest==4.3.0
pytest==4.3.1
requests==2.21.0
six==1.12.0
urllib3==1.24.1
Expand All @@ -29,7 +29,7 @@ chardet==3.0.4
docutils==0.14
idna==2.8
imagesize==1.1.0
isort==4.3.14
isort==4.3.15
jinja2==2.10
lazy-object-proxy==1.3.1
markupsafe==1.1.1
Expand Down
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
to certain hosts and networks.
"""

from collections import OrderedDict
from setuptools import setup

setup(
name="Flask-IPFilter",
version="0.0.3",
version="0.0.4",
description="Limit access to a Flask website by IP address",
long_description=__doc__,
url="https://github.com/douganger/flask-ipfilter",
project_urls=OrderedDict((
('Documentation', 'https://flask-ipfilter.readthedocs.io/en/stable/'),
('Code', 'https://github.com/douganger/flask-ipfilter'),
('Issue tracker', 'https://github.com/douganger/flask-ipfilter/issues'))),
author="Douglas Anger",
author_email="douganger@gmail.com",
license="MIT",
Expand Down

0 comments on commit 30569b0

Please sign in to comment.