init CTFd source
This commit is contained in:
0
tests/users/__init__.py
Normal file
0
tests/users/__init__.py
Normal file
837
tests/users/test_auth.py
Normal file
837
tests/users/test_auth.py
Normal file
@@ -0,0 +1,837 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Users, db
|
||||
from CTFd.utils import get_config, set_config
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_register_user():
|
||||
"""Can a user be registered"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_unicode_user():
|
||||
"""Can a user with a unicode name be registered"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="你好")
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_duplicate_username():
|
||||
"""A user shouldn't be able to use an already registered team name"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app,
|
||||
name="user1",
|
||||
email="user1@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
register_user(
|
||||
app,
|
||||
name="user1",
|
||||
email="user2@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
register_user(
|
||||
app,
|
||||
name="admin ",
|
||||
email="admin2@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the first created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_duplicate_email():
|
||||
"""A user shouldn't be able to use an already registered email address"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app,
|
||||
name="user1",
|
||||
email="user1@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
register_user(
|
||||
app,
|
||||
name="user2",
|
||||
email="user1@examplectf.com",
|
||||
password="password",
|
||||
raise_for_error=False,
|
||||
)
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # There's the admin user and the first created user
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_whitelisted_email():
|
||||
"""A user shouldn't be able to register with an email that isn't on the whitelist"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config(
|
||||
"domain_whitelist", "whitelisted.com, whitelisted.org, whitelisted.net"
|
||||
)
|
||||
register_user(
|
||||
app, name="not_whitelisted", email="user@nope.com", raise_for_error=False
|
||||
)
|
||||
assert Users.query.count() == 1
|
||||
|
||||
register_user(app, name="user1", email="user@whitelisted.com")
|
||||
assert Users.query.count() == 2
|
||||
|
||||
register_user(app, name="user2", email="user@whitelisted.org")
|
||||
assert Users.query.count() == 3
|
||||
|
||||
register_user(app, name="user3", email="user@whitelisted.net")
|
||||
assert Users.query.count() == 4
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_register_blacklisted_email():
|
||||
"""A user shouldn't be able to register with an email that is on the blacklist"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config(
|
||||
"domain_blacklist", "blacklisted.com, blacklisted.org, blacklisted.net"
|
||||
)
|
||||
register_user(
|
||||
app, name="blacklisted", email="user@blacklisted.com", raise_for_error=False
|
||||
)
|
||||
assert Users.query.count() == 1
|
||||
|
||||
register_user(app, name="user1", email="user@yep.com")
|
||||
assert Users.query.count() == 2
|
||||
|
||||
register_user(app, name="user2", email="user@yay.org")
|
||||
assert Users.query.count() == 3
|
||||
|
||||
register_user(app, name="user3", email="user@yipee.net")
|
||||
assert Users.query.count() == 4
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_bad_login():
|
||||
"""A user should not be able to login with an incorrect password"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(
|
||||
app, name="user", password="wrong_password", raise_for_error=False
|
||||
)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess.get("id") is None
|
||||
r = client.get("/profile")
|
||||
assert r.location.startswith("/login") # We got redirected to login
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_login():
|
||||
"""Can a registered user can login"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/profile")
|
||||
assert r.location is None # We didn't get redirected to login
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_login_with_email():
|
||||
"""Can a registered user can login with an email address instead of a team name"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app, name="user@examplectf.com", password="password")
|
||||
r = client.get("/profile")
|
||||
assert r.location is None # We didn't get redirected to login
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_logout():
|
||||
"""Can a registered user load /logout"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
client.get("/logout", follow_redirects=True)
|
||||
r = client.get("/challenges")
|
||||
assert r.location == "/login?next=%2Fchallenges%3F"
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_isnt_admin():
|
||||
"""A registered user cannot access admin pages"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
for page in [
|
||||
"pages",
|
||||
"users",
|
||||
"teams",
|
||||
"scoreboard",
|
||||
"challenges",
|
||||
"statistics",
|
||||
"config",
|
||||
]:
|
||||
r = client.get("/admin/{}".format(page))
|
||||
assert r.location.startswith("/login?next=")
|
||||
assert r.status_code == 302
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_expired_confirmation_links():
|
||||
"""Test that expired confirmation links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
set_config("verify_emails", True)
|
||||
|
||||
register_user(app, email="user@user.com")
|
||||
client = login_as_user(app, name="user", password="password")
|
||||
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
confirm_link = (
|
||||
"http://localhost/confirm/bb8a8526146e50778b211ae63074595880edbc0b"
|
||||
)
|
||||
r = client.get(confirm_link)
|
||||
|
||||
assert (
|
||||
"Your confirmation link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is not True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_confirmation_links():
|
||||
"""Test that invalid confirmation links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
set_config("verify_emails", True)
|
||||
|
||||
register_user(app, email="user@user.com")
|
||||
client = login_as_user(app, name="user", password="password")
|
||||
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
confirm_link = "http://localhost/confirm/a8375iyu<script>alert(1)<script>hn3048wueorighkgnsfg"
|
||||
r = client.get(confirm_link)
|
||||
|
||||
assert (
|
||||
"Your confirmation link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is not True
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_expired_reset_password_link():
|
||||
"""Test that expired reset password links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
forgot_link = "http://localhost/reset_password/bb8a8526146e50778b211ae63074595880edbc0b"
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert (
|
||||
"Your reset link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_reset_password_link():
|
||||
"""Test that invalid reset password links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
forgot_link = "http://localhost/reset_password/5678ytfghjiu876tyfg<INVALID DATA>hvbnmkoi9u87y6trdf"
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert (
|
||||
"Your reset link is invalid, please generate a new one"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_contact_for_password_reset():
|
||||
"""Test that if there is no mailserver configured, users should contact admins"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
forgot_link = "http://localhost/reset_password"
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert "contact an organizer" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_user_can_confirm_email(mock_smtp):
|
||||
"""Test that a user is capable of confirming their email address"""
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2012-01-14 03:21:34"):
|
||||
# Set CTFd to only allow confirmed users and send emails
|
||||
set_config("verify_emails", True)
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
# Teams are not verified by default
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is False
|
||||
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
r = client.get("/confirm")
|
||||
assert "We've sent a confirmation email" in r.get_data(as_text=True)
|
||||
|
||||
# smtp send message function was called
|
||||
mock_smtp.return_value.send_message.assert_called()
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
urandom_value = b"\xff" * 32
|
||||
with patch("os.urandom", return_value=urandom_value):
|
||||
data = {"nonce": sess.get("nonce")}
|
||||
r = client.post("http://localhost/confirm", data=data)
|
||||
assert "Confirmation email sent to" in r.get_data(as_text=True)
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.location == "/confirm" # We got redirected to /confirm
|
||||
|
||||
r = client.get(
|
||||
"http://localhost/confirm/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
)
|
||||
assert r.location == "/challenges"
|
||||
|
||||
# The team is now verified
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert user.verified is True
|
||||
|
||||
r = client.get("http://localhost/confirm")
|
||||
assert r.location == "/settings"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_user_can_reset_password(mock_smtp):
|
||||
"""Test that a user is capable of resetting their password"""
|
||||
from email.message import EmailMessage
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set CTFd to send emails
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
# Create a user
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
client.get("/reset_password")
|
||||
|
||||
# Build reset password data
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
||||
|
||||
# Issue the password reset request
|
||||
urandom_value = b"\xff" * 32
|
||||
with patch("os.urandom", return_value=urandom_value):
|
||||
client.post("/reset_password", data=data)
|
||||
|
||||
ctf_name = get_config("ctf_name")
|
||||
from_addr = get_config("mailfrom_addr") or app.config.get("MAILFROM_ADDR")
|
||||
from_addr = "{} <{}>".format(ctf_name, from_addr)
|
||||
|
||||
to_addr = "user@user.com"
|
||||
|
||||
# Build the email
|
||||
msg = (
|
||||
"Did you initiate a password reset on CTFd? If you didn't initiate this request you can ignore this email. "
|
||||
"\n\nClick the following link to reset your password:\n"
|
||||
"http://localhost/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\n"
|
||||
"If the link is not clickable, try copying and pasting it into your browser."
|
||||
)
|
||||
ctf_name = get_config("ctf_name")
|
||||
|
||||
email_msg = EmailMessage()
|
||||
email_msg.set_content(msg)
|
||||
|
||||
email_msg["Subject"] = "Password Reset Request from {ctf_name}".format(
|
||||
ctf_name=ctf_name
|
||||
)
|
||||
email_msg["From"] = from_addr
|
||||
email_msg["To"] = to_addr
|
||||
|
||||
# Make sure that the reset password email is sent
|
||||
mock_smtp.return_value.send_message.assert_called()
|
||||
assert str(mock_smtp.return_value.send_message.call_args[0][0]) == str(
|
||||
email_msg
|
||||
)
|
||||
|
||||
# Get user's original password
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
|
||||
# Build the POST data
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "password": "passwordtwo"}
|
||||
|
||||
# Do the password reset
|
||||
client.get(
|
||||
"/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
)
|
||||
client.post(
|
||||
"/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Make sure that the user's password changed
|
||||
user = Users.query.filter_by(email="user@user.com").first()
|
||||
assert verify_password("passwordtwo", user.password)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_banned_user():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
user.banned = True
|
||||
db.session.commit()
|
||||
|
||||
routes = ["/", "/challenges", "/api/v1/challenges"]
|
||||
for route in routes:
|
||||
r = client.get(route)
|
||||
assert r.status_code == 403
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_registration_code_required():
|
||||
"""
|
||||
Test that registration code configuration properly blocks logins
|
||||
with missing and incorrect registration codes
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set a registration code
|
||||
set_config("registration_code", "secret-sauce")
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Registration Code" in resp
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
# Attempt registration without password
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "The registration code you entered was incorrect" in resp
|
||||
|
||||
# Attempt registration with wrong password
|
||||
data["registration_code"] = "wrong-sauce"
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "The registration code you entered was incorrect" in resp
|
||||
|
||||
# Attempt registration with right password
|
||||
data["registration_code"] = "secret-sauce"
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_registration_code_allows_numeric():
|
||||
"""
|
||||
Test that registration code is allowed to be all numeric
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set a registration code
|
||||
set_config("registration_code", "1234567890")
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Registration Code" in resp
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Attempt registration with numeric registration code
|
||||
data["registration_code"] = "1234567890"
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_registration_password_minimum_length():
|
||||
"""
|
||||
Test that registration enforces minimum password length when configured
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Set a minimum password length
|
||||
set_config("password_min_length", 8)
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "short", # Only 5 characters
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Attempt registration with password too short
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Password must be at least 8 characters" in resp
|
||||
assert r.status_code == 200 # Should stay on registration page
|
||||
|
||||
# Verify user was not created
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 1 # Only admin user exists
|
||||
|
||||
# Attempt registration with password meeting minimum length
|
||||
data["password"] = "validpassword" # 13 characters, meets minimum
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
|
||||
# Verify user was created
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 2 # Admin user + new user
|
||||
|
||||
# Test with minimum length set to 0 (disabled)
|
||||
set_config("password_min_length", 0)
|
||||
|
||||
with app.test_client() as client:
|
||||
# Load CSRF nonce
|
||||
r = client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user2",
|
||||
"email": "user2@examplectf.com",
|
||||
"password": "x", # Only 1 character
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Should allow short password when minimum length is 0
|
||||
r = client.post("/register", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/challenges")
|
||||
|
||||
# Verify user was created
|
||||
user_count = Users.query.count()
|
||||
assert user_count == 3 # Admin user + 2 new users
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_change_password_required():
|
||||
"""
|
||||
Test that users with change_password=True are redirected to reset password
|
||||
and cannot access other pages until they change their password
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create a user with change_password=True
|
||||
register_user(
|
||||
app, name="testuser", email="test@example.com", password="oldpassword"
|
||||
)
|
||||
user = Users.query.filter_by(name="testuser").first()
|
||||
user.change_password = True
|
||||
db.session.commit()
|
||||
|
||||
with app.test_client() as client:
|
||||
# Login as the user
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "testuser",
|
||||
"password": "oldpassword",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
data["nonce"] = sess.get("nonce")
|
||||
|
||||
# Login
|
||||
r = client.post("/login", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Test that user is redirected to reset_password when accessing various pages
|
||||
protected_routes = [
|
||||
"/",
|
||||
"/challenges",
|
||||
"/scoreboard",
|
||||
"/profile",
|
||||
"/settings",
|
||||
"/api/v1/challenges",
|
||||
]
|
||||
|
||||
for route in protected_routes:
|
||||
r = client.get(route, follow_redirects=False)
|
||||
# Should be redirected to reset_password with a token
|
||||
assert r.status_code == 302
|
||||
assert "/reset_password/" in r.location
|
||||
|
||||
# Test that the user can access the reset_password page directly
|
||||
r = client.get(route, follow_redirects=True)
|
||||
# Should end up on reset_password page
|
||||
final_url = r.request.path
|
||||
assert "/reset_password/" in final_url
|
||||
|
||||
# Get redirected to reset password page with token
|
||||
r = client.get("/challenges", follow_redirects=False)
|
||||
assert r.status_code == 302
|
||||
assert "/reset_password/" in r.location
|
||||
|
||||
# Extract the token from the redirect URL
|
||||
reset_url = r.location
|
||||
token = reset_url.split("/reset_password/")[-1]
|
||||
|
||||
# Access the reset password page with the token
|
||||
r = client.get(f"/reset_password/{token}")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Actually reset the password using the reset password form
|
||||
with client.session_transaction() as sess:
|
||||
reset_data = {"password": "newpassword123", "nonce": sess.get("nonce")}
|
||||
|
||||
# Submit the password reset form
|
||||
r = client.post(f"/reset_password/{token}", data=reset_data)
|
||||
assert r.status_code == 302 # Should redirect after successful reset
|
||||
|
||||
# Verify the password was actually changed and change_password flag was cleared
|
||||
user = Users.query.filter_by(name="testuser").first()
|
||||
assert user.change_password is False
|
||||
assert verify_password("newpassword123", user.password)
|
||||
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "testuser",
|
||||
"password": "newpassword123",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
|
||||
r = client.post("/login", data=data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Now user should be able to access protected routes normally
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
assert r.location is None # No redirect
|
||||
|
||||
r = client.get("/profile")
|
||||
assert r.status_code == 200
|
||||
assert r.location is None # No redirect
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_admin_can_set_change_password_via_api():
|
||||
"""
|
||||
Test that admins can set the change_password attribute via the API
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Login as admin
|
||||
client = login_as_user(app, name="admin", password="password")
|
||||
|
||||
# Create a user via API with change_password=True
|
||||
r = client.post(
|
||||
"/api/v1/users",
|
||||
json={
|
||||
"name": "apiuser",
|
||||
"email": "apiuser@example.com",
|
||||
"password": "password123",
|
||||
"change_password": True,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify the user was created with change_password=True
|
||||
response_data = r.get_json()
|
||||
assert response_data["success"] is True
|
||||
user_id = response_data["data"]["id"]
|
||||
|
||||
user = Users.query.filter_by(id=user_id).first()
|
||||
assert user is not None
|
||||
assert user.change_password is True
|
||||
|
||||
# Update user via API to set change_password=False
|
||||
r = client.patch(f"/api/v1/users/{user_id}", json={"change_password": False})
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify the change_password was updated
|
||||
user = Users.query.filter_by(id=user_id).first()
|
||||
assert user.change_password is False
|
||||
|
||||
# Test that non-admin users cannot set change_password via API
|
||||
register_user(app, name="normaluser", email="normal@example.com")
|
||||
normal_client = login_as_user(app, name="normaluser", password="password")
|
||||
|
||||
# Try to modify change_password on their own account (should fail)
|
||||
normal_user = Users.query.filter_by(name="normaluser").first()
|
||||
r = normal_client.patch(
|
||||
f"/api/v1/users/{normal_user.id}",
|
||||
json={
|
||||
"change_password": True,
|
||||
},
|
||||
)
|
||||
# Normal users shouldn't be able to modify change_password field
|
||||
assert r.status_code == 403 # Forbidden
|
||||
|
||||
# Verify that change_password was not modified
|
||||
normal_user = Users.query.filter_by(name="normaluser").first()
|
||||
assert normal_user.change_password is False
|
||||
|
||||
# Also test via the /me endpoint
|
||||
r = normal_client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"change_password": True,
|
||||
},
|
||||
)
|
||||
# Request goes through but doesn't actually modify the attribute
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify that change_password was still not modified
|
||||
normal_user = Users.query.filter_by(name="normaluser").first()
|
||||
assert normal_user.change_password is False
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_user_reset_password_rate_limit(mock_smtp):
|
||||
"""
|
||||
Test that a user can only create 5 reset password attempts before they are rate limited
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Create a user before configuring mail to not send any extra emails
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
# Set CTFd to send emails
|
||||
set_config("mail_server", "localhost")
|
||||
set_config("mail_port", 25)
|
||||
set_config("mail_useauth", True)
|
||||
set_config("mail_username", "username")
|
||||
set_config("mail_password", "password")
|
||||
|
||||
with app.test_client() as client:
|
||||
# Make 5 password reset requests (which should all succeed)
|
||||
for _ in range(5):
|
||||
client.get("/reset_password")
|
||||
|
||||
# Build reset password data
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
||||
|
||||
# Issue the password reset request
|
||||
r = client.post("/reset_password", data=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_data(as_text=True)
|
||||
assert (
|
||||
"If that account exists you will receive an email, please check your inbox"
|
||||
in resp
|
||||
)
|
||||
assert "Too many password reset attempts" not in resp
|
||||
|
||||
# Verify that the email was sent 5 times
|
||||
assert mock_smtp.return_value.send_message.call_count == 5
|
||||
|
||||
# 6th attempt should be rate limited
|
||||
client.get("/reset_password")
|
||||
with client.session_transaction() as sess:
|
||||
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
||||
|
||||
r = client.post("/reset_password", data=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "Too many password reset attempts. Please try again later." in resp
|
||||
|
||||
# Verify that no additional email was sent
|
||||
assert mock_smtp.return_value.send_message.call_count == 5
|
||||
|
||||
destroy_ctfd(app)
|
||||
950
tests/users/test_challenges.py
Normal file
950
tests/users/test_challenges.py
Normal file
@@ -0,0 +1,950 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Challenges, Fails, Ratelimiteds, Solves
|
||||
from CTFd.utils import set_config, text_type
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_challenge,
|
||||
gen_fail,
|
||||
gen_flag,
|
||||
gen_hint,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_user_get_challenges():
|
||||
"""
|
||||
Can a registered user load /challenges
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_chals():
|
||||
"""
|
||||
Can a registered user load /chals
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_viewing_challenges():
|
||||
"""
|
||||
Test that users can see added challenges
|
||||
"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
gen_challenge(app.db)
|
||||
r = client.get("/api/v1/challenges")
|
||||
chals = r.get_json()["data"]
|
||||
assert len(chals) == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_viewing_challenge():
|
||||
"""Test that users can see individual challenges"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
gen_challenge(app.db)
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.get_json()
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
# def test_chals_solves():
|
||||
# """Test that the /chals/solves endpoint works properly"""
|
||||
# app = create_ctfd()
|
||||
# with app.app_context():
|
||||
# # Generate 5 users
|
||||
# for c in range(1, 6):
|
||||
# name = "user{}".format(c)
|
||||
# email = "user{}@examplectf.com".format(c)
|
||||
# register_user(app, name=name, email=email, password="password")
|
||||
#
|
||||
# # Generate 5 challenges
|
||||
# for c in range(6):
|
||||
# chal1 = gen_challenge(app.db, value=100)
|
||||
#
|
||||
# user_ids = list(range(2, 7))
|
||||
# chal_ids = list(range(1, 6))
|
||||
# for u in user_ids:
|
||||
# for c in chal_ids:
|
||||
# gen_solve(app.db, teamid=u, chalid=c)
|
||||
# chal_ids.pop()
|
||||
#
|
||||
# client = login_as_user(app, name="user1")
|
||||
#
|
||||
# with client.session_transaction() as sess:
|
||||
# r = client.get('/chals/solves')
|
||||
# output = r.get_data(as_text=True)
|
||||
# saved = json.loads('''{
|
||||
# "1": 5,
|
||||
# "2": 4,
|
||||
# "3": 3,
|
||||
# "4": 2,
|
||||
# "5": 1,
|
||||
# "6": 0
|
||||
# }
|
||||
# ''')
|
||||
# received = json.loads(output)
|
||||
# assert saved == received
|
||||
# set_config('hide_scores', True)
|
||||
# with client.session_transaction():
|
||||
# r = client.get('/chals/solves')
|
||||
# output = r.get_data(as_text=True)
|
||||
# saved = json.loads('''{
|
||||
# "1": -1,
|
||||
# "2": -1,
|
||||
# "3": -1,
|
||||
# "4": -1,
|
||||
# "5": -1,
|
||||
# "6": -1
|
||||
# }
|
||||
# ''')
|
||||
# received = json.loads(output)
|
||||
# assert saved == received
|
||||
# destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_correct_flag():
|
||||
"""Test that correct flags are correct"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
data = {"submission": "flag", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_correct_static_case_insensitive_flag():
|
||||
"""Test that correct static flags are correct if the static flag is marked case_insensitive"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag", data="case_insensitive")
|
||||
data = {"submission": "FLAG", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_correct_regex_case_insensitive_flag():
|
||||
"""Test that correct regex flags are correct if the regex flag is marked case_insensitive"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(
|
||||
app.db,
|
||||
challenge_id=chal.id,
|
||||
type="regex",
|
||||
content="flag",
|
||||
data="case_insensitive",
|
||||
)
|
||||
data = {"submission": "FLAG", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_invalid_regex_flag():
|
||||
"""Test that invalid regex flags are errored out to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(
|
||||
app.db,
|
||||
challenge_id=chal.id,
|
||||
type="regex",
|
||||
content="**",
|
||||
data="case_insensitive",
|
||||
)
|
||||
data = {"submission": "FLAG", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "incorrect"
|
||||
assert resp.get("message") == "Regex parse error occured"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_incorrect_flag():
|
||||
"""Test that incorrect flags are incorrect"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
data = {"submission": "notflag", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "incorrect"
|
||||
assert resp.get("message") == "Incorrect"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_submitting_unicode_flag():
|
||||
"""Test that users can submit a unicode flag"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="你好")
|
||||
with client.session_transaction():
|
||||
data = {"submission": "你好", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_with_max_attempts():
|
||||
"""Test that users are locked out of a challenge after they reach max_attempts"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal = Challenges.query.filter_by(id=chal.id).first()
|
||||
chal_id = chal.id
|
||||
chal.max_attempts = 3
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
for _ in range(3):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 3
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert resp.get("message") == "Not accepted. You have 0 tries remaining"
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_with_max_attempts_timeout_behavior():
|
||||
"""Test that users are temporarily locked out of a challenge after reaching max_attempts with timeout behavior"""
|
||||
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("max_attempts_behavior", "timeout")
|
||||
set_config("max_attempts_timeout", 300) # 300 seconds timeout for test
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal = Challenges.query.filter_by(id=chal.id).first()
|
||||
chal_id = chal.id
|
||||
chal.max_attempts = 2
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
for _ in range(2):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Should be locked out now
|
||||
with freeze_time(timedelta(seconds=0)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Not accepted. Try again in 300 seconds" in resp.get("message")
|
||||
|
||||
# Use freeze_time to advance time by 290 seconds
|
||||
with freeze_time(timedelta(seconds=290)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Not accepted. Try again in 10 seconds" in resp.get("message")
|
||||
|
||||
# Use freeze_time to advance time by 301 seconds
|
||||
with freeze_time(timedelta(seconds=301)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
# Should be correct now
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_with_max_attempts_timeout_ratelimit():
|
||||
"""Test that max_attempts timeout ratelimit and global ratelimit work together correctly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("max_attempts_behavior", "timeout")
|
||||
set_config("max_attempts_timeout", 30) # 30 seconds timeout for test
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
# Challenge 1 with max_attempts = 5
|
||||
chal1 = gen_challenge(app.db)
|
||||
chal1_obj = Challenges.query.filter_by(id=chal1.id).first()
|
||||
chal1_obj.max_attempts = 5
|
||||
app.db.session.commit()
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag1")
|
||||
|
||||
# Challenge 2 with no max_attempts
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag2")
|
||||
|
||||
base_time = datetime.utcnow()
|
||||
|
||||
# Submit 5 wrong attempts to challenge 1 (triggers max_attempts ratelimit)
|
||||
with freeze_time(base_time):
|
||||
for _ in range(5):
|
||||
data = {"submission": "wrong", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# 6th attempt should be blocked by max_attempts timeout
|
||||
data = {"submission": "flag1", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Try again in 30 seconds" in resp.get("message")
|
||||
|
||||
# Now submit 5 more wrong attempts to challenge 2 (total 10 fails, triggers global ratelimit)
|
||||
for i in range(6):
|
||||
data = {"submission": "wrong", "challenge_id": chal2.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
if i < 5:
|
||||
assert r.status_code == 200
|
||||
else:
|
||||
# 11th attempt should be blocked by global ratelimit (60 seconds)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "You're submitting flags too fast" in resp.get("message")
|
||||
|
||||
# Check counts
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert (
|
||||
ratelimiteds == 2
|
||||
) # One max_attempts ratelimit + one global ratelimit
|
||||
|
||||
# After 30 seconds, max_attempts timeout should release but global ratelimit (60s) still active
|
||||
with freeze_time(base_time + timedelta(seconds=31)):
|
||||
# Try challenge 1 - should still be blocked by global ratelimit
|
||||
data = {"submission": "flag1", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "Try again in 30 seconds" in resp.get("message")
|
||||
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert ratelimiteds == 3 # Another ratelimit entry
|
||||
|
||||
# After 60 seconds, both ratelimits should be released
|
||||
with freeze_time(base_time + timedelta(seconds=61)):
|
||||
# Should be able to solve challenge 1 now
|
||||
data = {"submission": "flag1", "challenge_id": chal1.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
|
||||
# Verify solve was recorded
|
||||
solves = Solves.query.count()
|
||||
assert solves == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_max_attempts_timeout_config_change():
|
||||
"""Test that changing max_attempts_timeout resets attempt count because cache key changes"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("max_attempts_behavior", "timeout")
|
||||
set_config("max_attempts_timeout", 300) # Start with 300 seconds
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal = Challenges.query.filter_by(id=chal.id).first()
|
||||
chal_id = chal.id
|
||||
chal.max_attempts = 3
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
# Make 3 wrong attempts (hit the limit)
|
||||
for _ in range(3):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify we're locked out
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert "300 seconds" in resp.get("message")
|
||||
|
||||
# Change the max_attempts_timeout config (this changes the cache key)
|
||||
set_config("max_attempts_timeout", 30) # Change to 30 seconds
|
||||
|
||||
# Jump forward in time to ensure we're past the new 30 second rate limit
|
||||
with freeze_time(timedelta(seconds=35)):
|
||||
# Now we should be able to submit again because the cache key changed
|
||||
# Old submissions have also fallen off the 30 second window
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "correct"
|
||||
assert resp.get("message") == "Correct"
|
||||
|
||||
# Verify solve was recorded
|
||||
solves = Solves.query.count()
|
||||
assert solves == 1
|
||||
|
||||
# Verify the fail count is still accurate (3 fails from before)
|
||||
fails = Fails.query.count()
|
||||
assert fails == 3
|
||||
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert ratelimiteds == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_kpm_limit_no_freeze():
|
||||
"""Test that users are properly ratelimited when submitting flags"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
for _ in range(11):
|
||||
with client.session_transaction():
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 1
|
||||
|
||||
# We just want a consistent flag response countdown
|
||||
with freeze_time(timedelta(seconds=0)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 2
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert (
|
||||
resp.get("message")
|
||||
== "You're submitting flags too fast. Try again in 60 seconds."
|
||||
)
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_kpm_limit_freeze_time():
|
||||
"""Test that users are properly ratelimited when submitting flags"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
base_time = datetime.utcnow()
|
||||
|
||||
# First section: Use API to generate 10 fails + 1 ratelimit
|
||||
with freeze_time(base_time):
|
||||
for _ in range(11):
|
||||
with client.session_transaction():
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 1
|
||||
|
||||
# Within the 1 min time frame we should still be ratelimited
|
||||
with freeze_time(base_time + timedelta(seconds=11)):
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 429
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 10
|
||||
assert ratelimiteds == 2
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("status") == "ratelimited"
|
||||
assert (
|
||||
resp.get("message")
|
||||
== "You're submitting flags too fast. Try again in 50 seconds."
|
||||
)
|
||||
|
||||
# Generate 10 more fails at +60 seconds using gen_fail because freezegun cannot patch to sqlalchemy's default
|
||||
for _ in range(10):
|
||||
fail = gen_fail(app.db, user_id=2, challenge_id=chal_id, provided="notflag")
|
||||
fail.date = base_time + timedelta(seconds=60)
|
||||
app.db.session.commit()
|
||||
|
||||
# The 11th attempt via API should trigger another ratelimit
|
||||
with freeze_time(base_time + timedelta(seconds=60)):
|
||||
data = {"submission": "notflag", "challenge_id": chal_id}
|
||||
client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
ratelimiteds = Ratelimiteds.query.count()
|
||||
assert wrong_keys == 20
|
||||
assert ratelimiteds == 3
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_that_view_challenges_unregistered_works():
|
||||
"""Test that view_challenges_unregistered works"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
chal = gen_challenge(app.db, name=text_type("🐺"))
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
|
||||
client = app.test_client()
|
||||
r = client.get("/api/v1/challenges", json="")
|
||||
assert r.status_code == 403
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 302
|
||||
|
||||
set_config("challenge_visibility", "public")
|
||||
|
||||
client = app.test_client()
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.get_json()["data"]
|
||||
|
||||
r = client.get("/api/v1/challenges/1/solves")
|
||||
assert r.get_json().get("data") is not None
|
||||
|
||||
data = {"submission": "not_flag", "challenge_id": chal_id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data").get("status") == "authentication_required"
|
||||
assert r.get_json().get("data").get("message") is None
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_challenge_is_unreachable():
|
||||
"""Test that hidden challenges return 404 and do not insert a solve or wrong key"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db, state="hidden")
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
chal_id = chal.id
|
||||
|
||||
assert Challenges.query.count() == 1
|
||||
|
||||
r = client.get("/api/v1/challenges", json="")
|
||||
data = r.get_json().get("data")
|
||||
assert data == []
|
||||
|
||||
r = client.get("/api/v1/challenges/1", json="")
|
||||
assert r.status_code == 404
|
||||
data = r.get_json().get("data")
|
||||
assert data is None
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal_id}
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 404
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt?preview=true", json=data)
|
||||
assert r.status_code == 404
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_challenge_is_unsolveable():
|
||||
"""Test that hidden challenges return 404 and do not insert a solve or wrong key"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal = gen_challenge(app.db, state="hidden")
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal.id}
|
||||
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 404
|
||||
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_requirements_are_rejected():
|
||||
"""Test that invalid requirements JSON blobs are rejected by the API"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Test None/null values
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [None]}}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {
|
||||
"requirements": [
|
||||
"Challenge requirements cannot have a null prerequisite"
|
||||
]
|
||||
},
|
||||
}
|
||||
# Test empty strings
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [""]}}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {
|
||||
"requirements": [
|
||||
"Challenge requirements cannot have a null prerequisite"
|
||||
]
|
||||
},
|
||||
}
|
||||
# Test a valid integer
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [2]}}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_with_requirements_is_unsolveable():
|
||||
"""Test that a challenge with a requirement is unsolveable without first solving the requirement"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
|
||||
requirements = {"prerequisites": [1]}
|
||||
chal2 = gen_challenge(app.db, requirements=requirements)
|
||||
app.db.session.commit()
|
||||
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
|
||||
r = client.get("/api/v1/challenges")
|
||||
challenges = r.get_json()["data"]
|
||||
assert len(challenges) == 1
|
||||
assert challenges[0]["id"] == 1
|
||||
|
||||
r = client.get("/api/v1/challenges/2")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
# Attempt to solve hidden Challenge 2
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
# Solve Challenge 1
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
resp = r.get_json()["data"]
|
||||
assert resp["status"] == "correct"
|
||||
|
||||
# Challenge 2 should now be visible
|
||||
r = client.get("/api/v1/challenges")
|
||||
challenges = r.get_json()["data"]
|
||||
assert len(challenges) == 2
|
||||
|
||||
r = client.get("/api/v1/challenges/2")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json().get("data")["id"] == 2
|
||||
|
||||
# Attempt to solve the now-visible Challenge 2
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert resp["status"] == "correct"
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_cannot_be_solved_while_paused():
|
||||
"""Test that challenges cannot be solved when the CTF is paused"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("paused", True)
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Assert that there is a paused message
|
||||
data = r.get_data(as_text=True)
|
||||
assert "paused" in data
|
||||
|
||||
chal = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
data = {"submission": "flag", "challenge_id": chal.id}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# Assert that the JSON message is correct
|
||||
resp = r.get_json()["data"]
|
||||
assert r.status_code == 403
|
||||
assert resp["status"] == "paused"
|
||||
assert resp["message"] == "CTFd is paused"
|
||||
|
||||
# There are no solves saved
|
||||
solves = Solves.query.count()
|
||||
assert solves == 0
|
||||
|
||||
# There are no wrong keys saved
|
||||
wrong_keys = Fails.query.count()
|
||||
assert wrong_keys == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_board_under_view_after_ctf():
|
||||
"""Test that the challenge board does not show an error under view_after_ctf"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("view_after_ctf", True)
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=1, content="flag")
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=2, content="flag")
|
||||
|
||||
# CTF hasn't started yet. There should be an error message.
|
||||
with freeze_time("2017-10-3"):
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
assert "has not started yet" in r.get_data(as_text=True)
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert Solves.query.count() == 0
|
||||
|
||||
# CTF is ongoing. Normal operation.
|
||||
with freeze_time("2017-10-5"):
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
assert "has ended" not in r.get_data(as_text=True)
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
assert Solves.query.count() == 1
|
||||
|
||||
# CTF is now over. There should be a message and challenges should show submission status but not store solves
|
||||
with freeze_time("2017-10-7"):
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
assert "has ended" in r.get_data(as_text=True)
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 2}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
assert Solves.query.count() == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_under_view_after_ctf():
|
||||
app = create_ctfd()
|
||||
with app.app_context(), freeze_time("2017-10-7"):
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=1, content="flag")
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
assert Solves.query.count() == 0
|
||||
|
||||
data = {"submission": "notflag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
assert Fails.query.count() == 0
|
||||
|
||||
set_config("view_after_ctf", True)
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/api/v1/challenges")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"][0]["id"] == 1
|
||||
|
||||
r = client.get("/api/v1/challenges/1")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["id"] == 1
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "correct"
|
||||
assert Solves.query.count() == 0
|
||||
|
||||
data = {"submission": "notflag", "challenge_id": 1}
|
||||
r = client.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["data"]["status"] == "incorrect"
|
||||
assert Fails.query.count() == 0
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenges_admin_only_as_user():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("challenge_visibility", "admins")
|
||||
|
||||
register_user(app)
|
||||
admin = login_as_user(app, name="admin")
|
||||
|
||||
gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=1, content="flag")
|
||||
|
||||
r = admin.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = admin.get("/api/v1/challenges", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = admin.get("/api/v1/challenges/1", json="")
|
||||
assert r.status_code == 200
|
||||
|
||||
data = {"submission": "flag", "challenge_id": 1}
|
||||
r = admin.post("/api/v1/challenges/attempt", json=data)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
319
tests/users/test_fields.py
Normal file
319
tests/users/test_fields.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import UserFieldEntries
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_field,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_new_fields_show_on_pages():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
|
||||
gen_field(app.db)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
assert "CustomField" in r.get_data(as_text=True)
|
||||
assert "CustomFieldDescription" in r.get_data(as_text=True)
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
assert "CustomField" in r.get_data(as_text=True)
|
||||
assert "CustomFieldDescription" in r.get_data(as_text=True)
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"fields": [{"field_id": 1, "value": "CustomFieldEntry"}]},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp["success"] is True
|
||||
assert resp["data"]["fields"][0]["value"] == "CustomFieldEntry"
|
||||
assert resp["data"]["fields"][0]["description"] == "CustomFieldDescription"
|
||||
assert resp["data"]["fields"][0]["name"] == "CustomField"
|
||||
assert resp["data"]["fields"][0]["field_id"] == 1
|
||||
|
||||
r = client.get("/user")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField" in resp
|
||||
assert "CustomFieldEntry" in resp
|
||||
|
||||
r = client.get("/users/2")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField" in resp
|
||||
assert "CustomFieldEntry" in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_fields_required_on_register():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_field(app.db)
|
||||
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
client.get("/register")
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess.get("id") is None
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"fields[1]": "custom_field_value",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_fields_properties():
|
||||
"""Test that users can set and edit custom fields"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_field(
|
||||
app.db, name="CustomField1", required=True, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField2", required=False, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField3", required=False, public=False, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField4", required=False, public=False, editable=False
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# Manually register user so that we can populate the required field
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"fields[1]": "custom_field_value",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
{"field_id": 4, "value": "CustomFieldEntry4"},
|
||||
]
|
||||
},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp == {
|
||||
"success": False,
|
||||
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
|
||||
}
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 1, "value": "CustomFieldEntry1"},
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/user")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" not in resp
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
r = client.get("/users/2")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" not in resp
|
||||
assert "CustomField4" not in resp
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_boolean_checkbox_field():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_field(app.db, name="CustomField1", field_type="boolean", required=False)
|
||||
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
|
||||
# We should have rendered a checkbox input
|
||||
assert "checkbox" in resp
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
"fields[1]": "y",
|
||||
}
|
||||
client.post("/register", data=data)
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["id"]
|
||||
|
||||
assert UserFieldEntries.query.count() == 1
|
||||
assert UserFieldEntries.query.filter_by(id=1).first().value is True
|
||||
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "checkbox" in resp
|
||||
|
||||
r = client.patch(
|
||||
"/api/v1/users/me", json={"fields": [{"field_id": 1, "value": False}]}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert UserFieldEntries.query.count() == 1
|
||||
assert UserFieldEntries.query.filter_by(id=1).first().value is False
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_needs_all_required_fields():
|
||||
"""Test that users need to submit all required fields before viewing challenges"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# Manually create a user who has no fields set
|
||||
register_user(app)
|
||||
|
||||
# Create the fields that we want
|
||||
gen_field(
|
||||
app.db, name="CustomField1", required=True, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField2", required=False, public=True, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField3", required=False, public=False, editable=True
|
||||
)
|
||||
gen_field(
|
||||
app.db, name="CustomField4", required=False, public=False, editable=False
|
||||
)
|
||||
|
||||
# We can see all fields when we try to register
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# When we login with our manually made user
|
||||
# we should see all fields because we are missing a required field
|
||||
with login_as_user(app) as client:
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/settings")
|
||||
|
||||
# Populate the non-required fields
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={
|
||||
"fields": [
|
||||
{"field_id": 2, "value": "CustomFieldEntry2"},
|
||||
{"field_id": 3, "value": "CustomFieldEntry3"},
|
||||
{"field_id": 4, "value": "CustomFieldEntry4"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# I should still be restricted from seeing challenges
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 302
|
||||
assert r.location.startswith("/settings")
|
||||
|
||||
# I should still see all fields b/c I don't have a complete profile
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" in resp
|
||||
|
||||
# Populate the required fields
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"fields": [{"field_id": 1, "value": "CustomFieldEntry1"}]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# I can now go to challenges
|
||||
r = client.get("/challenges")
|
||||
assert r.status_code == 200
|
||||
|
||||
# I should only see edittable fields
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "CustomField1" in resp
|
||||
assert "CustomField2" in resp
|
||||
assert "CustomField3" in resp
|
||||
assert "CustomField4" not in resp
|
||||
|
||||
# I can't edit a non-editable field
|
||||
r = client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"fields": [{"field_id": 4, "value": "CustomFieldEntry4"}]},
|
||||
)
|
||||
resp = r.get_json()
|
||||
assert resp == {
|
||||
"success": False,
|
||||
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
|
||||
}
|
||||
destroy_ctfd(app)
|
||||
289
tests/users/test_hints.py
Normal file
289
tests/users/test_hints.py
Normal file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Unlocks, Users, db
|
||||
from CTFd.utils import set_config, text_type
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_hint,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_user_cannot_unlock_hint():
|
||||
"""Test that a user can't unlock a hint if they don't have enough points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client():
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
hint = gen_hint(db, chal_id, cost=10)
|
||||
hint_id = hint.id
|
||||
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
with client.session_transaction():
|
||||
r = client.get("/api/v1/hints/{}".format(hint_id))
|
||||
resp = r.get_json()
|
||||
assert resp["data"].get("content") is None
|
||||
assert resp["data"].get("cost") == 10
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_can_unlock_hint():
|
||||
"""Test that a user can unlock a hint if they have enough points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
with app.test_client():
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
chal_id = chal.id
|
||||
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
|
||||
hint = gen_hint(app.db, chal_id, cost=10)
|
||||
hint_id = hint.id
|
||||
|
||||
gen_award(app.db, user_id=2, value=15)
|
||||
|
||||
client = login_as_user(app, name="user1", password="password")
|
||||
|
||||
user = Users.query.filter_by(name="user1").first()
|
||||
assert user.score == 15
|
||||
|
||||
with client.session_transaction():
|
||||
r = client.get("/api/v1/hints/{}".format(hint_id))
|
||||
resp = r.get_json()
|
||||
assert resp["data"].get("content") is None
|
||||
|
||||
params = {"target": hint_id, "type": "hints"}
|
||||
|
||||
r = client.post("/api/v1/unlocks", json=params)
|
||||
resp = r.get_json()
|
||||
assert resp["success"] is True
|
||||
|
||||
r = client.get("/api/v1/hints/{}".format(hint_id))
|
||||
resp = r.get_json()
|
||||
assert resp["data"].get("content") == "This is a hint"
|
||||
|
||||
user = Users.query.filter_by(name="user1").first()
|
||||
assert user.score == 5
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_no_cost():
|
||||
"""Test that hints with no cost can be unlocked"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
client = login_as_user(app)
|
||||
# Attempt to access hint
|
||||
r = client.get("/api/v1/hints/1")
|
||||
resp = r.get_json()["data"]
|
||||
|
||||
# Hint does not provide content until an unlock is generated
|
||||
assert resp.get("content") is None
|
||||
|
||||
# We generate an unlock for the free hint
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
|
||||
# We should now be able to see content
|
||||
r = client.get("/api/v1/hints/1")
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("content") == "This is a hint"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_ctf_with_points():
|
||||
"""Test that hints with a cost are unlocked if you have the points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") == "This is a hint"
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 90
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_ctf_without_points():
|
||||
"""Test that hints with a cost are not unlocked if you don't have the points"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert (
|
||||
r.get_json()["errors"]["score"]
|
||||
== "You do not have enough points to unlock this hint"
|
||||
)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json()["data"].get("content") is None
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_before_ctf():
|
||||
"""Test that hints are not unlocked if the CTF hasn't begun"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
with freeze_time("2017-10-1"):
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 403
|
||||
assert r.get_json().get("data") is None
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json().get("data") is None
|
||||
assert r.status_code == 403
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
|
||||
assert user.score == 100
|
||||
assert Unlocks.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_ended_ctf():
|
||||
"""Test that hints with a cost are not unlocked if the CTF has ended"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
set_config(
|
||||
"start", "1507089600"
|
||||
) # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
|
||||
set_config(
|
||||
"end", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
|
||||
with freeze_time("2017-11-4"):
|
||||
client = login_as_user(app)
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.get_json().get("data") is None
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
assert r.status_code == 403
|
||||
assert r.get_json()
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 403
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 100
|
||||
assert Unlocks.query.count() == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hints_with_cost_during_frozen_ctf():
|
||||
"""Test that hints with a cost are unlocked if the CTF is frozen."""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config(
|
||||
"freeze", "1507262400"
|
||||
) # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
|
||||
with freeze_time("2017-10-4"):
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db)
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id, cost=10)
|
||||
gen_award(app.db, user_id=2)
|
||||
|
||||
with freeze_time("2017-10-8"):
|
||||
client = login_as_user(app)
|
||||
|
||||
client.get("/api/v1/hints/1")
|
||||
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
|
||||
r = client.get("/api/v1/hints/1")
|
||||
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("content") == "This is a hint"
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.score == 100
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_unlocking_hint_for_unicode_challenge():
|
||||
"""Test that hints for challenges with unicode names can be unlocked"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
chal = gen_challenge(app.db, name=text_type("🐺"))
|
||||
chal_id = chal.id
|
||||
gen_hint(app.db, chal_id)
|
||||
|
||||
client = login_as_user(app)
|
||||
|
||||
# Generate an unlock for the free hint
|
||||
client.post("/api/v1/unlocks", json={"target": 1, "type": "hints"})
|
||||
r = client.get("/api/v1/hints/1")
|
||||
assert r.status_code == 200
|
||||
resp = r.get_json()["data"]
|
||||
assert resp.get("content") == "This is a hint"
|
||||
destroy_ctfd(app)
|
||||
37
tests/users/test_profile.py
Normal file
37
tests/users/test_profile.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_email_cannot_be_changed_without_password():
|
||||
"""Test that a user can't update their email address without current password"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
data = {"name": "user", "email": "user2@examplectf.com"}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 400
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.email == "user@examplectf.com"
|
||||
|
||||
data = {"name": "user", "email": "user2@examplectf.com", "confirm": "asdf"}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 400
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.email == "user@examplectf.com"
|
||||
|
||||
data = {"name": "user", "email": "user2@examplectf.com", "confirm": "password"}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.email == "user2@examplectf.com"
|
||||
assert verify_password(plaintext="password", ciphertext=user.password)
|
||||
destroy_ctfd(app)
|
||||
365
tests/users/test_scoreboard.py
Normal file
365
tests/users/test_scoreboard.py
Normal file
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from CTFd.models import Users
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
gen_bracket,
|
||||
gen_challenge,
|
||||
gen_flag,
|
||||
gen_solve,
|
||||
get_scores,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_user_get_scoreboard_components():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
# test_user_get_scoreboard
|
||||
"""Can a registered user load scoreboard components"""
|
||||
r = client.get("/scoreboard")
|
||||
assert r.status_code == 200
|
||||
|
||||
# test_user_get_scores
|
||||
"""Can a registered user load /api/v1/scoreboard"""
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
assert r.status_code == 200
|
||||
|
||||
# test_user_get_topteams
|
||||
"""Can a registered user load /api/v1/scoreboard/top/10"""
|
||||
r = client.get("/api/v1/scoreboard/top/10")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_score_is_correct():
|
||||
"""Test that a user's score is correct"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
# create user1
|
||||
register_user(app, name="user1", email="user1@examplectf.com")
|
||||
|
||||
# create challenge
|
||||
chal = gen_challenge(app.db, value=100)
|
||||
gen_flag(app.db, challenge_id=chal.id, content="flag")
|
||||
chal_id = chal.id
|
||||
|
||||
# create a solve for the challenge for user1. (the id is 2 because of the admin)
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal_id)
|
||||
user1 = Users.query.filter_by(id=2).first()
|
||||
|
||||
# assert that user1's score is 100
|
||||
assert user1.score == 100
|
||||
assert user1.place == "1st"
|
||||
|
||||
# create user2
|
||||
register_user(app, name="user2", email="user2@examplectf.com")
|
||||
|
||||
# user2 solves the challenge
|
||||
gen_solve(app.db, 3, challenge_id=chal_id)
|
||||
|
||||
# assert that user2's score is 100 but is in 2nd place
|
||||
user2 = Users.query.filter_by(id=3).first()
|
||||
assert user2.score == 100
|
||||
assert user2.place == "2nd"
|
||||
|
||||
# create an award for user2
|
||||
gen_award(app.db, user_id=3, value=5)
|
||||
|
||||
# assert that user2's score is now 105 and is in 1st place
|
||||
assert user2.score == 105
|
||||
assert user2.place == "1st"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_top_10():
|
||||
"""Make sure top10 returns correct information"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_bracket(app.db, name="players1")
|
||||
gen_bracket(app.db, name="players2")
|
||||
register_user(app, name="user1", email="user1@examplectf.com", bracket_id=1)
|
||||
register_user(app, name="user2", email="user2@examplectf.com", bracket_id=2)
|
||||
register_user(app, bracket_id=1)
|
||||
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
chal1_id = chal1.id
|
||||
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
chal2_id = chal2.id
|
||||
|
||||
# Generates solve for user1
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal1_id)
|
||||
|
||||
with freeze_time("2017-10-4 03:25:45"):
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal2_id)
|
||||
|
||||
# Generate solve for user2
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
gen_solve(app.db, user_id=3, challenge_id=chal1_id)
|
||||
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/scoreboard/top/10")
|
||||
response = r.get_json()["data"]
|
||||
|
||||
saved = {
|
||||
"1": {
|
||||
"id": 2,
|
||||
"account_url": "/users/2",
|
||||
"name": "user1",
|
||||
"score": 200,
|
||||
"bracket_id": 1,
|
||||
"bracket_name": "players1",
|
||||
"solves": [
|
||||
{
|
||||
"date": "2017-10-03T03:21:34Z",
|
||||
"challenge_id": 1,
|
||||
"account_id": 2,
|
||||
"user_id": 2,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
},
|
||||
{
|
||||
"date": "2017-10-04T03:25:45Z",
|
||||
"challenge_id": 2,
|
||||
"account_id": 2,
|
||||
"user_id": 2,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
"2": {
|
||||
"id": 3,
|
||||
"account_url": "/users/3",
|
||||
"name": "user2",
|
||||
"score": 100,
|
||||
"bracket_id": 2,
|
||||
"bracket_name": "players2",
|
||||
"solves": [
|
||||
{
|
||||
"date": "2017-10-03T03:21:34Z",
|
||||
"challenge_id": 1,
|
||||
"account_id": 3,
|
||||
"user_id": 3,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
assert saved == response
|
||||
|
||||
r = client.get("/api/v1/scoreboard/top/10?bracket_id=2")
|
||||
response = r.get_json()["data"]
|
||||
saved = {
|
||||
"1": {
|
||||
"id": 3,
|
||||
"account_url": "/users/3",
|
||||
"name": "user2",
|
||||
"score": 100,
|
||||
"bracket_id": 2,
|
||||
"bracket_name": "players2",
|
||||
"solves": [
|
||||
{
|
||||
"date": "2017-10-03T03:21:34Z",
|
||||
"challenge_id": 1,
|
||||
"account_id": 3,
|
||||
"user_id": 3,
|
||||
"team_id": None,
|
||||
"value": 100,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
assert saved == response
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_scoring_logic():
|
||||
"""Test that scoring logic is correct"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
client1 = login_as_user(app, name="user1", password="password")
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
client2 = login_as_user(app, name="user2", password="password")
|
||||
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
chal1_id = chal1.id
|
||||
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
chal2_id = chal2.id
|
||||
|
||||
# user1 solves chal1
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user1 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user1"
|
||||
|
||||
# user2 solves chal1 and chal2
|
||||
with freeze_time("2017-10-4 03:30:34"):
|
||||
with client2.session_transaction():
|
||||
# solve chal1
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
# solve chal2
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
|
||||
# user1 solves chal2
|
||||
with freeze_time("2017-10-5 03:50:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 should still be on top because they solved chal2 first
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_scoring_logic_with_zero_point_challenges():
|
||||
"""Test that scoring logic is correct with zero point challenges. Zero point challenges should not tie break"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
admin = login_as_user(app, name="admin", password="password")
|
||||
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
client1 = login_as_user(app, name="user1", password="password")
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
client2 = login_as_user(app, name="user2", password="password")
|
||||
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal1.id, content="flag")
|
||||
chal1_id = chal1.id
|
||||
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_flag(app.db, challenge_id=chal2.id, content="flag")
|
||||
chal2_id = chal2.id
|
||||
|
||||
# A 0 point challenge shouldn't influence the scoreboard (see #577)
|
||||
chal0 = gen_challenge(app.db, value=0)
|
||||
gen_flag(app.db, challenge_id=chal0.id, content="flag")
|
||||
chal0_id = chal0.id
|
||||
|
||||
# user1 solves chal1
|
||||
with freeze_time("2017-10-3 03:21:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user1 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user1"
|
||||
|
||||
# user2 solves chal1 and chal2
|
||||
with freeze_time("2017-10-4 03:30:34"):
|
||||
with client2.session_transaction():
|
||||
# solve chal1
|
||||
data = {"submission": "flag", "challenge_id": chal1_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
# solve chal2
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 is now on top
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
|
||||
# user1 solves chal2
|
||||
with freeze_time("2017-10-5 03:50:34"):
|
||||
with client1.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal2_id}
|
||||
client1.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 should still be on top because they solved chal2 first
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
|
||||
# user2 solves a 0 point challenge
|
||||
with freeze_time("2017-10-5 03:55:34"):
|
||||
with client2.session_transaction():
|
||||
data = {"submission": "flag", "challenge_id": chal0_id}
|
||||
client2.post("/api/v1/challenges/attempt", json=data)
|
||||
|
||||
# user2 should still be on top because 0 point challenges should not tie break
|
||||
scores = get_scores(admin)
|
||||
assert scores[0]["name"] == "user2"
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_users_should_not_influence_scores():
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app, name="user1", email="user1@examplectf.com", password="password"
|
||||
)
|
||||
register_user(
|
||||
app, name="user2", email="user2@examplectf.com", password="password"
|
||||
)
|
||||
register_user(
|
||||
app, name="user3", email="user3@examplectf.com", password="password"
|
||||
)
|
||||
|
||||
user = Users.query.filter_by(name="user3").first()
|
||||
user.hidden = True
|
||||
app.db.session.commit()
|
||||
|
||||
client1 = login_as_user(app, name="user1", password="password")
|
||||
|
||||
# User 1 solves 1st challenge
|
||||
chal1 = gen_challenge(app.db)
|
||||
gen_solve(app.db, user_id=2, challenge_id=chal1.id)
|
||||
|
||||
# User 2 solves 2nd challenge
|
||||
chal2 = gen_challenge(app.db)
|
||||
gen_solve(app.db, user_id=3, challenge_id=chal2.id)
|
||||
|
||||
# User 3 solves both
|
||||
gen_solve(app.db, user_id=4, challenge_id=chal1.id)
|
||||
gen_solve(app.db, user_id=4, challenge_id=chal2.id)
|
||||
|
||||
scores = get_scores(client1)
|
||||
|
||||
for entry in scores:
|
||||
assert entry["name"] != "user3"
|
||||
|
||||
user1 = Users.query.filter_by(name="user1").first()
|
||||
assert user1.place == "1st"
|
||||
|
||||
user2 = Users.query.filter_by(name="user2").first()
|
||||
assert user2.place == "2nd"
|
||||
destroy_ctfd(app)
|
||||
101
tests/users/test_settings.py
Normal file
101
tests/users/test_settings.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_user_set_profile():
|
||||
"""Test that a user can set and remove their information in their profile"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"confirm": "",
|
||||
"password": "",
|
||||
"affiliation": "affiliation_test",
|
||||
"website": "https://examplectf.com",
|
||||
"country": "US",
|
||||
}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.affiliation == data["affiliation"]
|
||||
assert user.website == data["website"]
|
||||
assert user.country == data["country"]
|
||||
|
||||
r = client.get("/settings")
|
||||
resp = r.get_data(as_text=True)
|
||||
for _k, v in data.items():
|
||||
assert v in resp
|
||||
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"confirm": "",
|
||||
"password": "",
|
||||
"affiliation": "",
|
||||
"website": "",
|
||||
"country": "",
|
||||
}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert user.affiliation == data["affiliation"]
|
||||
assert user.website == data["website"]
|
||||
assert user.country == data["country"]
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_can_change_password():
|
||||
"""Test that a user can change their password and is prompted properly"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"confirm": "",
|
||||
"password": "new_password",
|
||||
"affiliation": "",
|
||||
"website": "",
|
||||
"country": "",
|
||||
}
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert verify_password(data["password"], user.password) is False
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"errors": {"confirm": ["Please confirm your current password"]},
|
||||
"success": False,
|
||||
}
|
||||
|
||||
data["confirm"] = "wrong_password"
|
||||
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert verify_password(data["password"], user.password) is False
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"errors": {"confirm": ["Your previous password is incorrect"]},
|
||||
"success": False,
|
||||
}
|
||||
|
||||
data["confirm"] = "password"
|
||||
r = client.patch("/api/v1/users/me", json=data)
|
||||
assert r.status_code == 200
|
||||
user = Users.query.filter_by(id=2).first()
|
||||
assert verify_password(data["password"], user.password) is True
|
||||
destroy_ctfd(app)
|
||||
56
tests/users/test_setup.py
Normal file
56
tests/users/test_setup.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, gen_user
|
||||
|
||||
|
||||
def test_ctfd_setup_redirect():
|
||||
"""Test that a fresh CTFd instance redirects to /setup"""
|
||||
app = create_ctfd(setup=False)
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/users")
|
||||
assert r.status_code == 302
|
||||
assert r.location == "/setup"
|
||||
|
||||
# Files in /themes load properly
|
||||
r = client.get("/themes/core/static/manifest.json")
|
||||
r = client.get("/themes/core/static/img/favicon.ico")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_ctfd_setup_verification():
|
||||
app = create_ctfd(setup=False)
|
||||
with app.app_context():
|
||||
with app.test_client() as client:
|
||||
r = client.get("/setup")
|
||||
assert r.status_code == 200
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"ctf_name": "CTFd",
|
||||
"ctf_description": "CTF description",
|
||||
"name": "test",
|
||||
"email": "test@examplectf.com",
|
||||
"password": "",
|
||||
"user_mode": "users",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/setup", data=data)
|
||||
assert "longer password" in r.get_data(as_text=True)
|
||||
|
||||
gen_user(app.db, name="test", email="test@examplectf.com")
|
||||
|
||||
data["password"] = "password"
|
||||
r = client.post("/setup", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert "email has already been used" in resp
|
||||
assert "name is already taken" in resp
|
||||
|
||||
data["name"] = "admin"
|
||||
data["email"] = "admin@examplectf.com"
|
||||
r = client.post("/setup", data=data)
|
||||
assert r.status_code == 302
|
||||
assert r.location == "/"
|
||||
destroy_ctfd(app)
|
||||
105
tests/users/test_submissions.py
Normal file
105
tests/users/test_submissions.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||
|
||||
|
||||
def test_user_get_private_solves():
|
||||
"""Can a registered user load /api/v1/users/me/solves"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/me/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_public_solves():
|
||||
"""Can a registered user load /api/v1/users/1/solves"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/2/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_another_public_solves():
|
||||
"""Can a registered user load public solves page of another user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com") # ID 2
|
||||
register_user(app, name="user2", email="user2@examplectf.com") # ID 3
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/api/v1/users/2/solves")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_private_fails():
|
||||
"""Can a registered user load /api/v1/users/me/fails"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/me/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_public_fails():
|
||||
"""Can a registered user load /api/v1/users/2/fails"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/api/v1/users/2/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_another_public_fails():
|
||||
"""Can a registered user load public fails page of another user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com") # ID 2
|
||||
register_user(app, name="user2", email="user2@examplectf.com") # ID 3
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/api/v1/users/2/fails")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_public_team_page():
|
||||
"""Can a registered user load their public profile (/profile)"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/profile")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_another_public_team_page():
|
||||
"""Can a registered user load the public profile of another user (/users/1)"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user1@examplectf.com") # ID 2
|
||||
register_user(app, name="user2", email="user2@examplectf.com") # ID 3
|
||||
client = login_as_user(app, name="user2")
|
||||
r = client.get("/users/2")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_user_get_private_team_page():
|
||||
"""Can a registered user load their private team page /user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/user")
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
151
tests/users/test_users.py
Normal file
151
tests/users/test_users.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from CTFd.models import Users
|
||||
from CTFd.utils import set_config
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_award,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_accessing_hidden_users():
|
||||
"""Hidden users should not give any data from /users or /api/v1/users"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(
|
||||
app, name="visible_user", email="visible_user@examplectf.com"
|
||||
) # ID 2
|
||||
register_user(
|
||||
app, name="hidden_user", email="hidden_user@examplectf.com"
|
||||
) # ID 3
|
||||
register_user(
|
||||
app, name="banned_user", email="banned_user@examplectf.com"
|
||||
) # ID 4
|
||||
user = Users.query.filter_by(name="hidden_user").first()
|
||||
user.hidden = True
|
||||
app.db.session.commit()
|
||||
user = Users.query.filter_by(name="banned_user").first()
|
||||
user.banned = True
|
||||
app.db.session.commit()
|
||||
|
||||
with login_as_user(app, name="visible_user") as client:
|
||||
assert client.get("/users/3").status_code == 404
|
||||
assert client.get("/api/v1/users/3").status_code == 404
|
||||
assert client.get("/api/v1/users/3/solves").status_code == 404
|
||||
assert client.get("/api/v1/users/3/fails").status_code == 404
|
||||
assert client.get("/api/v1/users/3/awards").status_code == 404
|
||||
|
||||
assert client.get("/users/4").status_code == 404
|
||||
assert client.get("/api/v1/users/4").status_code == 404
|
||||
assert client.get("/api/v1/users/4/solves").status_code == 404
|
||||
assert client.get("/api/v1/users/4/fails").status_code == 404
|
||||
assert client.get("/api/v1/users/4/awards").status_code == 404
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_hidden_user_visibility():
|
||||
"""Hidden users should not show up on /users or /api/v1/users or /api/v1/scoreboard"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="hidden_user")
|
||||
|
||||
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()
|
||||
|
||||
r = client.get("/users")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert user_name not in response
|
||||
|
||||
r = client.get("/api/v1/users")
|
||||
response = r.get_json()
|
||||
assert user_name not in response
|
||||
|
||||
gen_award(app.db, user_id)
|
||||
|
||||
r = client.get("/scoreboard")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert user_name not in response
|
||||
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
response = r.get_json()
|
||||
assert user_name not in response
|
||||
|
||||
# User should re-appear after disabling hiding
|
||||
# Use an API call to cause a cache clear
|
||||
with login_as_user(app, name="admin") as admin:
|
||||
r = admin.patch("/api/v1/users/2", json={"hidden": False})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = client.get("/users")
|
||||
response = r.get_data(as_text=True)
|
||||
# Only search in body content
|
||||
body_start = response.find("<body>")
|
||||
body_end = response.find("</body>")
|
||||
response = response[body_start:body_end]
|
||||
assert user_name in response
|
||||
|
||||
r = client.get("/api/v1/users")
|
||||
response = r.get_data(as_text=True)
|
||||
assert user_name in response
|
||||
|
||||
r = client.get("/api/v1/scoreboard")
|
||||
response = r.get_data(as_text=True)
|
||||
assert user_name in response
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_num_users_limit():
|
||||
"""Only num_users users can be created"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config("num_users", 1)
|
||||
|
||||
register_user(app)
|
||||
with app.test_client() as client:
|
||||
r = client.get("/register")
|
||||
assert r.status_code == 403
|
||||
|
||||
# team should be blocked from creation
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user",
|
||||
"email": "user@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
# This number is 2 to account for the admin and the registered user
|
||||
assert Users.query.count() == 2
|
||||
assert "Reached the maximum number of users" in resp
|
||||
|
||||
# Can the team be created after the num has been bumped
|
||||
set_config("num_users", 2)
|
||||
with client.session_transaction() as sess:
|
||||
data = {
|
||||
"name": "user1",
|
||||
"email": "user1@examplectf.com",
|
||||
"password": "password",
|
||||
"nonce": sess.get("nonce"),
|
||||
}
|
||||
r = client.post("/register", data=data)
|
||||
resp = r.get_data(as_text=True)
|
||||
assert r.status_code == 302
|
||||
assert Users.query.count() == 3
|
||||
destroy_ctfd(app)
|
||||
Reference in New Issue
Block a user