Merge remote-tracking branch 'origin/2.4.0-dev' into update-jquery-3.5.0

update-jquery-3.5.0
Kevin Chung 2020-04-30 14:34:47 -04:00
commit 7d35e552bc
15 changed files with 195 additions and 17 deletions

3
.gitignore vendored
View File

@ -73,3 +73,6 @@ CTFd/uploads
# JS # JS
node_modules/ node_modules/
# Flask Profiler files
flask_profiler.sql

View File

@ -3,7 +3,7 @@ import copy
from flask import abort, request, session from flask import abort, request, session
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.cache import clear_standings from CTFd.cache import clear_standings, clear_team_session, clear_user_session
from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db
from CTFd.schemas.awards import AwardSchema from CTFd.schemas.awards import AwardSchema
from CTFd.schemas.submissions import SubmissionSchema from CTFd.schemas.submissions import SubmissionSchema
@ -91,25 +91,31 @@ class TeamPublic(Resource):
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.commit() db.session.commit()
db.session.close()
clear_team_session(team_id=team.id)
clear_standings() clear_standings()
db.session.close()
return {"success": True, "data": response.data} return {"success": True, "data": response.data}
@admins_only @admins_only
def delete(self, team_id): def delete(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404() team = Teams.query.filter_by(id=team_id).first_or_404()
team_id = team.id
for member in team.members: for member in team.members:
member.team_id = None member.team_id = None
clear_user_session(user_id=member.id)
db.session.delete(team) db.session.delete(team)
db.session.commit() db.session.commit()
db.session.close()
clear_team_session(team_id=team_id)
clear_standings() clear_standings()
db.session.close()
return {"success": True} return {"success": True}
@ -150,7 +156,7 @@ class TeamPrivate(Resource):
return {"success": False, "errors": response.errors}, 400 return {"success": False, "errors": response.errors}, 400
db.session.commit() db.session.commit()
clear_team_session(team_id=team.id)
response = TeamSchema("self").dump(response.data) response = TeamSchema("self").dump(response.data)
db.session.close() db.session.close()

View File

@ -1,7 +1,7 @@
from flask import abort, request from flask import abort, request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.cache import clear_standings from CTFd.cache import clear_standings, clear_user_session
from CTFd.models import ( from CTFd.models import (
Awards, Awards,
Notifications, Notifications,
@ -107,6 +107,7 @@ class UserPublic(Resource):
db.session.close() db.session.close()
clear_user_session(user_id=user_id)
clear_standings() clear_standings()
return {"success": True, "data": response} return {"success": True, "data": response}
@ -123,6 +124,7 @@ class UserPublic(Resource):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
clear_user_session(user_id=user_id)
clear_standings() clear_standings()
return {"success": True} return {"success": True}
@ -149,6 +151,7 @@ class UserPrivate(Resource):
db.session.commit() db.session.commit()
clear_user_session(user_id=user.id)
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.close() db.session.close()

View File

@ -9,6 +9,7 @@ from itsdangerous.exc import BadSignature, BadTimeSignature, SignatureExpired
from CTFd.models import Teams, Users, db from CTFd.models import Teams, Users, db
from CTFd.utils import config, email, get_app_config, get_config from CTFd.utils import config, email, get_app_config, get_config
from CTFd.utils import user as current_user from CTFd.utils import user as current_user
from CTFd.cache import clear_user_session, clear_team_session
from CTFd.utils import validators from CTFd.utils import validators
from CTFd.utils.config import is_teams_mode from CTFd.utils.config import is_teams_mode
from CTFd.utils.config.integrations import mlc_registration from CTFd.utils.config.integrations import mlc_registration
@ -57,6 +58,7 @@ def confirm(data=None):
name=user.name, name=user.name,
) )
db.session.commit() db.session.commit()
clear_user_session(user_id=user.id)
email.successful_registration_notification(user.email) email.successful_registration_notification(user.email)
db.session.close() db.session.close()
if current_user.authed(): if current_user.authed():
@ -126,6 +128,7 @@ def reset_password(data=None):
user.password = password user.password = password
db.session.commit() db.session.commit()
clear_user_session(user_id=user.id)
log( log(
"logins", "logins",
format="[{date}] {ip} - successful password reset for {name}", format="[{date}] {ip} - successful password reset for {name}",
@ -411,6 +414,7 @@ def oauth_redirect():
team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id) team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id)
db.session.add(team) db.session.add(team)
db.session.commit() db.session.commit()
clear_team_session(team_id=team.id)
team_size_limit = get_config("team_size", default=0) team_size_limit = get_config("team_size", default=0)
if team_size_limit and len(team.members) >= team_size_limit: if team_size_limit and len(team.members) >= team_size_limit:
@ -428,6 +432,7 @@ def oauth_redirect():
user.oauth_id = user_id user.oauth_id = user_id
user.verified = True user.verified = True
db.session.commit() db.session.commit()
clear_user_session(user_id=user.id)
login_user(user) login_user(user)

View File

@ -44,3 +44,15 @@ def clear_pages():
cache.delete_memoized(get_pages) cache.delete_memoized(get_pages)
cache.delete_memoized(get_page) cache.delete_memoized(get_page)
def clear_user_session(user_id):
from CTFd.utils.user import get_user_attrs
cache.delete_memoized(get_user_attrs, user_id=user_id)
def clear_team_session(team_id):
from CTFd.utils.user import get_team_attrs
cache.delete_memoized(get_team_attrs, team_id=team_id)

20
CTFd/constants/teams.py Normal file
View File

@ -0,0 +1,20 @@
from collections import namedtuple
TeamAttrs = namedtuple(
"TeamAttrs",
[
"id",
"oauth_id",
"name",
"email",
"secret",
"website",
"affiliation",
"country",
"bracket",
"hidden",
"banned",
"captain_id",
"created",
],
)

22
CTFd/constants/users.py Normal file
View File

@ -0,0 +1,22 @@
from collections import namedtuple
UserAttrs = namedtuple(
"UserAttrs",
[
"id",
"oauth_id",
"name",
"email",
"type",
"secret",
"website",
"affiliation",
"country",
"bracket",
"hidden",
"banned",
"verified",
"team_id",
"created",
],
)

View File

@ -1,5 +1,6 @@
from flask import Blueprint, redirect, render_template, request, url_for from flask import Blueprint, redirect, render_template, request, url_for
from CTFd.cache import clear_user_session, clear_team_session
from CTFd.models import Teams, db from CTFd.models import Teams, db
from CTFd.utils import config, get_config from CTFd.utils import config, get_config
from CTFd.utils.crypto import verify_password from CTFd.utils.crypto import verify_password
@ -63,7 +64,6 @@ def join():
passphrase = request.form.get("password", "").strip() passphrase = request.form.get("password", "").strip()
team = Teams.query.filter_by(name=teamname).first() team = Teams.query.filter_by(name=teamname).first()
user = get_current_user()
if team and verify_password(passphrase, team.password): if team and verify_password(passphrase, team.password):
team_size_limit = get_config("team_size", default=0) team_size_limit = get_config("team_size", default=0)
@ -77,6 +77,7 @@ def join():
"teams/join_team.html", infos=infos, errors=errors "teams/join_team.html", infos=infos, errors=errors
) )
user = get_current_user()
user.team_id = team.id user.team_id = team.id
db.session.commit() db.session.commit()
@ -84,6 +85,9 @@ def join():
team.captain_id = user.id team.captain_id = user.id
db.session.commit() db.session.commit()
clear_user_session(user_id=user.id)
clear_team_session(team_id=team.id)
return redirect(url_for("challenges.listing")) return redirect(url_for("challenges.listing"))
else: else:
errors.append("That information is incorrect") errors.append("That information is incorrect")
@ -130,6 +134,10 @@ def new():
user.team_id = team.id user.team_id = team.id
db.session.commit() db.session.commit()
clear_user_session(user_id=user.id)
clear_team_session(team_id=team.id)
return redirect(url_for("challenges.listing")) return redirect(url_for("challenges.listing"))

