Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose new public api for rate limiting and user blocking #243

Merged
merged 48 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
907c997
Create should_block_request function and move ratelimiting and user b…
Oct 24, 2024
a0404d5
set_user add warning if middleware was already executed for rlm
Oct 24, 2024
b3a791a
Add some unit tests for should_block_requests
Oct 24, 2024
ddefcc6
Re-export should_block_request
Oct 24, 2024
f3e5b28
Add flask middleware and use it in sample app
Oct 24, 2024
518726c
Fix flask/block_request middleware and add logging in except
Oct 24, 2024
a54db77
First check for matched endpoints before rate limiting
Oct 24, 2024
e0242c4
Fix context unit tests
Oct 24, 2024
e174f4a
Create new Quart middleware
Oct 24, 2024
755cf3f
Add comments to flask middleware
Oct 24, 2024
1f08ea7
Linting
Oct 24, 2024
692402f
Update aikido_zen/middleware/__init__.py
bitterpanda63 Oct 24, 2024
c702915
Clean up sample app
Oct 24, 2024
692d26e
Linting
Oct 24, 2024
47f0b38
Move should_block_request to seperate file
Oct 28, 2024
aacd0b5
Move quart middleware to "ASGI Middleware"
Oct 28, 2024
a7e0cd5
Add middleware to starlette app
Oct 28, 2024
97b020e
Update flask middleware with rename and dynamic import to avoid crashes
Oct 28, 2024
06720d9
aikido_zen.middleware re-export all middleware
Oct 28, 2024
8723847
Also still export should_block_request
Oct 28, 2024
80948bb
Linting
Oct 28, 2024
ede921b
Add SetUserMiddleware
Oct 28, 2024
0d265a8
Update starlette docs
Oct 28, 2024
fd21bcd
Add instructions for ratelimiting to Quart docs
Oct 28, 2024
21b8353
Update flask docs to add ratelimiting checks
Oct 28, 2024
926f213
Quart-postgres-uvicorn sample app add middleware
Oct 28, 2024
e7b7b99
Flask sample app add middleware
Oct 28, 2024
070feb8
Update starlette postgres end2end tests to add user
Oct 28, 2024
bdafef7
Rename quart to ASGI in asgi.py
Oct 28, 2024
63c29bb
Update django docs and create django middleware
Oct 28, 2024
d54bd2b
Update flask mysql end2end test
Oct 28, 2024
7ce8284
Update quart postgres end2end tests
Oct 28, 2024
a14a567
Linting for django.py file
Oct 28, 2024
d9ae04a
Disable middleware for benchmark in starlette postgres uvicorn app
Oct 28, 2024
1cc8706
Update flask mysql sample app to not add middleware for benchmarks
Oct 28, 2024
e8c7ffe
Move DONT_ADD_MIDDLEWARE to .env.benhcmark for flask mysql
Oct 28, 2024
be8d177
Check if middleware exists before modifying it
Oct 28, 2024
e34a1f7
Don't assert stack for starlette_postgres_uvicorn
Oct 28, 2024
685b390
Merge branch 'main' into AIK-3829
bitterpanda63 Nov 14, 2024
f1ad7f6
Merge remote-tracking branch 'origin/main' into AIK-3829
Nov 28, 2024
21957cf
Add extra tests to users function
Nov 28, 2024
b0c3055
Add unit tests for should_block_request
Nov 28, 2024
e0cf8c7
Fix flask e2e test
Nov 28, 2024
5ebc11b
Add docs explaining how to set users
Nov 28, 2024
74510dd
Update docs/starlette.md
willem-delbare Dec 2, 2024
7267ddc
Update docs/starlette.md
willem-delbare Dec 2, 2024
456f845
Update docs
Dec 2, 2024
b80f806
Add authorization middleware comment in django
Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion aikido_zen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

from dotenv import load_dotenv

# Re-export set_current_user :
# Re-export functions :
from aikido_zen.context.users import set_user
from aikido_zen.middleware import should_block_request
bitterpanda63 marked this conversation as resolved.
Show resolved Hide resolved

