Preparing for actual products...

IP Rate Limit "firewall"
This commit is contained in:
Kumi 2020-06-01 18:29:22 +02:00
parent 320e031786
commit d3348cc4de
20 changed files with 387 additions and 48 deletions

View file

@ -1,23 +1,31 @@
class BaseProductHandler: class BaseProductType:
def __init__(self, product_id): def __init__(self, product_type_id):
self.product_id = product_id self.id = product_type_id
@property @property
def name(self):
return "Base Product Type"
@property
def id(self):
return ""
def is_creatable(self): def is_creatable(self):
'''Returns True if this product can be created right now, or False if it cannot. Should return True if the module is correctly configured, the product is in stock (if physical), etc.''' '''Returns True if a product can be created right now using this type, or False if it cannot. Should return True if the module is correctly configured, etc.'''
return False return False
@property def is_orderable(self, product):
def setup_view(self): '''Returns True if this product can be ordered right now. Should return True if the product is in stock, etc.'''
'''Returns a view in which the product can be configured, or None if no configuration is possible/necessary.'''
return None
class FallbackProductHandler(BaseProductHandler):
pass
class CoreProductHandler(BaseProductHandler):
@property
def is_creatable(self):
return True return True
ProductHandler = CoreProductHandler def orderable_by_user(self, product):
'''Returns a tuple of boolean values (user is allowed to order this, override by administrator allowed). First boolean is True if the user can order this product. If the first boolean is False, the second boolean indicates if an administrator can provide the product manually nonetheless.'''
return self.is_orderable()
def product_setup_view(self):
'''Returns a view in which a new product can be configured, or None if no configuration is possible/necessary.'''
return None
def service_setup_view(self, product):
'''Returns a view in which a new service can be configured, or None if no configuration is possible/necessary.'''
return True

View file

@ -1,6 +1,6 @@
from core.helpers.mail import get_template, simple_send_mail from core.helpers.mail import get_template, simple_send_mail
from core.helpers.urls import relative_to_absolute as reltoabs from core.helpers.urls import relative_to_absolute as reltoabs
from core.models.auth import LoginLog, PWResetToken from core.models.auth import LoginLog, PWResetToken, IPLimit
from core.helpers.request import get_client_ip from core.helpers.request import get_client_ip
from django.urls import reverse from django.urls import reverse
@ -28,7 +28,7 @@ def clear_login_log(maxage=int(getValue("core.auth.ratelimit.period", 600))):
def clear_ratelimits(maxage=int(getValue("core.auth.ratelimit.block", 3600))): def clear_ratelimits(maxage=int(getValue("core.auth.ratelimit.block", 3600))):
timestamp = timezone.now() - timezone.timedelta(seconds=maxage) timestamp = timezone.now() - timezone.timedelta(seconds=maxage)
LoginLog.objects.filter(timestamp__lt=timestamp).delete() IPLimit.objects.filter(end__lt=timestamp).delete()
def request_password(user): def request_password(user):
token = PWResetToken.objects.create(user=user) token = PWResetToken.objects.create(user=user)

View file

@ -1,9 +1,11 @@
from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.mixins import AccessMixin
from django.contrib.messages import error from django.contrib.messages import error
from django.views.decorators.cache import never_cache
from core.models.profiles import AdminProfile from core.models.profiles import AdminProfile
class AdminMixin(AccessMixin): class AdminMixin(AccessMixin):
@never_cache
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
self.permission_denied_message = "You must be logged in to access this area." self.permission_denied_message = "You must be logged in to access this area."

View file

@ -3,6 +3,8 @@ from django.contrib.auth import get_user_model
from dbsettings.functions import getValue from dbsettings.functions import getValue
from core.fields.base import LongCharField
from uuid import uuid4 from uuid import uuid4
from datetime import timedelta from datetime import timedelta
@ -24,9 +26,5 @@ class LoginLog(Model):
class IPLimit(Model): class IPLimit(Model):
ip = GenericIPAddressField() ip = GenericIPAddressField()
start = DateTimeField(auto_now_add=True) end = DateTimeField()
reason = LongCharField(null=True, blank=True)
@property
def end(self):
delta = timedelta(seconds=int(getValue("core.auth.ratelimit.block", 3600)))
return self.start + delta

