Merge remote-tracking branch 'origin/2.4.0-dev' into cache-user-ips-for-tracker

cache-user-ips-for-tracker
Kevin Chung 2020-04-30 03:10:31 -04:00
commit 0bd6c0d958
17 changed files with 226 additions and 49 deletions

3
.gitignore vendored
View File

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

View File

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

View File

@ -1,7 +1,7 @@
from flask import abort, request
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 (
Awards,
Notifications,
@ -107,6 +107,7 @@ class UserPublic(Resource):
db.session.close()
clear_user_session(user_id=user_id)
clear_standings()
return {"success": True, "data": response}
@ -123,6 +124,7 @@ class UserPublic(Resource):
db.session.commit()
db.session.close()
clear_user_session(user_id=user_id)
clear_standings()
return {"success": True}
@ -149,6 +151,7 @@ class UserPrivate(Resource):
db.session.commit()
clear_user_session(user_id=user.id)
response = schema.dump(response.data)
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.utils import config, email, get_app_config, get_config
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.config import is_teams_mode
from CTFd.utils.config.integrations import mlc_registration
@ -57,6 +58,7 @@ def confirm(data=None):
name=user.name,
)
db.session.commit()
clear_user_session(user_id=user.id)
email.successful_registration_notification(user.email)
db.session.close()
if current_user.authed():
@ -126,6 +128,7 @@ def reset_password(data=None):
user.password = password
db.session.commit()
clear_user_session(user_id=user.id)
log(
"logins",
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)
db.session.add(team)
db.session.commit()
clear_team_session(team_id=team.id)
team_size_limit = get_config("team_size", default=0)
if team_size_limit and len(team.members) >= team_size_limit:
@ -428,6 +432,7 @@ def oauth_redirect():
user.oauth_id = user_id
user.verified = True
db.session.commit()
clear_user_session(user_id=user.id)
login_user(user)

View File

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

View File

@ -41,10 +41,9 @@ from CTFd.utils.security.auth import login_user, logout_user, lookup_user_token
from CTFd.utils.security.csrf import generate_nonce
from CTFd.utils.user import (
authed,
get_current_team,
get_current_user,
get_current_user_attrs,
get_current_team_attrs,
get_ip,
get_current_user_ips,
is_admin,
)
@ -84,6 +83,9 @@ def init_template_globals(app):
app.jinja_env.globals.update(integrations=integrations)
app.jinja_env.globals.update(authed=authed)
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):
@ -199,31 +201,34 @@ def init_request_processors(app):
logout_user()
clear_user_ips(user_id=session["id"])
if authed():
user = get_current_user()
team = get_current_team()
if request.path.startswith("/themes") is False:
if user and user.banned:
return (
render_template(
"errors/403.html",
error="You have been banned from this CTF",
),
403,
)
if team and team.banned:
return (
render_template(
"errors/403.html",
error="Your team has been banned from this CTF",
),
403,
)
db.session.close()
@app.before_request
def banned():
if request.endpoint == "views.themes":
return
if authed():
user = get_current_user_attrs()
team = get_current_team_attrs()
if user and user.banned:
return (
render_template(
"errors/403.html", error="You have been banned from this CTF"
),
403,
)
if team and team.banned:
return (
render_template(
"errors/403.html",
error="Your team has been banned from this CTF",
),
403,
)
@app.before_request
def tokens():
token = request.headers.get("Authorization")

View File

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

View File

@ -5,7 +5,9 @@ from flask import current_app as app
from flask import request, session
from CTFd.cache import cache
from CTFd.models import Fails, Users, db, Tracking
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
@ -17,6 +19,24 @@ def get_current_user():
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():
if authed():
user = get_current_user()
@ -25,9 +45,28 @@ def get_current_team():
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):
if authed():
user = Users.query.filter_by(id=session["id"]).first()
user = get_current_user_attrs()
return user.type
else:
return fallback
@ -39,7 +78,7 @@ def authed():
def is_admin():
if authed():
user = get_current_user()
user = get_current_user_attrs()
return user.type == "admin"
else:
return False
@ -47,7 +86,7 @@ def is_admin():
def is_verified():
if get_config("verify_emails"):
user = get_current_user()
user = get_current_user_attrs()
if user:
return user.verified
else:

View File

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

View File

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

View File

@ -59,28 +59,31 @@ def test_hidden_teams_visibility():
register_user(app)
with login_as_user(app) as client:
user = Users.query.filter_by(id=2).first()
user_id = user.id
team = gen_team(app.db, name="visible_team", hidden=True)
team_id = team.id
team_name = team.name
team.members.append(user)
user.team_id = team.id
app.db.session.commit()
r = client.get("/teams")
response = r.get_data(as_text=True)
assert team.name not in response
assert team_name not in response
r = client.get("/api/v1/teams")
response = r.get_json()
assert team.name not in response
assert team_name not in response
gen_award(app.db, user.id, team_id=team.id)
gen_award(app.db, user_id, team_id=team_id)
r = client.get("/scoreboard")
response = r.get_data(as_text=True)
assert team.name not in response
assert team_name not in response
r = client.get("/api/v1/scoreboard")
response = r.get_json()
assert team.name not in response
assert team_name not in response
# Team should re-appear after disabling hiding
# Use an API call to cause a cache clear
@ -90,15 +93,15 @@ def test_hidden_teams_visibility():
r = client.get("/teams")
response = r.get_data(as_text=True)
assert team.name in response
assert team_name in response
r = client.get("/api/v1/teams")
response = r.get_data(as_text=True)
assert team.name in response
assert team_name in response
r = client.get("/api/v1/scoreboard")
response = r.get_data(as_text=True)
assert team.name in response
assert team_name in response
destroy_ctfd(app)

View File

@ -48,6 +48,7 @@ def test_hidden_user_visibility():
with login_as_user(app, name="hidden_user") as client:
user = Users.query.filter_by(id=2).first()
user_id = user.id
user_name = user.name
user.hidden = True
app.db.session.commit()
@ -60,7 +61,7 @@ def test_hidden_user_visibility():
response = r.get_json()
assert user_name not in response
gen_award(app.db, user.id)
gen_award(app.db, user_id)
r = client.get("/scoreboard")
response = r.get_data(as_text=True)