From bf241eb1a56231e2336300befc4c8f1e2465ee89 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Thu, 22 Nov 2018 18:33:08 -0500 Subject: [PATCH] Auth Improvments (#746) * Create generic get_errors, get_infos; add MLC OAuth settings in config * Use new get_errors function --- CTFd/admin/__init__.py | 3 +- CTFd/admin/teams.py | 9 ++-- CTFd/admin/users.py | 8 ++-- CTFd/auth.py | 45 ++++++++++++++++---- CTFd/challenges.py | 14 +++--- CTFd/teams.py | 5 ++- CTFd/themes/admin/templates/config.html | 5 +++ CTFd/themes/admin/templates/configs/mlc.html | 30 +++++++++++++ CTFd/utils/helpers/__init__.py | 24 ++++++++++- tests/users/test_hints.py | 2 +- 10 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 CTFd/themes/admin/templates/configs/mlc.html diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index deecf3b4..c85ddd58 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -24,6 +24,7 @@ from CTFd.utils import ( set_config, ) from CTFd.cache import cache, clear_config +from CTFd.utils.helpers import get_errors from CTFd.utils.exports import ( export_ctf as export_ctf_util, import_ctf as import_ctf_util @@ -94,7 +95,7 @@ def plugin(plugin): def import_ctf(): backup = request.files['backup'] segments = request.form.get('segments') - errors = [] + errors = get_errors() try: import_ctf_util(backup) except Exception as e: diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py index dddc922d..ba50b8cb 100644 --- a/CTFd/admin/teams.py +++ b/CTFd/admin/teams.py @@ -1,11 +1,10 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from CTFd.utils.decorators import admins_only, ratelimit from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs -from passlib.hash import bcrypt_sha256 -from sqlalchemy.sql import not_ - -from CTFd import utils from CTFd.admin import admin +from CTFd.utils.helpers import get_errors, get_infos + +from sqlalchemy.sql import not_ @admin.route('/admin/teams') @@ -16,7 +15,7 @@ def teams_listing(): if q: field = request.args.get('field') teams = [] - errors = [] + errors = get_errors() if field == 'id': if q.isnumeric(): teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all() diff --git a/CTFd/admin/users.py b/CTFd/admin/users.py index 4e04c625..46425b87 100644 --- a/CTFd/admin/users.py +++ b/CTFd/admin/users.py @@ -3,10 +3,10 @@ from CTFd.utils import get_config from CTFd.utils.decorators import admins_only, ratelimit from CTFd.utils.modes import USERS_MODE, TEAMS_MODE from CTFd.models import db, Users, Challenges, Tracking -from sqlalchemy.sql import not_ - -from CTFd import utils from CTFd.admin import admin +from CTFd.utils.helpers import get_errors, get_infos + +from sqlalchemy.sql import not_ @admin.route('/admin/users') @@ -17,7 +17,7 @@ def users_listing(): if q: field = request.args.get('field') users = [] - errors = [] + errors = get_errors() if field == 'id': if q.isnumeric(): users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all() diff --git a/CTFd/auth.py b/CTFd/auth.py index 6fbd2bea..deb35d39 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -1,4 +1,12 @@ -from flask import current_app as app, render_template, request, redirect, url_for, session, Blueprint, abort +from flask import ( + current_app as app, + render_template, + request, + redirect, + url_for, + session, + Blueprint, +) from passlib.hash import bcrypt_sha256 from CTFd.models import db, Users, Teams @@ -13,6 +21,7 @@ from CTFd.utils.logging import log from CTFd.utils.decorators.visibility import check_registration_visibility from CTFd.utils.modes import TEAMS_MODE, USERS_MODE from CTFd.utils.security.signing import serialize, unserialize, SignatureExpired, BadSignature, BadTimeSignature +from CTFd.utils.helpers import info_for, error_for, get_errors, get_infos import base64 import requests @@ -96,7 +105,7 @@ def reset_password(data=None): email_address = request.form['email'].strip() team = Users.query.filter_by(email=email_address).first() - errors = [] + errors = get_errors() if config.can_send_mail() is False: return render_template( @@ -123,8 +132,8 @@ def reset_password(data=None): @check_registration_visibility @ratelimit(method="POST", limit=10, interval=5) def register(): + errors = get_errors() if request.method == 'POST': - errors = [] name = request.form['name'] email_address = request.form['email'] password = request.form['password'] @@ -200,14 +209,14 @@ def register(): db.session.close() return redirect(url_for('challenges.listing')) else: - return render_template('register.html') + return render_template('register.html', errors=errors) @auth.route('/login', methods=['POST', 'GET']) @ratelimit(method="POST", limit=10, interval=5) def login(): + errors = get_errors() if request.method == 'POST': - errors = [] name = request.form['name'] # Check if the user submitted an email address or a team name @@ -242,7 +251,7 @@ def login(): return render_template('login.html', errors=errors) else: db.session.close() - return render_template('login.html') + return render_template('login.html', errors=errors) @auth.route('/oauth') @@ -257,6 +266,15 @@ def oauth_login(): scope = 'profile' client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id') + + if client_id is None: + error_for( + endpoint='auth.login', + message='OAuth Settings not configured. ' + 'Ask your CTF administrator to configure MajorLeagueCyber integration.' + ) + return redirect(url_for('auth.login')) + redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format( endpoint=endpoint, client_id=client_id, @@ -273,7 +291,8 @@ def oauth_redirect(): state = request.args.get('state') if session['nonce'] != state: log('logins', "[{date}] {ip} - OAuth State validation mismatch") - abort(500) + error_for(endpoint='auth.login', message='OAuth State validation mismatch.') + return redirect(url_for('auth.login')) if oauth_code: url = get_app_config('OAUTH_TOKEN_ENDPOINT') \ @@ -341,10 +360,18 @@ def oauth_redirect(): return redirect(url_for('challenges.listing')) else: log('logins', "[{date}] {ip} - OAuth token retrieval failure") - abort(500) + error_for( + endpoint='auth.login', + message='OAuth token retrieval failure.' + ) + return redirect(url_for('auth.login')) else: log('logins', "[{date}] {ip} - Received redirect without OAuth code") - abort(500) + error_for( + endpoint='auth.login', + message='Received redirect without OAuth code.' + ) + return redirect(url_for('auth.login')) @auth.route('/logout') diff --git a/CTFd/challenges.py b/CTFd/challenges.py index 39a03f48..ac622949 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -1,9 +1,8 @@ -from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint, abort -from CTFd.models import db, Challenges, Files, Solves, Fails, Flags, Tags, Teams, Awards, Hints, Unlocks -from CTFd.plugins.challenges import get_chal_class +from flask import ( + render_template, + Blueprint, +) from CTFd.utils.decorators import ( - authed_only, - admins_only, during_ctf_time_only, require_verified_emails, ratelimit, @@ -12,6 +11,7 @@ from CTFd.utils.decorators import ( from CTFd.utils.decorators.visibility import check_challenge_visibility from CTFd.utils import config, text_type, user as current_user, get_config from CTFd.utils.dates import ctftime, ctf_started, ctf_paused, ctf_ended, unix_time, unix_time_to_utc +from CTFd.utils.helpers import get_errors, get_infos challenges = Blueprint('challenges', __name__) @@ -22,8 +22,8 @@ challenges = Blueprint('challenges', __name__) @check_challenge_visibility @require_team def listing(): - infos = [] - errors = [] + infos = get_infos() + errors = get_errors() start = get_config('start') or 0 end = get_config('end') or 0 diff --git a/CTFd/teams.py b/CTFd/teams.py index da047e36..16aca918 100644 --- a/CTFd/teams.py +++ b/CTFd/teams.py @@ -9,6 +9,7 @@ from CTFd.utils.user import get_current_user, authed, get_ip from CTFd.utils.dates import unix_time_to_utc from CTFd.utils.crypto import verify_password from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility +from CTFd.utils.helpers import get_errors, get_infos teams = Blueprint('teams', __name__) @@ -65,7 +66,7 @@ def new(): elif request.method == 'POST': teamname = request.form.get('name') passphrase = request.form.get('password', '').strip() - errors = [] + errors = get_errors() user = get_current_user() @@ -127,7 +128,7 @@ def private(): @check_score_visibility @require_team_mode def public(team_id): - errors = [] + errors = get_errors() team = Teams.query.filter_by(id=team_id).first_or_404() solves = team.get_solves() awards = team.get_awards() diff --git a/CTFd/themes/admin/templates/config.html b/CTFd/themes/admin/templates/config.html index 5a8386f8..496f2926 100644 --- a/CTFd/themes/admin/templates/config.html +++ b/CTFd/themes/admin/templates/config.html @@ -16,6 +16,9 @@ + @@ -49,6 +52,8 @@ {% include "admin/configs/accounts.html" %} + {% include "admin/configs/mlc.html" %} + {% include "admin/configs/settings.html" %} {% include "admin/configs/email.html" %} diff --git a/CTFd/themes/admin/templates/configs/mlc.html b/CTFd/themes/admin/templates/configs/mlc.html new file mode 100644 index 00000000..aac41daa --- /dev/null +++ b/CTFd/themes/admin/templates/configs/mlc.html @@ -0,0 +1,30 @@ +
+
+ +
+ + +
+ +
+ + +
+ + +
+
\ No newline at end of file diff --git a/CTFd/utils/helpers/__init__.py b/CTFd/utils/helpers/__init__.py index 64410ff4..6cc37827 100644 --- a/CTFd/utils/helpers/__init__.py +++ b/CTFd/utils/helpers/__init__.py @@ -1 +1,23 @@ -from flask import url_for, request +from flask import request, flash, get_flashed_messages + + +def info_for(endpoint, message): + flash( + message=message, + category=endpoint + '.infos' + ) + + +def error_for(endpoint, message): + flash( + message=message, + category=endpoint + '.errors' + ) + + +def get_infos(): + return get_flashed_messages(category_filter=request.endpoint + '.infos') + + +def get_errors(): + return get_flashed_messages(category_filter=request.endpoint + '.errors') diff --git a/tests/users/test_hints.py b/tests/users/test_hints.py index 77f9a80c..5028f54e 100644 --- a/tests/users/test_hints.py +++ b/tests/users/test_hints.py @@ -69,7 +69,7 @@ def test_user_can_unlock_hint(): r = client.get('/api/v1/hints/{}'.format(hint_id)) resp = r.get_json() - assert resp['data'].get('content') + assert resp['data'].get('content') == "This is a hint" user = Users.query.filter_by(name="user1").first() assert user.score == 5