View file

@ -15,17 +15,17 @@ class ProductGroup(Model):
class Product(Model): class Product(Model):
name = LongCharField() name = LongCharField()
description = TextField(null=True, blank=True) description = TextField(null=True, blank=True)
handler_module = LongCharField(null=True, blank=True) product_type = LongCharField(null=True, blank=True)
product_groups = ManyToManyField(ProductGroup) product_groups = ManyToManyField(ProductGroup)
@property @property
def handler(self): def handler(self):
if self.handler_module: if self.product_type:
try: try:
handler_module = import_module(self.handler_module) product_type = import_module(self.product_type)
return handler_module.ProductRouter(self.id) return product_type.ProductRouter(self.id)
except Exception as e: except Exception as e:
logger.error(f"Could not load product handler {self.handler_module} for product {self.id}: {e}") logger.error(f"Could not load product type {self.product_type} for product {self.id}: {e}")
return None return None
class ProductPlan(Model): class ProductPlan(Model):

View file

@ -2,7 +2,7 @@ import importlib
from django.conf import settings from django.conf import settings
from core.classes.navigation import NavItem, NavSection, Navigation from core.classes.navigation import Navigation
navigations = { navigations = {
"backend_main": Navigation(), "backend_main": Navigation(),

15
core/modules/products.py Normal file
View file

@ -0,0 +1,15 @@
import importlib
from django.conf import settings
producttypes = {}
for module in ["core"] + settings.EXPEPHALON_MODULES:
try:
mop = importlib.import_module(f"{module}.products")
for name, ptype in mop.PRODUCTTYPES:
if name in producttypes.keys:
raise ValueError(f"Error in {module}: Product type of name {name} already exists!")
producttypes[name] = ptype
except (AttributeError, ModuleNotFoundError):
continue

View file

@ -25,6 +25,10 @@ from core.views import (
BrandDeleteView, BrandDeleteView,
BrandEditView, BrandEditView,
BrandListView, BrandListView,
RateLimitCreateView,
RateLimitDeleteView,
RateLimitEditView,
RateLimitListView,
) )
URLPATTERNS = [] URLPATTERNS = []
@ -65,6 +69,13 @@ URLPATTERNS.append(path("admin/brands/<pk>/delete/", BrandDeleteView.as_view(),
URLPATTERNS.append(path("admin/brands/<pk>/edit/", BrandEditView.as_view(), name="brands_edit")) URLPATTERNS.append(path("admin/brands/<pk>/edit/", BrandEditView.as_view(), name="brands_edit"))
URLPATTERNS.append(path("admin/brands/create/", BrandCreateView.as_view(), name="brands_create")) URLPATTERNS.append(path("admin/brands/create/", BrandCreateView.as_view(), name="brands_create"))
# Rate Limit Administration URLs
URLPATTERNS.append(path('admin/firewall/', RateLimitListView.as_view(), name="ratelimits"))
URLPATTERNS.append(path("admin/firewall/<pk>/delete/", RateLimitDeleteView.as_view(), name="ratelimits_delete"))
URLPATTERNS.append(path("admin/firewall/<pk>/edit/", RateLimitEditView.as_view(), name="ratelimits_edit"))
URLPATTERNS.append(path("admin/firewall/create/", RateLimitCreateView.as_view(), name="ratelimits_create"))
# External Module URLs # External Module URLs
for module in settings.EXPEPHALON_MODULES: for module in settings.EXPEPHALON_MODULES:

View file

@ -1,4 +1,5 @@
from core.modules.navigation import navigations, NavSection, NavItem from core.modules.navigation import navigations
from core.classes.navigation import NavSection, NavItem
from django.conf import settings from django.conf import settings
@ -90,6 +91,7 @@ administration_section = NavSection("Administration", "")
user_administration_item = NavItem("Administrator Users", "fa-users-cog", "admins") user_administration_item = NavItem("Administrator Users", "fa-users-cog", "admins")
brand_administration_item = NavItem("Brands", "fa-code-branch", "brands") brand_administration_item = NavItem("Brands", "fa-code-branch", "brands")
ratelimit_administration_item = NavItem("Firewall", "fa-shield-alt", "ratelimits")
sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni") sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni")
otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "backendni") otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "backendni")
backup_administration_item = NavItem("Backups", "fa-shield-alt", "backendni") backup_administration_item = NavItem("Backups", "fa-shield-alt", "backendni")
@ -100,6 +102,7 @@ dbsettings_item = NavItem("Database Settings", "fa-database", "dbsettings")
administration_section.add_item(user_administration_item) administration_section.add_item(user_administration_item)
administration_section.add_item(brand_administration_item) administration_section.add_item(brand_administration_item)
administration_section.add_item(ratelimit_administration_item)
administration_section.add_item(sms_administration_item) administration_section.add_item(sms_administration_item)
administration_section.add_item(otp_administration_item) administration_section.add_item(otp_administration_item)
administration_section.add_item(backup_administration_item) administration_section.add_item(backup_administration_item)

17
core/products.py Normal file
View file

@ -0,0 +1,17 @@
from core.classes.products import BaseProductType
PRODUCTTYPES = {}
class CoreProductType(BaseProductType):
@property
def name(self):
return "Basic Product"
@property
def id(self):
return "core.products.core"
def is_creatable(self):
return True
PRODUCTTYPES[CoreProductType().id()] = CoreProductType

View file

@ -7,6 +7,7 @@ from core.views.auth import *
from core.views.profiles import * from core.views.profiles import *
from core.views.generic import * from core.views.generic import *
from core.views.brands import * from core.views.brands import *
from core.views.firewall import *
from core.mixins.auth import AdminMixin from core.mixins.auth import AdminMixin
# Create your views here. # Create your views here.

View file

@ -5,6 +5,8 @@ from django.shortcuts import redirect
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.contrib import messages from django.contrib import messages
from django.utils import timezone from django.utils import timezone
from django.db.models import Max
from django.views.decorators.cache import never_cache
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm
from core.models.auth import LoginSession, PWResetToken, IPLimit, LoginLog from core.models.auth import LoginSession, PWResetToken, IPLimit, LoginLog
@ -18,25 +20,25 @@ from dbsettings.functions import getValue
class RateLimitedView(TemplateView): class RateLimitedView(TemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/ratelimit.html" template_name = f"{settings.EXPEPHALON_BACKEND}/auth/ratelimit.html"
@never_cache
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
for iplimit in list(IPLimit.objects.filter(ip=get_client_ip(request))): try:
if iplimit.end >= timezone.now(): limit = IPLimit.objects.filter(ip=get_client_ip(request), end__gte=timezone.now()).first()
messages.error(request, f"Sorry, there have been to many failed login attempts from your IP. Please try again after {str(iplimit.end)}, or contact support if you need help getting into your account.") messages.error(request, f"Sorry, your IP has been blocked, so you cannot login at the moment.{f' Reason: {limit.reason}' if limit.reason else ''} Please try again after {str(iplimit.end)}, or contact support if you need help getting into your account.")
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
except:
return redirect("login") return redirect("login")
class AuthView(FormView): class AuthView(FormView):
@never_cache
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
limits = list(IPLimit.objects.filter(ip=get_client_ip(request))) if IPLimit.objects.filter(ip=get_client_ip(request), end__gte=timezone.now()):
for limit in limits:
if limit.end > timezone.now():
return redirect("ratelimited") return redirect("ratelimited")
period = timezone.now() - timezone.timedelta(seconds=int(getValue("core.auth.ratelimit.period", 600))) period = timezone.now() - timezone.timedelta(seconds=int(getValue("core.auth.ratelimit.period", 600)))
failures = LoginLog.objects.filter(ip=get_client_ip(request), success=False, timestamp__gte=period) failures = LoginLog.objects.filter(ip=get_client_ip(request), success=False, timestamp__gte=period)
if len(failures) >= int(getValue("core.auth.ratelimit.attempts", 5)): if len(failures) >= int(getValue("core.auth.ratelimit.attempts", 5)):
IPLimit.objects.create(ip=get_client_ip(request)) IPLimit.objects.create(ip=get_client_ip(request), end=timezone.now() + timezone.timedelta(seconds=getValue("core.auth.ratelimit.block", 3600)), reason="Too many failed login attempts.")
return redirect("ratelimited") return redirect("ratelimited")
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)

