Skip to content

Commit cb07340

Browse files
Rating system + optimizations (#73)
* Adding Rating System (#69) * Adding Rating System * Improvements to admin views * Bug fixes where cache invalidation was being done wrongly in admin/views.py Co-authored-by: Eshaan Bansal <eshaan7bansal@gmail.com> * Update README.md * Update README.md Co-authored-by: Kainaat Singh <kainaat.singh@gmail.com>
1 parent feb53ca commit cb07340

File tree

15 files changed

+428
-141
lines changed

15 files changed

+428
-141
lines changed

README.md

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Please see [INSTALLATION.md](INSTALLATION.md).
6868

6969
The main purpose of this project is to serve as a scoring engine and CTF manager. One that is packed with features, can handle enterprise/global level traffic on a scalable yet free heroku's dyno.
7070

71-
[CTFd](https://github.com/ctfd/ctfd) is one of the most popular CTF framework and we have used it for multiple engagements and will surely use it again. But at the same time, CTFd is heavy (~22.2 mb) (it gives poor performance even on a $49/mo heroku dyno) and not everybody has $$$ to spend on cloud, especially students (like us). So, that's where RTB-CTF-Framework (~100 KB) comes in.
71+
[CTFd](https://github.com/ctfd/ctfd) is one of the most popular CTF framework and we have used it for multiple engagements and will surely use it again. But at the same time, CTFd is heavy (~22.2 mb) (it gives poor performance even on a $49/mo heroku dyno) and not everyone has $$$ to spend on cloud, especially students (like us). So, that's where RTB-CTF-Framework (~100 KB) comes in.
7272

7373
## Contributing
7474

@@ -81,19 +81,12 @@ The main purpose of this project is to serve as a scoring engine and CTF manager
8181
</a>
8282
</p>
8383

84-
##### 👨 Project Owner
8584

86-
- Eshaan Bansal ([github](https://github.com/eshaan7), [linkedin](https://www.linkedin.com/in/eshaan7/))
85+
##### Join us on slack
8786

88-
##### 👬 Mentors
87+
- [#rtb-ctf-framework on slack](https://rtb-ctf-framework.slack.com)
8988

90-
- Sombuddha Chakravarty ([github](https://github.com/sammy1997), [linkedin](https://www.linkedin.com/in/sombuddha-chakravarty-9482b5131/))
91-
92-
##### Slack Channel for GSSoC 2020
93-
94-
- [#proj_root-the-box-ctf-framework](https://app.slack.com/client/TRN1H1V43/CUC71PDD2)
95-
96-
For further guidelines, Please refer to [CONTRIBUTING.md](CONTRIBUTING.md)
89+
Please refer to [CONTRIBUTING.md](CONTRIBUTING.md)
9790

9891

9992
## Live Demo

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# content of setup.cfg (deprecated atm)
22
[tool:pytest]
3-
flake8-ignore = W191
3+
flake8-ignore = W191, E231

src/FlaskRTBCTF/admin/views.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
from flask_admin.contrib.sqla import ModelView
88

99
from ..utils.cache import cache
10-
from ..utils.helpers import clear_points_cache
10+
from ..utils.helpers import clear_points_cache, clear_rating_cache
1111

1212

1313
class BaseModelView(ModelView):
14+
can_view_details = True
1415
export_types = ("csv", "json")
1516
can_export = True
1617
form_base_class = SecureForm
@@ -36,7 +37,6 @@ def _handle_view(self, name, **kwargs):
3637

3738

3839
class UserAdminView(BaseModelView):
39-
can_view_details = True
4040
column_exclude_list = ("password",)
4141
form_exclude_list = ("password",)
4242
column_searchable_list = ("username", "email")
@@ -54,9 +54,9 @@ def after_model_delete(model):
5454

5555

5656
class MachineAdminView(BaseModelView):
57-
can_view_details = True
5857
column_exclude_list = ("user_hash", "root_hash", "updated_on")
5958
column_searchable_list = ("name", "ip")
59+
column_filters = ("difficulty", "user_points", "root_points", "os")
6060

6161
@expose("/new/")
6262
def create_view(self):
@@ -69,9 +69,9 @@ def edit_view(self):
6969

7070

7171
class ChallengeAdminView(BaseModelView):
72-
can_view_details = True
7372
column_exclude_list = ("description", "flag", "url")
74-
column_searchable_list = ("title", "url")
73+
column_searchable_list = ("title", "url", "flag")
74+
column_filters = ("difficulty", "points", "category")
7575
form_choices = {
7676
"difficulty": [
7777
("easy", "Easy"),
@@ -93,36 +93,39 @@ def after_model_delete(model):
9393

9494

9595
class UserChallengeAdminView(BaseModelView):
96-
column_filters = ("completed",)
97-
column_list = ("user_id", "challenge_id", "completed")
96+
column_filters = ("completed", "user_id", "challenge_id")
97+
column_list = ("user_id", "challenge_id", "completed", "rating")
98+
form_choices = {
99+
"rating": [("1", "1"), ("2", "2"), ("3", "3"), ("4", "4"), ("5", "5")]
100+
}
98101

99102
@staticmethod
100103
def after_model_change(form, model, is_created):
101-
if form.completed != model.completed:
102-
clear_points_cache(userId=model.user_id, mode="c")
104+
clear_points_cache(userId=model.user_id, mode="c")
105+
clear_rating_cache(user_id=model.user_id, ch_id=model.challenge_id)
103106
return
104107

105108
@staticmethod
106109
def after_model_delete(model):
107110
clear_points_cache(userId=model.user_id, mode="c")
111+
clear_rating_cache(user_id=model.user_id, ch_id=model.challenge_id)
108112
return
109113

110114

111115
class UserMachineAdminView(BaseModelView):
112-
column_filters = ("owned_user", "owned_root")
113-
column_list = ("user_id", "machine_id", "owned_user", "owned_root")
116+
column_filters = ("user_id", "machine_id", "owned_user", "owned_root", "rating")
117+
column_list = ("user_id", "machine_id", "owned_user", "owned_root", "rating")
114118

115119
@staticmethod
116120
def after_model_change(form, model, is_created):
117-
if (form.owned_user != model.owned_user) or (
118-
form.owned_root != model.owned_root
119-
):
120-
clear_points_cache(userId=model.user_id, mode="m")
121+
clear_points_cache(userId=model.user_id, mode="m")
122+
clear_rating_cache(user_id=model.user_id, machine_id=model.machine_id)
121123
return
122124

123125
@staticmethod
124126
def after_model_delete(model):
125127
clear_points_cache(userId=model.user_id, mode="m")
128+
clear_rating_cache(user_id=model.user_id, machine_id=model.machine_id)
126129
return
127130

128131

src/FlaskRTBCTF/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
class Config:
10-
DEBUG = False # Turn DEBUG OFF before deployment
10+
DEBUG = True # Turn DEBUG OFF before deployment
1111
SECRET_KEY = handle_secret_key()
1212
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or "sqlite:///site.db"
1313
# For local use, one can simply use SQLlite with: 'sqlite:///site.db'

src/FlaskRTBCTF/ctf/forms.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from flask_wtf import FlaskForm
22
from wtforms import StringField, SubmitField, HiddenField, RadioField
3-
from wtforms.validators import DataRequired, Length, ValidationError, IPAddress
3+
from wtforms.validators import (
4+
DataRequired,
5+
Length,
6+
ValidationError,
7+
IPAddress,
8+
NumberRange,
9+
AnyOf,
10+
)
411
from wtforms.fields.html5 import IntegerField
12+
from wtforms.widgets import HiddenInput, SubmitInput
513
from .models import Machine, Challenge
614

715

@@ -92,3 +100,18 @@ def validate_flag(self, flag):
92100
raise ValidationError("No challenge with that ID exists")
93101
elif ch.flag != str(flag.data):
94102
raise ValidationError("Incorrect flag.")
103+
104+
105+
class RatingForm(FlaskForm):
106+
machine_challenge_id = IntegerField(
107+
label="ID", widget=HiddenInput(), validators=[DataRequired()]
108+
)
109+
rating_for = HiddenField(
110+
label="Machine or Challenge?",
111+
validators=[DataRequired(), AnyOf(("machine", "challenge"))],
112+
)
113+
rating_value = IntegerField(
114+
label="Rating",
115+
widget=SubmitInput(),
116+
validators=[DataRequired(), NumberRange(min=1, max=5)],
117+
)

src/FlaskRTBCTF/ctf/models.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from sqlalchemy.orm import joinedload
2+
from sqlalchemy.sql import func
23

34
from FlaskRTBCTF.utils.models import db, TimeMixin, ReprMixin
45
from FlaskRTBCTF.utils.cache import cache
@@ -21,6 +22,16 @@ class Machine(TimeMixin, ReprMixin, db.Model):
2122
ip = db.Column(db.String(64), nullable=False)
2223
difficulty = db.Column(db.String, nullable=False, default="Easy")
2324

25+
@staticmethod
26+
@cache.memoize(timeout=3600)
27+
def avg_rating(id):
28+
avg_rating = (
29+
UserMachine.query.with_entities(func.avg(UserMachine.rating))
30+
.filter(UserMachine.machine_id == id, UserMachine.rating != 0)
31+
.scalar()
32+
)
33+
return round(avg_rating, 1) if avg_rating else 0
34+
2435
@staticmethod
2536
@cache.cached(timeout=3600 * 3, key_prefix="machines")
2637
def get_all():
@@ -46,6 +57,7 @@ class UserMachine(TimeMixin, db.Model):
4657
)
4758
owned_user = db.Column(db.Boolean, nullable=False, default=False)
4859
owned_root = db.Column(db.Boolean, nullable=False, default=False)
60+
rating = db.Column(db.Integer, nullable=False, default=0)
4961

5062
@classmethod
5163
@cache.memoize(timeout=3600 * 3)
@@ -65,6 +77,17 @@ def completed_machines(cls, user_id):
6577
completed["root"] = [int(id[0]) for id in _ids2]
6678
return completed
6779

80+
@classmethod
81+
@cache.memoize(timeout=3600 * 3)
82+
def rated_machines(cls, user_id):
83+
_ids = (
84+
cls.query.with_entities(cls.machine_id)
85+
.filter(cls.user_id == user_id, cls.rating != 0)
86+
.all()
87+
)
88+
_ids = [int(id[0]) for id in _ids]
89+
return _ids
90+
6891

6992
# Tag Model
7093
class Tag(ReprMixin, db.Model):
@@ -107,6 +130,16 @@ class Challenge(TimeMixin, ReprMixin, db.Model):
107130
backref=db.backref("challenges", lazy="noload"),
108131
)
109132

133+
@staticmethod
134+
@cache.memoize(timeout=3600)
135+
def avg_rating(id):
136+
avg_rating = (
137+
UserChallenge.query.with_entities(func.avg(UserChallenge.rating))
138+
.filter(UserChallenge.challenge_id == id, UserChallenge.rating != 0)
139+
.scalar()
140+
)
141+
return round(avg_rating, 1) if avg_rating else 0
142+
110143

111144
# UserChallenge: N to N relationship
112145
class UserChallenge(TimeMixin, db.Model):
@@ -126,6 +159,7 @@ class UserChallenge(TimeMixin, db.Model):
126159
index=True,
127160
)
128161
completed = db.Column(db.Boolean, nullable=False, default=False)
162+
rating = db.Column(db.Integer, nullable=False, default=0)
129163

130164
@classmethod
131165
@cache.memoize(timeout=3600 * 3)
@@ -138,6 +172,17 @@ def completed_challenges(cls, user_id):
138172
_ids = [int(id[0]) for id in _ids]
139173
return _ids
140174

175+
@classmethod
176+
@cache.memoize(timeout=3600 * 3)
177+
def rated_challenges(cls, user_id):
178+
_ids = (
179+
cls.query.with_entities(cls.challenge_id)
180+
.filter(cls.user_id == user_id, cls.rating != 0)
181+
.all()
182+
)
183+
_ids = [int(id[0]) for id in _ids]
184+
return _ids
185+
141186

142187
# Category Model
143188
class Category(ReprMixin, db.Model):

0 commit comments

Comments
 (0)