mirror of https://github.com/JohnHammond/CTFd.git
Auth Improvments (#746)
* Create generic get_errors, get_infos; add MLC OAuth settings in config * Use new get_errors functionselenium-screenshot-testing
parent
a3bc7b3917
commit
bf241eb1a5
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
45
CTFd/auth.py
45
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 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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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" %}
|
||||||
|
|
|
@ -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>
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue