Skip to content

Commit

Permalink
feature: Allow List for IPs (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
worldbeater authored Jun 4, 2024
1 parent 45cb024 commit 4dd1bcf
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 13 deletions.
3 changes: 1 addition & 2 deletions tests/config.defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,5 @@
"ИКБО-01-21": 30,
"ИКБО-02-21": 10
},
"CLEARABLE_DATABASE": false,
"ALLOW_IP": null
"CLEARABLE_DATABASE": false
}
29 changes: 29 additions & 0 deletions webapp/alembic/versions/20240605.00-27.create_allowed_ips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""create_allowed_ips
Revision ID: c9cc6ddcb46a
Revises: 0ed9e0bfabcb
Create Date: 2024-06-05 00:27:17.497763
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'c9cc6ddcb46a'
down_revision = '0ed9e0bfabcb'
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
'allowed_ips',
sa.Column("id", sa.Integer, primary_key=True, nullable=False),
sa.Column("ip", sa.String, nullable=False),
sa.Column("label", sa.String, nullable=True),
)


def downgrade():
op.drop_table('allowed_ips')
8 changes: 2 additions & 6 deletions webapp/config.defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
"HIDE_GROUPS": false,
"IMAP_LOGIN": null,
"IMAP_PASSWORD": null,
"FINAL_TASKS": {
"0": [0, 1, 2, 3, 4],
"1": [5, 6, 7, 8]
},
"FINAL_TASKS": null,
"ENABLE_LKS_OAUTH": false,
"LKS_OAUTH_CLIENT_ID": "kispython.ru",
"LKS_OAUTH_CLIENT_SECRET": "CHANGE_ME",
Expand All @@ -24,6 +21,5 @@
"PLACES_IN_RATING": 8,
"PLACES_IN_GROUP": 30,
"GROUPS": {},
"CLEARABLE_DATABASE": true,
"ALLOW_IP": null
"CLEARABLE_DATABASE": true
}
1 change: 0 additions & 1 deletion webapp/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def __init__(self, config: Config):
self.places_in_rating: int = config["PLACES_IN_RATING"]
self.places_in_group: int = config["PLACES_IN_GROUP"]
self.groups: dict = config["GROUPS"]
self.allow_ip: list[str] = config["ALLOW_IP"]
self.hide_groups: bool = config["HIDE_GROUPS"]

@property
Expand Down
7 changes: 7 additions & 0 deletions webapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,10 @@ class Mailer(Base):
__tablename__ = "mailers"
id = sa.Column("id", sa.Integer, primary_key=True, nullable=False)
domain = sa.Column("domain", sa.String, nullable=False)


class AllowedIp(Base):
__tablename__ = "allowed_ips"
id = sa.Column("id", sa.Integer, primary_key=True, nullable=False)
ip = sa.Column("ip", sa.String, nullable=False)
label = sa.Column("label", sa.String, nullable=True)
36 changes: 35 additions & 1 deletion webapp/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import uuid
from typing import Callable

from sqlalchemy import desc, func, null
from sqlalchemy import desc, func, literal, null
from sqlalchemy.orm import Session