View file

@ -16,8 +16,8 @@ class BrandListView(BackendListView):
class BrandEditView(BackendUpdateView): class BrandEditView(BackendUpdateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/brands/update.html" template_name = f"{settings.EXPEPHALON_BACKEND}/brands/update.html"
model = Brand model = Brand
success_url = reverse_lazy("dbsettings") success_url = reverse_lazy("brands")
fields = ["key", "value"] fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -27,7 +27,7 @@ class BrandEditView(BackendUpdateView):
class BrandDeleteView(BackendDeleteView): class BrandDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/brands/delete.html" template_name = f"{settings.EXPEPHALON_BACKEND}/brands/delete.html"
model = Brand model = Brand
success_url = reverse_lazy("dbsettings") success_url = reverse_lazy("brands")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -35,10 +35,10 @@ class BrandDeleteView(BackendDeleteView):
return context return context
class BrandCreateView(BackendCreateView): class BrandCreateView(BackendCreateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/dbsettings/create.html" template_name = f"{settings.EXPEPHALON_BACKEND}/brands/create.html"
model = Brand model = Brand
success_url = reverse_lazy("dbsettings") success_url = reverse_lazy("brands")
fields = ["key", "value"] fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)

46
core/views/firewall.py Normal file
View file

@ -0,0 +1,46 @@
from django.conf import settings
from django.urls import reverse_lazy
from core.models.auth import IPLimit
from core.views.generic import BackendListView, BackendUpdateView, BackendDeleteView, BackendCreateView
class RateLimitListView(BackendListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/index.html"
model = IPLimit
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Firewall Settings"
return context
class RateLimitEditView(BackendUpdateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/update.html"
model = IPLimit
success_url = reverse_lazy("ratelimits")
fields = ["ip", "end", "reason"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Rule"
return context
class RateLimitDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/delete.html"
model = IPLimit
success_url = reverse_lazy("ratelimits")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Rule"
return context
class RateLimitCreateView(BackendCreateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/create.html"
model = IPLimit
success_url = reverse_lazy("ratelimits")
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Create Rule"
return context

1
start_celery.sh Normal file
View file

@ -0,0 +1 @@
celery -A expephalon worker --loglevel=info

1
start_celery_beat.sh Normal file
View file

@ -0,0 +1 @@
celery -A expephalon beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Firewall - Create Rule
<div class="page-title-subheading">Create a new firewall rule
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Rule
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Create Rule
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-database">
</i>
</div>
<div>Firewall Rules - Delete Rule
<div class="page-title-subheading">Delete a rule from the system
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Rule" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Rule
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Deleting rule for {{ object.ip }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
{% csrf_token %}
Are you sure you wish to delete the rule for {{ object.ip }}?
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,63 @@
{% extends "backend/base.html" %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-shield-alt">
</i>
</div>
<div>Firewall Rules
<div class="page-title-subheading">Create, edit and delete rules
</div>
</div>
</div>
<div class="page-title-actions">
<a href="{% url "ratelimits_create" %}" type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Rule
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Active Rules
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<div class="card mb-3 widget-chart widget-chart2 text-left w-100">
<table class="mb-0 table table-hover">
<thead>
<tr>
<th>IP</th>
<th>End of block</th>
<th>Reason</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{% for rule in object_list %}
<tr>
<td>{{ rule.ip }}</td>
<td>{{ rule.end }}</td>
<td>{{ rule.reason }}</td>
<td><a href="{% url "ratelimits_edit" rule.id %}"><i class="fas fa-edit" title="Edit Rule"></i></a> <a href="{% url "ratelimits_delete" rule.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Rule"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Brands - Edit Brand
<div class="page-title-subheading">Edit brand properties
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Brand
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Editing {{ object.name }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}