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

feature: Allow List for IPs #52

Merged
merged 1 commit into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 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
Loading