from webapp.models import (
AllowedIp,
FinalSeed,
Group,
Mailer,
Expand Down Expand Up @@ -545,6 +546,38 @@ def create(self, domain: str) -> Mailer:
return mailer


class AllowedIpRepository:
def __init__(self, db: DbContextManager):
self.db = db

def is_allowed(self, ip: str) -> bool:
with self.db.create_session() as session:
total = session.query(AllowedIp).count()
if total == 0:
return True
return session.query(AllowedIp) \
.filter(literal(ip).contains(AllowedIp.ip)) \
.count()

def list_allowed(self):
with self.db.create_session() as session:
return session.query(AllowedIp) \
.order_by(AllowedIp.id) \
.all()

def allow(self, ip: str, label: str):
with self.db.create_session() as session:
aip = AllowedIp(ip=ip, label=label)
session.add(aip)
return aip

def disallow(self, id: int):
with self.db.create_session() as session:
session.query(AllowedIp) \
.filter_by(id=id) \
.delete()


class AppDatabase:
def __init__(self, get_connection: Callable[[], str]):
db = DbContextManager(get_connection)
Expand All @@ -557,3 +590,4 @@ def __init__(self, get_connection: Callable[[], str]):
self.seeds = FinalSeedRepository(db)
self.students = StudentRepository(db)
self.mailers = MailerRepository(db)
self.ips = AllowedIpRepository(db)
35 changes: 35 additions & 0 deletions webapp/templates/teacher/dashboard.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,40 @@
{% endif %}
</div>
{% endif %}

<div class="col-12 mb-3">
<b class="card-title d-block">Разрешённые IP-адреса</b>
<p>Оставьте список пустым, чтобы разрешить отправку решений с любого IP-адреса.</p>
<div class="row">
{% for ip in ips %}
<div class="col-7 mb-2">
<input class="form-control" type="text" value="{{ ip.ip }}" readonly>
</div>
<div class="col-3 mb-2">
<input class="form-control" type="text" value="{{ ip.label }}" readonly>
</div>
<div class="col-2 mb-2">
<form action="/teacher/ips/disallow/{{ ip.id }}" method="GET">
<button type="submit" class="btn btn-danger w-100">
Удалить
</button>
</form>
</div>
{% endfor %}
</div>
<form class="row" action="/teacher/ips/allow" method="GET">
<div class="col-7">
<input class="form-control" type="text" placeholder="IP-адрес" name="ip">
</div>
<div class="col-3">
<input class="form-control" type="text" placeholder="Пометка" name="label">
</div>
<div class="col-2">
<button type="submit" class="btn btn-primary w-100">
Добавить
</button>
</div>
</form>
</div>
</div>
{% endblock %}
3 changes: 1 addition & 2 deletions webapp/views/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,7 @@ def submit_task(student: Student | None, gid: int, vid: int, tid: int):
form = StudentMessageForm()
valid = form.validate_on_submit()
ip = get_real_ip(request)
allow_ip = config.config.allow_ip or ""
if valid and allowed and not status.disabled and allow_ip in ip:
if valid and allowed and not status.disabled and db.ips.is_allowed(ip):
sid = student.id if student else None
session_id = request.cookies.get("anonymous_identifier")
db.messages.submit_task(tid, vid, gid, form.code.data, ip, sid, session_id)
Expand Down
20 changes: 19 additions & 1 deletion webapp/views/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def dashboard(teacher: Student):
glist = db.groups.get_all()
vlist = db.variants.get_all()
tlist = db.tasks.get_all()
ips = db.ips.list_allowed()
return render_template(
"teacher/dashboard.jinja",
student=teacher,
Expand All @@ -81,7 +82,8 @@ def dashboard(teacher: Student):
groups=groups,
glist=glist,
vlist=vlist,
tlist=tlist
tlist=tlist,
ips=ips,
)


Expand Down Expand Up @@ -228,6 +230,22 @@ def reject(teacher: Student, group_id: int, message_id: int):
return redirect(f"/teacher/group/{group_id}")


@blueprint.route("/teacher/ips/allow", methods=["GET"])
@teacher_jwt_required(db.students)
def allow_ip(teacher: Student):
ip = request.args.get('ip')
label = request.args.get('label')
db.ips.allow(ip, label)
return redirect("/teacher")


@blueprint.route("/teacher/ips/disallow/<int:id>", methods=["GET"])
@teacher_jwt_required(db.students)
def disallow_ip(teacher: Student, id: int):
db.ips.disallow(id)
return redirect("/teacher")


@blueprint.errorhandler(Exception)
def handle_view_errors(e):
print(get_exception_info())
Expand Down

0 comments on commit 4dd1bcf

Please sign in to comment.