View File

@ -38,7 +38,13 @@ from CTFd.utils.plugins import (
) )
from CTFd.utils.security.auth import login_user, logout_user, lookup_user_token from CTFd.utils.security.auth import login_user, logout_user, lookup_user_token
from CTFd.utils.security.csrf import generate_nonce from CTFd.utils.security.csrf import generate_nonce
from CTFd.utils.user import authed, get_current_team, get_current_user, get_ip, is_admin from CTFd.utils.user import (
authed,
get_current_user_attrs,
get_current_team_attrs,
get_ip,
is_admin,
)
def init_template_filters(app): def init_template_filters(app):
@ -76,6 +82,9 @@ def init_template_globals(app):
app.jinja_env.globals.update(integrations=integrations) app.jinja_env.globals.update(integrations=integrations)
app.jinja_env.globals.update(authed=authed) app.jinja_env.globals.update(authed=authed)
app.jinja_env.globals.update(is_admin=is_admin) app.jinja_env.globals.update(is_admin=is_admin)
app.jinja_env.globals.update(get_current_user_attrs=get_current_user_attrs)
app.jinja_env.globals.update(get_current_team_attrs=get_current_team_attrs)
app.jinja_env.globals.update(get_ip=get_ip)
def init_logs(app): def init_logs(app):
@ -191,8 +200,8 @@ def init_request_processors(app):
return return
if authed(): if authed():
user = get_current_user() user = get_current_user_attrs()
team = get_current_team() team = get_current_team_attrs()
if user and user.banned: if user and user.banned:
return ( return (

View File

@ -26,9 +26,7 @@ def generate_user_token(user, expiration=None):
value = hexencode(os.urandom(32)) value = hexencode(os.urandom(32))
temp_token = UserTokens.query.filter_by(value=value).first() temp_token = UserTokens.query.filter_by(value=value).first()
token = UserTokens( token = UserTokens(user_id=user.id, expiration=expiration, value=value)
user_id=user.id, expiration=expiration, value=value
)
db.session.add(token) db.session.add(token)
db.session.commit() db.session.commit()
return token return token

View File

@ -4,7 +4,10 @@ import re
from flask import current_app as app from flask import current_app as app
from flask import request, session from flask import request, session
from CTFd.models import Fails, Users, db from CTFd.cache import cache
from CTFd.constants.users import UserAttrs
from CTFd.constants.teams import TeamAttrs
from CTFd.models import Fails, Users, db, Teams
from CTFd.utils import get_config from CTFd.utils import get_config
@ -16,6 +19,24 @@ def get_current_user():
return None return None
def get_current_user_attrs():
if authed():
return get_user_attrs(user_id=session["id"])
else:
return None
@cache.memoize(timeout=30)
def get_user_attrs(user_id):
user = Users.query.filter_by(id=user_id).first()
if user:
d = {}
for field in UserAttrs._fields:
d[field] = getattr(user, field)
return UserAttrs(**d)
return None
def get_current_team(): def get_current_team():
if authed(): if authed():
user = get_current_user() user = get_current_user()
@ -24,9 +45,28 @@ def get_current_team():
return None return None
def get_current_team_attrs():
if authed():
user = get_user_attrs(user_id=session["id"])
if user.team_id:
return get_team_attrs(team_id=user.team_id)
return None
@cache.memoize(timeout=30)
def get_team_attrs(team_id):
team = Teams.query.filter_by(id=team_id).first()
if team:
d = {}
for field in TeamAttrs._fields:
d[field] = getattr(team, field)
return TeamAttrs(**d)
return None
def get_current_user_type(fallback=None): def get_current_user_type(fallback=None):
if authed(): if authed():
user = Users.query.filter_by(id=session["id"]).first() user = get_current_user_attrs()
return user.type return user.type
else: else:
return fallback return fallback
@ -38,7 +78,7 @@ def authed():
def is_admin(): def is_admin():
if authed(): if authed():
user = get_current_user() user = get_current_user_attrs()
return user.type == "admin" return user.type == "admin"
else: else:
return False return False
@ -46,7 +86,7 @@ def is_admin():
def is_verified(): def is_verified():
if get_config("verify_emails"): if get_config("verify_emails"):
user = get_current_user() user = get_current_user_attrs()
if user: if user:
return user.verified return user.verified
else: else:

View File

@ -10,7 +10,7 @@ psycopg2-binary==2.7.5
codecov==2.0.15 codecov==2.0.15
moto==1.3.7 moto==1.3.7
bandit==1.5.1 bandit==1.5.1
flask_profiler==1.7 flask_profiler==1.8.1
pytest-xdist==1.28.0 pytest-xdist==1.28.0
pytest-cov==2.8.1 pytest-cov==2.8.1
sphinx_rtd_theme==0.4.3 sphinx_rtd_theme==0.4.3

View File

@ -18,6 +18,7 @@ if args.profile:
"enabled": app.config["DEBUG"], "enabled": app.config["DEBUG"],
"storage": {"engine": "sqlite"}, "storage": {"engine": "sqlite"},
"basicAuth": {"enabled": False}, "basicAuth": {"enabled": False},
"ignore": ["^/themes/.*", "^/events"],
} }
flask_profiler.init_app(app) flask_profiler.init_app(app)
app.config["DEBUG_TB_PROFILER_ENABLED"] = True app.config["DEBUG_TB_PROFILER_ENABLED"] = True

0
tests/cache/__init__.py vendored Normal file
View File

51
tests/cache/test_cache.py vendored Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from CTFd.models import Users
from CTFd.utils.user import is_admin, get_current_user
from CTFd.utils.security.auth import login_user
from tests.helpers import create_ctfd, destroy_ctfd, register_user
from CTFd.cache import clear_user_session
def test_clear_user_session():
app = create_ctfd()
with app.app_context():
register_user(app)
# Users by default should have a non-admin type
user = Users.query.filter_by(id=2).first()
with app.test_request_context("/"):
login_user(user)
user = get_current_user()
assert user.id == 2
assert user.type == "user"
assert is_admin() is False
# Set the user's updated type
user = Users.query.filter_by(id=2).first()
user.type = "admin"
app.db.session.commit()
# The user shouldn't be considered admin because their type is still cached
user = Users.query.filter_by(id=2).first()
with app.test_request_context("/"):
login_user(user)
user = get_current_user()
assert user.id == 2
assert user.type == "admin"
assert is_admin() is False
# Clear the user's cached session (for now just the type)
clear_user_session(user_id=2)
# The user's type should now be admin
user = Users.query.filter_by(id=2).first()
with app.test_request_context("/"):
login_user(user)
user = get_current_user()
assert user.id == 2
assert user.type == "admin"
assert is_admin() is True
destroy_ctfd(app)