# Import logger
from aikido_zen.helpers.logging import logger
Expand Down
3 changes: 3 additions & 0 deletions aikido_zen/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def __init__(self, context_obj=None, body=None, req=None, source=None):
self.route_params = extract_route_params(self.url)
self.subdomains = get_subdomains_from_url(self.url)

self.executed_middleware = False

def __reduce__(self):
return (
self.__class__,
Expand All @@ -81,6 +83,7 @@ def __reduce__(self):
"user": self.user,
"xml": self.xml,
"outgoing_req_redirects": self.outgoing_req_redirects,
"executed_middleware": self.executed_middleware,
"route_params": self.route_params,
},
None,
Expand Down
2 changes: 2 additions & 0 deletions aikido_zen/context/init_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def test_wsgi_context_1():
"parsed_userinput": {},
"xml": {},
"outgoing_req_redirects": [],
"executed_middleware": False,
"route_params": [],
}

Expand Down Expand Up @@ -95,6 +96,7 @@ def test_wsgi_context_2():
"parsed_userinput": {},
"xml": {},
"outgoing_req_redirects": [],
"executed_middleware": False,
"route_params": [],
}

Expand Down
6 changes: 6 additions & 0 deletions aikido_zen/context/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ def set_user(user):
if not context:
logger.debug("No context set, returning")
return
if context.executed_middleware is True:
bitterpanda63 marked this conversation as resolved.
Show resolved Hide resolved
# Middleware to rate-limit/check for users ran already. Could be misconfiguration.
logger.warning(
"set_user(...) must be called before the Zen middleware is executed."
)

validated_user["lastIpAddress"] = context.remote_address
context.user = validated_user

Expand Down
89 changes: 89 additions & 0 deletions aikido_zen/context/users_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
from lib2to3.fixes.fix_input import context

import pytest

from . import current_context, Context
from .users import validate_user, set_user
from .. import should_block_request


@pytest.fixture(autouse=True)
def run_around_tests():
yield
# Make sure to reset context and cache after every test so it does not
# interfere with other tests
current_context.set(None)


def set_context_and_lifecycle():
wsgi_request = {
"REQUEST_METHOD": "GET",
"HTTP_HEADER_1": "header 1 value",
"HTTP_HEADER_2": "Header 2 value",
"RANDOM_VALUE": "Random value",
"HTTP_COOKIE": "sessionId=abc123xyz456;",
"wsgi.url_scheme": "http",
"HTTP_HOST": "localhost:8080",
"PATH_INFO": "/hello",
"QUERY_STRING": "user=JohnDoe&age=30&age=35",
"CONTENT_TYPE": "application/json",
"REMOTE_ADDR": "198.51.100.23",
}
context = Context(
req=wsgi_request,
body=None,
source="flask",
)
context.set_as_current_context()
return context


def test_validate_user_valid_input():
Expand Down Expand Up @@ -67,3 +102,57 @@ def test_validate_user_invalid_user_type_dict_without_id(caplog):
def test_set_user_with_none(caplog):
result = set_user(None)
assert "expects a dict with 'id' and 'name' properties" in caplog.text


def test_set_valid_user():
context1 = set_context_and_lifecycle()
assert context1.user is None

user = {"id": 456, "name": "Bob"}
set_user(user)

assert context1.user == {
"id": "456",
"name": "Bob",
"lastIpAddress": "198.51.100.23",
}


def test_re_set_valid_user():
context1 = set_context_and_lifecycle()
assert context1.user is None

user = {"id": 456, "name": "Bob"}
set_user(user)

assert context1.user == {
"id": "456",
"name": "Bob",
"lastIpAddress": "198.51.100.23",
}

user = {"id": "1000", "name": "Alice"}
set_user(user)

assert context1.user == {
"id": "1000",
"name": "Alice",
"lastIpAddress": "198.51.100.23",
}


def test_after_middleware(caplog):
context1 = set_context_and_lifecycle()
assert context1.user is None

should_block_request()

user = {"id": 456, "name": "Bob"}
set_user(user)

