Files
CTFd/CTFd/models/__init__.py
2019-04-17 01:36:30 -04:00

761 lines
24 KiB
Python

from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from sqlalchemy.sql.expression import union_all
from sqlalchemy.orm import validates, column_property
from sqlalchemy.ext.hybrid import hybrid_property
from CTFd.utils.crypto import hash_password
from CTFd.cache import cache
import datetime
import six
db = SQLAlchemy()
ma = Marshmallow()
def get_class_by_tablename(tablename):
"""Return class reference mapped to table.
https://stackoverflow.com/a/23754464
:param tablename: String with name of table.
:return: Class reference or None.
"""
for c in db.Model._decl_class_registry.values():
if hasattr(c, '__tablename__') and c.__tablename__ == tablename:
return c
return None
class Notifications(db.Model):
__tablename__ = 'notifications'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Text)
content = db.Column(db.Text)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
user = db.relationship('Users', foreign_keys="Notifications.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Notifications.team_id", lazy='select')
def __init__(self, *args, **kwargs):
super(Notifications, self).__init__(**kwargs)
class Pages(db.Model):
__tablename__ = 'pages'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80))
route = db.Column(db.String(128), unique=True)
content = db.Column(db.Text)
draft = db.Column(db.Boolean)
hidden = db.Column(db.Boolean)
auth_required = db.Column(db.Boolean)
# TODO: Use hidden attribute
files = db.relationship("PageFiles", backref="page")
def __init__(self, *args, **kwargs):
super(Pages, self).__init__(**kwargs)
def __repr__(self):
return "<Pages {0}>".format(self.route)
class Challenges(db.Model):
__tablename__ = 'challenges'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.Text)
max_attempts = db.Column(db.Integer, default=0)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
type = db.Column(db.String(80))
state = db.Column(db.String(80), nullable=False, default='visible')
requirements = db.Column(db.JSON)
files = db.relationship("ChallengeFiles", backref="challenge")
tags = db.relationship("Tags", backref="challenge")
hints = db.relationship("Hints", backref="challenge")
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
def __init__(self, *args, **kwargs):
super(Challenges, self).__init__(**kwargs)
def __repr__(self):
return '<Challenge %r>' % self.name
class Hints(db.Model):
__tablename__ = 'hints'
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(80), default='standard')
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
content = db.Column(db.Text)
cost = db.Column(db.Integer, default=0)
requirements = db.Column(db.JSON)
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
@property
def name(self):
return "Hint {id}".format(id=self.id)
@property
def category(self):
return self.__tablename__
@property
def description(self):
return "Hint for {name}".format(name=self.challenge.name)
def __init__(self, *args, **kwargs):
super(Hints, self).__init__(**kwargs)
def __repr__(self):
return '<Hint %r>' % self.content
class Awards(db.Model):
__tablename__ = 'awards'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
type = db.Column(db.String(80), default='standard')
name = db.Column(db.String(80))
description = db.Column(db.Text)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
value = db.Column(db.Integer)
category = db.Column(db.String(80))
icon = db.Column(db.Text)
requirements = db.Column(db.JSON)
user = db.relationship('Users', foreign_keys="Awards.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Awards.team_id", lazy='select')
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.user_id
def __init__(self, *args, **kwargs):
super(Awards, self).__init__(**kwargs)
def __repr__(self):
return '<Award %r>' % self.name
class Tags(db.Model):
__tablename__ = 'tags'
id = db.Column(db.Integer, primary_key=True)
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
value = db.Column(db.String(80))
def __init__(self, *args, **kwargs):
super(Tags, self).__init__(**kwargs)
class Files(db.Model):
__tablename__ = 'files'
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(80), default='standard')
location = db.Column(db.Text)
__mapper_args__ = {
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
def __init__(self, *args, **kwargs):
super(Files, self).__init__(**kwargs)
def __repr__(self):
return "<File type={type} location={location}>".format(type=self.type, location=self.location)
class ChallengeFiles(Files):
__mapper_args__ = {
'polymorphic_identity': 'challenge'
}
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
def __init__(self, *args, **kwargs):
super(ChallengeFiles, self).__init__(**kwargs)
class PageFiles(Files):
__mapper_args__ = {
'polymorphic_identity': 'page'
}
page_id = db.Column(db.Integer, db.ForeignKey('pages.id'))
def __init__(self, *args, **kwargs):
super(PageFiles, self).__init__(**kwargs)
class Flags(db.Model):
__tablename__ = 'flags'
id = db.Column(db.Integer, primary_key=True)
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
type = db.Column(db.String(80))
content = db.Column(db.Text)
data = db.Column(db.Text)
challenge = db.relationship('Challenges', foreign_keys="Flags.challenge_id", lazy='select')
__mapper_args__ = {
'polymorphic_on': type
}
def __init__(self, *args, **kwargs):
super(Flags, self).__init__(**kwargs)
def __repr__(self):
return "<Flag {0} for challenge {1}>".format(self.content, self.challenge_id)
class Users(db.Model):
__tablename__ = 'users'
__table_args__ = (
db.UniqueConstraint('id', 'oauth_id'),
{}
)
# Core attributes
id = db.Column(db.Integer, primary_key=True)
oauth_id = db.Column(db.Integer, unique=True)
# User names are not constrained to be unique to allow for official/unofficial teams.
name = db.Column(db.String(128))
password = db.Column(db.String(128))
email = db.Column(db.String(128), unique=True)
type = db.Column(db.String(80))
secret = db.Column(db.String(128))
# Supplementary attributes
website = db.Column(db.String(128))
affiliation = db.Column(db.String(128))
country = db.Column(db.String(32))
bracket = db.Column(db.String(32))
hidden = db.Column(db.Boolean, default=False)
banned = db.Column(db.Boolean, default=False)
verified = db.Column(db.Boolean, default=False)
# Relationship for Teams
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
__mapper_args__ = {
'polymorphic_identity': 'user',
'polymorphic_on': type
}
def __init__(self, **kwargs):
super(Users, self).__init__(**kwargs)
@validates('password')
def validate_password(self, key, plaintext):
return hash_password(str(plaintext))
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.id
@property
def solves(self):
return self.get_solves(admin=False)
@property
def fails(self):
return self.get_fails(admin=False)
@property
def awards(self):
return self.get_awards(admin=False)
@property
def score(self):
return self.get_score(admin=False)
@property
def place(self):
return self.get_place(admin=False)
def get_solves(self, admin=False):
solves = Solves.query.filter_by(user_id=self.id)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
solves = solves.filter(Solves.date < dt)
return solves.all()
def get_fails(self, admin=False):
fails = Fails.query.filter_by(user_id=self.id)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
fails = fails.filter(Solves.date < dt)
return fails.all()
def get_awards(self, admin=False):
awards = Awards.query.filter_by(user_id=self.id)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
awards = awards.filter(Solves.date < dt)
return awards.all()
def get_score(self, admin=False):
score = db.func.sum(Challenges.value).label('score')
user = db.session.query(
Solves.user_id,
score
) \
.join(Users, Solves.user_id == Users.id) \
.join(Challenges, Solves.challenge_id == Challenges.id) \
.filter(Users.id == self.id)
award_score = db.func.sum(Awards.value).label('award_score')
award = db.session.query(award_score).filter_by(user_id=self.id)
if not admin:
freeze = Configs.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
user = user.filter(Solves.date < freeze)
award = award.filter(Awards.date < freeze)
user = user.group_by(Solves.user_id).first()
award = award.first()
if user and award:
return int(user.score or 0) + int(award.award_score or 0)
elif user:
return int(user.score or 0)
elif award:
return int(award.award_score or 0)
else:
return 0
def get_place(self, admin=False, numeric=False):
"""
This method is generally a clone of CTFd.scoreboard.get_standings.
The point being that models.py must be self-reliant and have little
to no imports within the CTFd application as importing from the
application itself will result in a circular import.
"""
scores = db.session.query(
Solves.user_id.label('user_id'),
db.func.sum(Challenges.value).label('score'),
db.func.max(Solves.id).label('id'),
db.func.max(Solves.date).label('date')
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.user_id)
awards = db.session.query(
Awards.user_id.label('user_id'),
db.func.sum(Awards.value).label('score'),
db.func.max(Awards.id).label('id'),
db.func.max(Awards.date).label('date')
).filter(Awards.value != 0).group_by(Awards.user_id)
if not admin:
freeze = Configs.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
scores = scores.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
results = union_all(scores, awards).alias('results')
sumscores = db.session.query(
results.columns.user_id,
db.func.sum(results.columns.score).label('score'),
db.func.max(results.columns.id).label('id'),
db.func.max(results.columns.date).label('date')
).group_by(results.columns.user_id).subquery()
if admin:
standings_query = db.session.query(
Users.id.label('user_id'),
) \
.join(sumscores, Users.id == sumscores.columns.user_id) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
else:
standings_query = db.session.query(
Users.id.label('user_id'),
) \
.join(sumscores, Users.id == sumscores.columns.user_id) \
.filter(Users.banned == False, Users.hidden == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
standings = standings_query.all()
# http://codegolf.stackexchange.com/a/4712
try:
i = standings.index((self.id,)) + 1
if numeric:
return i
else:
k = i % 10
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
except ValueError:
return 0
class Admins(Users):
__tablename__ = 'admins'
__mapper_args__ = {
'polymorphic_identity': 'admin'
}
class Teams(db.Model):
__tablename__ = 'teams'
__table_args__ = (
db.UniqueConstraint('id', 'oauth_id'),
{}
)
# Core attributes
id = db.Column(db.Integer, primary_key=True)
oauth_id = db.Column(db.Integer, unique=True)
# Team names are not constrained to be unique to allow for official/unofficial teams.
name = db.Column(db.String(128))
email = db.Column(db.String(128), unique=True)
password = db.Column(db.String(128))
secret = db.Column(db.String(128))
members = db.relationship("Users", backref="team", foreign_keys='Users.team_id')
# Supplementary attributes
website = db.Column(db.String(128))
affiliation = db.Column(db.String(128))
country = db.Column(db.String(32))
bracket = db.Column(db.String(32))
hidden = db.Column(db.Boolean, default=False)
banned = db.Column(db.Boolean, default=False)
# Relationship for Users
captain_id = db.Column(db.Integer, db.ForeignKey('users.id'))
captain = db.relationship("Users", foreign_keys=[captain_id])
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
def __init__(self, **kwargs):
super(Teams, self).__init__(**kwargs)
@validates('password')
def validate_password(self, key, plaintext):
return hash_password(str(plaintext))
@property
def solves(self):
return self.get_solves(admin=False)
@property
def fails(self):
return self.get_fails(admin=False)
@property
def awards(self):
return self.get_awards(admin=False)
@property
def score(self):
return self.get_score(admin=False)
@property
def place(self):
return self.get_place(admin=False)
def get_solves(self, admin=False):
member_ids = [member.id for member in self.members]
solves = Solves.query.filter(
Solves.user_id.in_(member_ids)
).order_by(
Solves.date.asc()
)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
solves = solves.filter(Solves.date < dt)
return solves.all()
def get_fails(self, admin=False):
member_ids = [member.id for member in self.members]
fails = Fails.query.filter(
Fails.user_id.in_(member_ids)
).order_by(
Fails.date.asc()
)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
fails = fails.filter(Solves.date < dt)
return fails.all()
def get_awards(self, admin=False):
member_ids = [member.id for member in self.members]
awards = Awards.query.filter(
Awards.user_id.in_(member_ids)
).order_by(
Awards.date.asc()
)
freeze = get_config('freeze')
if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze)
awards = awards.filter(Solves.date < dt)
return awards.all()
def get_score(self, admin=False):
score = 0
for member in self.members:
score += member.get_score(admin=admin)
return score
def get_place(self, admin=False):
"""
This method is generally a clone of CTFd.scoreboard.get_standings.
The point being that models.py must be self-reliant and have little
to no imports within the CTFd application as importing from the
application itself will result in a circular import.
"""
scores = db.session.query(
Solves.team_id.label('team_id'),
db.func.sum(Challenges.value).label('score'),
db.func.max(Solves.id).label('id'),
db.func.max(Solves.date).label('date')
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.team_id)
awards = db.session.query(
Awards.team_id.label('team_id'),
db.func.sum(Awards.value).label('score'),
db.func.max(Awards.id).label('id'),
db.func.max(Awards.date).label('date')
).filter(Awards.value != 0).group_by(Awards.team_id)
if not admin:
freeze = Configs.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
scores = scores.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
results = union_all(scores, awards).alias('results')
sumscores = db.session.query(
results.columns.team_id,
db.func.sum(results.columns.score).label('score'),
db.func.max(results.columns.id).label('id'),
db.func.max(results.columns.date).label('date')
).group_by(results.columns.team_id).subquery()
if admin:
standings_query = db.session.query(
Teams.id.label('team_id'),
) \
.join(sumscores, Teams.id == sumscores.columns.team_id) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
else:
standings_query = db.session.query(
Teams.id.label('team_id'),
) \
.join(sumscores, Teams.id == sumscores.columns.team_id) \
.filter(Teams.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
standings = standings_query.all()
# http://codegolf.stackexchange.com/a/4712
try:
i = standings.index((self.id,)) + 1
k = i % 10
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
except ValueError:
return 0
class Submissions(db.Model):
__tablename__ = 'submissions'
id = db.Column(db.Integer, primary_key=True)
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE'))
ip = db.Column(db.String(46))
provided = db.Column(db.Text)
type = db.Column(db.String(32))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
# Relationships
user = db.relationship('Users', foreign_keys="Submissions.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Submissions.team_id", lazy='select')
challenge = db.relationship('Challenges', foreign_keys="Submissions.challenge_id", lazy='select')
__mapper_args__ = {
'polymorphic_on': type,
}
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.user_id
@hybrid_property
def account(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team
elif user_mode == 'users':
return self.user
@staticmethod
def get_child(type):
child_classes = {
x.polymorphic_identity: x.class_
for x in Submissions.__mapper__.self_and_descendants
}
return child_classes[type]
def __repr__(self):
return '<Submission {}, {}, {}, {}>'.format(self.team_id, self.challenge_id, self.ip, self.provided)
class Solves(Submissions):
__tablename__ = 'solves'
__table_args__ = (
db.UniqueConstraint('challenge_id', 'user_id'),
db.UniqueConstraint('challenge_id', 'team_id'),
{}
)
id = db.Column(None, db.ForeignKey('submissions.id', ondelete='CASCADE'), primary_key=True)
challenge_id = column_property(db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')),
Submissions.challenge_id)
user_id = column_property(db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')), Submissions.user_id)
team_id = column_property(db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')), Submissions.team_id)
user = db.relationship('Users', foreign_keys="Solves.user_id", lazy='select')
team = db.relationship('Teams', foreign_keys="Solves.team_id", lazy='select')
challenge = db.relationship('Challenges', foreign_keys="Solves.challenge_id", lazy='select')
__mapper_args__ = {
'polymorphic_identity': 'correct'
}
class Fails(Submissions):
__mapper_args__ = {
'polymorphic_identity': 'incorrect'
}
class Unlocks(db.Model):
__tablename__ = 'unlocks'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
target = db.Column(db.Integer)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
type = db.Column(db.String(32))
__mapper_args__ = {
'polymorphic_on': type,
}
@hybrid_property
def account_id(self):
user_mode = get_config('user_mode')
if user_mode == 'teams':
return self.team_id
elif user_mode == 'users':
return self.user_id
def __repr__(self):
return '<Unlock %r>' % self.id
class HintUnlocks(Unlocks):
__mapper_args__ = {
'polymorphic_identity': 'hints'
}
class Tracking(db.Model):
__tablename__ = 'tracking'
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(32))
ip = db.Column(db.String(46))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
user = db.relationship('Users', foreign_keys="Tracking.user_id", lazy='select')
__mapper_args__ = {
'polymorphic_on': type,
}
def __init__(self, *args, **kwargs):
super(Tracking, self).__init__(**kwargs)
def __repr__(self):
return '<Tracking %r>' % self.ip
class Configs(db.Model):
__tablename__ = 'config'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.Text)
value = db.Column(db.Text)
def __init__(self, *args, **kwargs):
super(Configs, self).__init__(**kwargs)
@cache.memoize()
def get_config(key):
"""
This should be a direct clone of its implementation in utils. It is used to avoid a circular import.
"""
config = Configs.query.filter_by(key=key).first()
if config and config.value:
value = config.value
if value and value.isdigit():
return int(value)
elif value and isinstance(value, six.string_types):
if value.lower() == 'true':
return True
elif value.lower() == 'false':
return False
else:
return value