Auth Improvments (#746)

* Create generic get_errors, get_infos; add MLC OAuth settings in config
* Use new get_errors function
selenium-screenshot-testing
Kevin Chung 2018-11-22 18:33:08 -05:00 committed by GitHub
parent a3bc7b3917
commit bf241eb1a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 30 deletions

View File

@ -24,6 +24,7 @@ from CTFd.utils import (
set_config, set_config,
) )
from CTFd.cache import cache, clear_config from CTFd.cache import cache, clear_config
from CTFd.utils.helpers import get_errors
from CTFd.utils.exports import ( from CTFd.utils.exports import (
export_ctf as export_ctf_util, export_ctf as export_ctf_util,
import_ctf as import_ctf_util import_ctf as import_ctf_util
@ -94,7 +95,7 @@ def plugin(plugin):
def import_ctf(): def import_ctf():
backup = request.files['backup'] backup = request.files['backup']
segments = request.form.get('segments') segments = request.form.get('segments')
errors = [] errors = get_errors()
try: try:
import_ctf_util(backup) import_ctf_util(backup)
except Exception as e: except Exception as e:

View File

@ -1,11 +1,10 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint 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.utils.decorators import admins_only, ratelimit
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs 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.admin import admin
from CTFd.utils.helpers import get_errors, get_infos
from sqlalchemy.sql import not_
@admin.route('/admin/teams') @admin.route('/admin/teams')
@ -16,7 +15,7 @@ def teams_listing():
if q: if q:
field = request.args.get('field') field = request.args.get('field')
teams = [] teams = []
errors = [] errors = get_errors()
if field == 'id': if field == 'id':
if q.isnumeric(): if q.isnumeric():
teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all() teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all()

View File

@ -3,10 +3,10 @@ from CTFd.utils import get_config
from CTFd.utils.decorators import admins_only, ratelimit from CTFd.utils.decorators import admins_only, ratelimit
from CTFd.utils.modes import USERS_MODE, TEAMS_MODE from CTFd.utils.modes import USERS_MODE, TEAMS_MODE
from CTFd.models import db, Users, Challenges, Tracking from CTFd.models import db, Users, Challenges, Tracking
from sqlalchemy.sql import not_
from CTFd import utils
from CTFd.admin import admin from CTFd.admin import admin
from CTFd.utils.helpers import get_errors, get_infos
from sqlalchemy.sql import not_
@admin.route('/admin/users') @admin.route('/admin/users')
@ -17,7 +17,7 @@ def users_listing():
if q: if q:
field = request.args.get('field') field = request.args.get('field')
users = [] users = []
errors = [] errors = get_errors()
if field == 'id': if field == 'id':
if q.isnumeric(): if q.isnumeric():
users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all() users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all()

View File

@ -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 passlib.hash import bcrypt_sha256
from CTFd.models import db, Users, Teams 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.decorators.visibility import check_registration_visibility
from CTFd.utils.modes import TEAMS_MODE, USERS_MODE from CTFd.utils.modes import TEAMS_MODE, USERS_MODE
from CTFd.utils.security.signing import serialize, unserialize, SignatureExpired, BadSignature, BadTimeSignature 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 base64
import requests import requests
@ -96,7 +105,7 @@ def reset_password(data=None):
email_address = request.form['email'].strip() email_address = request.form['email'].strip()
team = Users.query.filter_by(email=email_address).first() team = Users.query.filter_by(email=email_address).first()
errors = [] errors = get_errors()
if config.can_send_mail() is False: if config.can_send_mail() is False:
return render_template( return render_template(
@ -123,8 +132,8 @@ def reset_password(data=None):
@check_registration_visibility @check_registration_visibility
@ratelimit(method="POST", limit=10, interval=5) @ratelimit(method="POST", limit=10, interval=5)
def register(): def register():
errors = get_errors()
if request.method == 'POST': if request.method == 'POST':
errors = []
name = request.form['name'] name = request.form['name']
email_address = request.form['email'] email_address = request.form['email']
password = request.form['password'] password = request.form['password']
@ -200,14 +209,14 @@ def register():
db.session.close() db.session.close()
return redirect(url_for('challenges.listing')) return redirect(url_for('challenges.listing'))
else: else:
return render_template('register.html') return render_template('register.html', errors=errors)
@auth.route('/login', methods=['POST', 'GET']) @auth.route('/login', methods=['POST', 'GET'])
@ratelimit(method="POST", limit=10, interval=5) @ratelimit(method="POST", limit=10, interval=5)
def login(): def login():
errors = get_errors()
if request.method == 'POST': if request.method == 'POST':
errors = []
name = request.form['name'] name = request.form['name']
# Check if the user submitted an email address or a team 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) return render_template('login.html', errors=errors)
else: else:
db.session.close() db.session.close()
return render_template('login.html') return render_template('login.html', errors=errors)
@auth.route('/oauth') @auth.route('/oauth')
@ -257,6 +266,15 @@ def oauth_login():
scope = 'profile' scope = 'profile'
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id') 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( redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
endpoint=endpoint, endpoint=endpoint,
client_id=client_id, client_id=client_id,
@ -273,7 +291,8 @@ def oauth_redirect():
state = request.args.get('state') state = request.args.get('state')
if session['nonce'] != state: if session['nonce'] != state:
log('logins', "[{date}] {ip} - OAuth State validation mismatch") 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: if oauth_code:
url = get_app_config('OAUTH_TOKEN_ENDPOINT') \ url = get_app_config('OAUTH_TOKEN_ENDPOINT') \
@ -341,10 +360,18 @@ def oauth_redirect():
return redirect(url_for('challenges.listing')) return redirect(url_for('challenges.listing'))
else: else:
log('logins', "[{date}] {ip} - OAuth token retrieval failure") 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: else:
log('logins', "[{date}] {ip} - Received redirect without OAuth code") 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') @auth.route('/logout')

View File

@ -1,9 +1,8 @@
from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint, abort from flask import (
from CTFd.models import db, Challenges, Files, Solves, Fails, Flags, Tags, Teams, Awards, Hints, Unlocks render_template,
from CTFd.plugins.challenges import get_chal_class Blueprint,
)
from CTFd.utils.decorators import ( from CTFd.utils.decorators import (
authed_only,
admins_only,
during_ctf_time_only, during_ctf_time_only,
require_verified_emails, require_verified_emails,
ratelimit, ratelimit,
@ -12,6 +11,7 @@ from CTFd.utils.decorators import (
from CTFd.utils.decorators.visibility import check_challenge_visibility 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 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.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__) challenges = Blueprint('challenges', __name__)
@ -22,8 +22,8 @@ challenges = Blueprint('challenges', __name__)
@check_challenge_visibility @check_challenge_visibility
@require_team @require_team
def listing(): def listing():
infos = [] infos = get_infos()
errors = [] errors = get_errors()
start = get_config('start') or 0 start = get_config('start') or 0
end = get_config('end') or 0 end = get_config('end') or 0

View File

@ -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.dates import unix_time_to_utc
from CTFd.utils.crypto import verify_password from CTFd.utils.crypto import verify_password
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility 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__) teams = Blueprint('teams', __name__)
@ -65,7 +66,7 @@ def new():
elif request.method == 'POST': elif request.method == 'POST':
teamname = request.form.get('name') teamname = request.form.get('name')
passphrase = request.form.get('password', '').strip() passphrase = request.form.get('password', '').strip()
errors = [] errors = get_errors()
user = get_current_user() user = get_current_user()
@ -127,7 +128,7 @@ def private():
@check_score_visibility @check_score_visibility
@require_team_mode @require_team_mode
def public(team_id): def public(team_id):
errors = [] errors = get_errors()
team = Teams.query.filter_by(id=team_id).first_or_404() team = Teams.query.filter_by(id=team_id).first_or_404()
solves = team.get_solves() solves = team.get_solves()
awards = team.get_awards() awards = team.get_awards()

View File

@ -16,6 +16,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link rounded-0" href="#accounts" role="tab" data-toggle="tab">Accounts</a> <a class="nav-link rounded-0" href="#accounts" role="tab" data-toggle="tab">Accounts</a>
</li> </li>
<li class="nav-item">
<a class="nav-link rounded-0" href="#mlc" role="tab" data-toggle="tab">MajorLeagueCyber</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link rounded-0" href="#settings" role="tab" data-toggle="tab">Settings</a> <a class="nav-link rounded-0" href="#settings" role="tab" data-toggle="tab">Settings</a>
</li> </li>
@ -49,6 +52,8 @@
{% include "admin/configs/accounts.html" %} {% include "admin/configs/accounts.html" %}
{% include "admin/configs/mlc.html" %}
{% include "admin/configs/settings.html" %} {% include "admin/configs/settings.html" %}
{% include "admin/configs/email.html" %} {% include "admin/configs/email.html" %}

View File

@ -0,0 +1,30 @@
<div role="tabpanel" class="tab-pane config-section" id="mlc">
<form method="POST" autocomplete="off" class="w-100">
<div class="form-group">
<label>
Client ID
<small class="form-text text-muted">
OAuth Client ID for <a href="https://majorleaguecyber.org/">MajorLeagueCyber</a> integration.
</small>
</label>
<input class="form-control" id='oauth_client_id' name='oauth_client_id' type='text'
placeholder=""
{% if oauth_client_id is defined and oauth_client_id != None %}value="{{ oauth_client_id }}"{% endif %}>
</div>
<div class="form-group">
<label>
Client Secret
<small class="form-text text-muted">
OAuth Client Secret for <a href="https://majorleaguecyber.org/">MajorLeagueCyber</a> integration.
</small>
</label>
<input class="form-control" id='oauth_client_secret' name='oauth_client_secret' type='text'
placeholder=""
{% if oauth_client_secret is defined and oauth_client_secret != None %}value="{{ oauth_client_secret }}"{% endif %}>
</div>
<button type="submit" class="btn btn-md btn-primary float-right">Update</button>
</form>
</div>

View File

@ -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')

View File

@ -69,7 +69,7 @@ def test_user_can_unlock_hint():
r = client.get('/api/v1/hints/{}'.format(hint_id)) r = client.get('/api/v1/hints/{}'.format(hint_id))
resp = r.get_json() 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() user = Users.query.filter_by(name="user1").first()
assert user.score == 5 assert user.score == 5