assert "must be called before the Zen middleware is executed" in caplog.text
assert context1.user == {
"id": "456",
"name": "Bob",
"lastIpAddress": "198.51.100.23",
}
7 changes: 7 additions & 0 deletions aikido_zen/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Re-exports middleware"""

from .asgi import AikidoASGIMiddleware as AikidoQuartMiddleware
from .asgi import AikidoASGIMiddleware as AikidoStarletteMiddleware
from .flask import AikidoFlaskMiddleware
from .django import AikidoDjangoMiddleware
from .should_block_request import should_block_request
47 changes: 47 additions & 0 deletions aikido_zen/middleware/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Exports ratelimiting and user blocking middleware for ASGI"""

from aikido_zen.helpers.logging import logger
from .should_block_request import should_block_request


class AikidoASGIMiddleware:
"""Ratelimiting and user blocking middleware for ASGI"""

def __init__(self, app):
self.app = app

async def __call__(self, scope, receive, send):
result = should_block_request()
if result["block"] is not True:
return await self.app(scope, receive, send)

if result["type"] == "ratelimited":
message = "You are rate limited by Zen."
if result["trigger"] == "ip" and result["ip"]:
message += " (Your IP: " + result["ip"] + ")"
return await send_status_code_and_text(send, (message, 429))
elif result["type"] == "blocked":
return await send_status_code_and_text(
send, ("You are blocked by Zen.", 403)
)

logger.debug("Unknown type for blocking request: %s", result["type"])
return await self.app(scope, receive, send)


async def send_status_code_and_text(send, pre_response):
"""Sends a status code and text"""
await send(
{
"type": "http.response.start",
"status": pre_response[1],
"headers": [(b"content-type", b"text/plain")],
}
)
await send(
{
"type": "http.response.body",
"body": pre_response[0].encode("utf-8"),
"more_body": False,
}
)
37 changes: 37 additions & 0 deletions aikido_zen/middleware/django.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Exports AikidoDjangoMiddleware"""

from aikido_zen.helpers.logging import logger
from .should_block_request import should_block_request


class AikidoDjangoMiddleware:
"""Middleware for rate-limiting and user blocking for django"""

def __init__(self, get_response):
logger.critical("Django middleware ised")
self.get_response = get_response
try:
from django.http import HttpResponse

self.HttpResponse = HttpResponse
except ImportError:
logger.warning(
"django.http import not working, aikido rate-limiting middleware not running."
)

def __call__(self, request):
result = should_block_request()
if result["block"] is not True or self.HttpResponse is None:
return self.get_response(request)

if result["type"] == "ratelimited":
message = "You are rate limited by Zen."
if result["trigger"] == "ip" and result["ip"]:
message += " (Your IP: " + result["ip"] + ")"
return self.HttpResponse(message, content_type="text/plain", status=429)
elif result["type"] == "blocked":
return self.HttpResponse(
"You are blocked by Zen.", content_type="text/plain", status=403
)
logger.debug("Unknown type for blocking request: %s", result["type"])
return self.get_response(request)
38 changes: 38 additions & 0 deletions aikido_zen/middleware/flask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Exports ratelimiting and user blocking middleware for Flask"""

from aikido_zen.helpers.logging import logger
from .should_block_request import should_block_request


class AikidoFlaskMiddleware:
"""Ratelimiting and user blocking middleware for Flask"""

def __init__(self, app):
self.app = app
try:
from werkzeug.wrappers import Response

self.Response = Response
except ImportError:
logger.warning(
"Something went wrong whilst importing werkzeug.wrappers, middleware does not work"
)

def __call__(self, environ, start_response):
result = should_block_request()
if result["block"] is not True or self.Response is None:
return self.app(environ, start_response)

if result["type"] == "ratelimited":
message = "You are rate limited by Zen."
if result["trigger"] == "ip" and result["ip"]:
message += " (Your IP: " + result["ip"] + ")"
res = self.Response(message, mimetype="text/plain", status=429)
return res(environ, start_response)
elif result["type"] == "blocked":
res = self.Response(
"You are blocked by Zen.", mimetype="text/plain", status=403
)
return res(environ, start_response)
logger.debug("Unknown type for blocking request: %s", result["type"])
return self.app(environ, start_response)
Loading