init CTFd source
This commit is contained in:
450
tests/test_config.py
Normal file
450
tests/test_config.py
Normal file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from werkzeug.exceptions import SecurityError
|
||||
|
||||
from CTFd.config import TestingConfig
|
||||
from CTFd.models import Configs, Users, db
|
||||
from CTFd.utils import get_config
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from tests.helpers import (
|
||||
create_ctfd,
|
||||
destroy_ctfd,
|
||||
gen_user,
|
||||
login_as_user,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def test_reverse_proxy_config():
|
||||
"""Test that REVERSE_PROXY configuration behaves properly"""
|
||||
|
||||
class ReverseProxyConfig(TestingConfig):
|
||||
REVERSE_PROXY = "1,2,3,4"
|
||||
|
||||
app = create_ctfd(config=ReverseProxyConfig)
|
||||
with app.app_context():
|
||||
assert app.wsgi_app.x_for == 1
|
||||
assert app.wsgi_app.x_proto == 2
|
||||
assert app.wsgi_app.x_host == 3
|
||||
assert app.wsgi_app.x_port == 4
|
||||
assert app.wsgi_app.x_prefix == 0
|
||||
destroy_ctfd(app)
|
||||
|
||||
class ReverseProxyConfig(TestingConfig):
|
||||
REVERSE_PROXY = "true"
|
||||
|
||||
app = create_ctfd(config=ReverseProxyConfig)
|
||||
with app.app_context():
|
||||
assert app.wsgi_app.x_for == 1
|
||||
assert app.wsgi_app.x_proto == 1
|
||||
assert app.wsgi_app.x_host == 1
|
||||
assert app.wsgi_app.x_port == 1
|
||||
assert app.wsgi_app.x_prefix == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
class ReverseProxyConfig(TestingConfig):
|
||||
REVERSE_PROXY = True
|
||||
|
||||
app = create_ctfd(config=ReverseProxyConfig)
|
||||
with app.app_context():
|
||||
assert app.wsgi_app.x_for == 1
|
||||
assert app.wsgi_app.x_proto == 1
|
||||
assert app.wsgi_app.x_host == 1
|
||||
assert app.wsgi_app.x_port == 1
|
||||
assert app.wsgi_app.x_prefix == 1
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_server_sent_events_config():
|
||||
"""Test that SERVER_SENT_EVENTS configuration behaves properly"""
|
||||
|
||||
class ServerSentEventsConfig(TestingConfig):
|
||||
SERVER_SENT_EVENTS = False
|
||||
|
||||
app = create_ctfd(config=ServerSentEventsConfig)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/events")
|
||||
assert r.status_code == 204
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_trusted_hosts_config():
|
||||
"""Test that TRUSTED_HOSTS configuration behaves properly"""
|
||||
|
||||
class TrustedHostsConfig(TestingConfig):
|
||||
SERVER_NAME = "example.com"
|
||||
TRUSTED_HOSTS = ["example.com"]
|
||||
|
||||
app = create_ctfd(config=TrustedHostsConfig)
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
client = login_as_user(app)
|
||||
r = client.get("/", headers={"Host": "example.com"})
|
||||
assert r.status_code == 200
|
||||
|
||||
# TODO: We need to allow either a 500 or a 400 because Flask-RestX
|
||||
# seems to be overriding Flask's error handler
|
||||
try:
|
||||
r = client.get("/", headers={"Host": "evil.com"})
|
||||
except SecurityError:
|
||||
pass
|
||||
else:
|
||||
if r.status_code != 400:
|
||||
raise SecurityError("Responded to untrusted request")
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_config():
|
||||
"""Test that PRESET_ADMIN configuration allows login and creates admin user"""
|
||||
|
||||
class PresetAdminConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_admin"
|
||||
PRESET_ADMIN_EMAIL = "preset@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "preset_password_123"
|
||||
|
||||
app = create_ctfd(config=PresetAdminConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_admin").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Attempt login with incorrect preset admin credentials via name
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin", "password": "wrong_preset_password_123"}
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
assert r.status_code == 200
|
||||
assert "incorrect" in r.get_data(as_text=True)
|
||||
preset_admin = Users.query.filter_by(name="preset_admin").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Attempt login with preset admin credentials via name
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin", "password": "preset_password_123"}
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
|
||||
# Should redirect to challenges page after successful login
|
||||
assert r.status_code == 302
|
||||
assert "/challenges" in r.location or r.location.endswith("/")
|
||||
|
||||
# Verify admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_admin").first()
|
||||
assert preset_admin is not None
|
||||
assert preset_admin.email == "preset@example.com"
|
||||
assert preset_admin.type == "admin"
|
||||
assert preset_admin.verified is True
|
||||
|
||||
assert verify_password("preset_password_123", preset_admin.password) is True
|
||||
|
||||
# Test login via email as well
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset@example.com", "password": "preset_password_123"}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
assert r.status_code == 302
|
||||
|
||||
# Test that wrong password fails
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin", "password": "wrong_password"}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
# Should return login page with error, not redirect
|
||||
assert r.status_code == 200
|
||||
assert b"incorrect" in r.data.lower() or b"invalid" in r.data.lower()
|
||||
|
||||
assert Users.query.filter_by(type="admin").count() == 2
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_token_config():
|
||||
"""Test that PRESET_ADMIN_TOKEN allows API access and creates admin user"""
|
||||
|
||||
class PresetAdminTokenConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_token_admin"
|
||||
PRESET_ADMIN_EMAIL = "preset_token@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "preset_token_password_123"
|
||||
PRESET_ADMIN_TOKEN = "preset_secret_token_12345"
|
||||
|
||||
app = create_ctfd(config=PresetAdminTokenConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_token_admin").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Test that wrong token fails
|
||||
client = app.test_client()
|
||||
wrong_headers = {
|
||||
"Authorization": "Token wrong_token_123",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=wrong_headers)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
# Test API access without authentication (should fail)
|
||||
client = app.test_client()
|
||||
r = client.get("/api/v1/users/me", json=True)
|
||||
assert r.status_code in [401, 403] # Unauthorized or Forbidden
|
||||
|
||||
# Test API access with preset admin token
|
||||
headers = {
|
||||
"Authorization": "Token preset_secret_token_12345",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=headers, json=True)
|
||||
|
||||
# Should succeed and create the admin user
|
||||
assert r.status_code == 200
|
||||
|
||||
# Verify admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_token_admin").first()
|
||||
assert preset_admin is not None
|
||||
assert preset_admin.email == "preset_token@example.com"
|
||||
assert preset_admin.type == "admin"
|
||||
assert preset_admin.verified is True
|
||||
|
||||
assert (
|
||||
verify_password("preset_token_password_123", preset_admin.password) is True
|
||||
)
|
||||
|
||||
# Verify the API response contains the admin user information
|
||||
response_data = r.get_json()
|
||||
|
||||
assert response_data["success"] is True
|
||||
assert response_data["data"]["name"] == "preset_token_admin"
|
||||
assert response_data["data"]["email"] == "preset_token@example.com"
|
||||
|
||||
# Check that we are admin
|
||||
r = client.get("/api/v1/challenges/types", headers=headers, json=True)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Test that wrong token fails
|
||||
wrong_headers = {
|
||||
"Authorization": "Token wrong_token_123",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=wrong_headers, json=True)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
assert Users.query.filter_by(type="admin").count() == 2
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_no_promotion_existing_user():
|
||||
"""Test that existing regular users with preset credentials don't get promoted to admin"""
|
||||
|
||||
class PresetAdminConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "existing_user"
|
||||
PRESET_ADMIN_EMAIL = "existing@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "preset_password_123"
|
||||
|
||||
app = create_ctfd(config=PresetAdminConfig)
|
||||
with app.app_context():
|
||||
# Create a regular user with the same credentials as preset admin
|
||||
gen_user(
|
||||
app.db,
|
||||
name="existing_user",
|
||||
email="existing@example.com",
|
||||
password="preset_password_123",
|
||||
type="user", # Regular user, not admin
|
||||
)
|
||||
|
||||
# Verify user exists and is not admin
|
||||
user = Users.query.filter_by(name="existing_user").first()
|
||||
assert user is not None
|
||||
assert user.type == "user"
|
||||
assert user.email == "existing@example.com"
|
||||
|
||||
# Attempt login with preset admin credentials
|
||||
client = app.test_client()
|
||||
login_data = {"name": "existing_user", "password": "preset_password_123"}
|
||||
# Get login page first to get nonce
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
|
||||
# Should fail in logging in
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
"Preset admin user could not be created. Please contact an administrator"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
|
||||
# Test that the user is not an admin
|
||||
r = client.get("/api/v1/challenges/types", json=True)
|
||||
assert r.status_code in [
|
||||
401,
|
||||
403,
|
||||
] # Should be unauthorized/forbidden for regular user
|
||||
|
||||
# Verify user is still not admin (no promotion)
|
||||
user = Users.query.filter_by(name="existing_user").first()
|
||||
assert user is not None
|
||||
assert user.type == "user" # Should still be regular user
|
||||
assert user.email == "existing@example.com"
|
||||
|
||||
# Also test login via email
|
||||
client = app.test_client()
|
||||
login_data = {"name": "existing@example.com", "password": "preset_password_123"}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
# Should fail in logging in
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
"Preset admin user could not be created. Please contact an administrator"
|
||||
in r.get_data(as_text=True)
|
||||
)
|
||||
|
||||
# Test that the user is not an admin
|
||||
r = client.get("/api/v1/challenges/types", json=True)
|
||||
assert r.status_code in [
|
||||
401,
|
||||
403,
|
||||
] # Should be unauthorized/forbidden for regular user
|
||||
|
||||
# User should still be regular user after email login
|
||||
user = Users.query.filter_by(name="existing_user").first()
|
||||
assert user.type == "user"
|
||||
|
||||
# Verify only one admin exists (the default setup admin)
|
||||
admin_count = Users.query.filter_by(type="admin").count()
|
||||
assert admin_count == 1
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_admin_empty_credentials():
|
||||
"""Test that empty preset credentials don't allow login"""
|
||||
|
||||
# Test empty password
|
||||
class PresetAdminEmptyPasswordConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_admin_empty"
|
||||
PRESET_ADMIN_EMAIL = "preset_empty@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "" # Empty password
|
||||
|
||||
app = create_ctfd(config=PresetAdminEmptyPasswordConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Attempt login with empty password (should fail)
|
||||
client = app.test_client()
|
||||
login_data = {"name": "preset_admin_empty", "password": ""}
|
||||
client.get("/login")
|
||||
with client.session_transaction() as sess:
|
||||
login_data["nonce"] = sess.get("nonce")
|
||||
r = client.post("/login", data=login_data)
|
||||
|
||||
# Should not create user or allow login
|
||||
assert r.status_code == 200
|
||||
assert "incorrect" in r.get_data(as_text=True).lower()
|
||||
|
||||
# Verify no admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty").first()
|
||||
assert preset_admin is None
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
# Test empty token
|
||||
class PresetAdminEmptyTokenConfig(TestingConfig):
|
||||
PRESET_ADMIN_NAME = "preset_admin_empty_token"
|
||||
PRESET_ADMIN_EMAIL = "preset_empty_token@example.com"
|
||||
PRESET_ADMIN_PASSWORD = "some_password"
|
||||
PRESET_ADMIN_TOKEN = "" # Empty token
|
||||
|
||||
app = create_ctfd(config=PresetAdminEmptyTokenConfig)
|
||||
with app.app_context():
|
||||
# Verify no preset admin exists initially
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty_token").first()
|
||||
assert preset_admin is None
|
||||
|
||||
# Test API access with empty token (should fail)
|
||||
client = app.test_client()
|
||||
empty_headers = {
|
||||
"Authorization": "Token ",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
r = client.get("/api/v1/users/me", headers=empty_headers)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
# Test API access without Authorization header (should also fail)
|
||||
r = client.get("/api/v1/users/me", json=True)
|
||||
assert r.status_code in [401, 403] # Should be unauthorized
|
||||
|
||||
# Verify no admin user was created
|
||||
preset_admin = Users.query.filter_by(name="preset_admin_empty_token").first()
|
||||
assert preset_admin is None
|
||||
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_preset_configs():
|
||||
"""Test that PRESET_CONFIGS are loaded and accessible via get_config"""
|
||||
|
||||
class PresetConfigsConfig(TestingConfig):
|
||||
PRESET_CONFIGS = {
|
||||
"ctf_name": "Test CTF Name",
|
||||
"ctf_description": "This is a test CTF description",
|
||||
"user_mode": "users",
|
||||
"challenge_visibility": "public",
|
||||
"registration_visibility": "public",
|
||||
"score_visibility": "public",
|
||||
"account_visibility": "public",
|
||||
"custom_setting": "custom_value_123",
|
||||
}
|
||||
|
||||
app = create_ctfd(config=PresetConfigsConfig)
|
||||
with app.app_context():
|
||||
# Test that preset configs are accessible via get_config
|
||||
assert get_config("ctf_name") == "Test CTF Name"
|
||||
assert get_config("ctf_description") == "This is a test CTF description"
|
||||
assert get_config("user_mode") == "users"
|
||||
assert get_config("challenge_visibility") == "public"
|
||||
assert get_config("registration_visibility") == "public"
|
||||
assert get_config("score_visibility") == "public"
|
||||
assert get_config("account_visibility") == "public"
|
||||
assert get_config("custom_setting") == "custom_value_123"
|
||||
|
||||
# Test that non-existent config returns None (or default)
|
||||
assert get_config("non_existent_config") is None
|
||||
assert (
|
||||
get_config("non_existent_config", default="default_value")
|
||||
== "default_value"
|
||||
)
|
||||
|
||||
# Add a database config that conflicts with a preset config
|
||||
db_config = Configs(key="ctf_name", value="Database CTF Name")
|
||||
db.session.add(db_config)
|
||||
db.session.commit()
|
||||
|
||||
# The preset config should still take precedence (not overridden by database)
|
||||
assert get_config("ctf_name") == "Test CTF Name"
|
||||
|
||||
# Test that database configs work for keys not in PRESET_CONFIGS
|
||||
db_config_new = Configs(key="database_only_setting", value="database_value")
|
||||
db.session.add(db_config_new)
|
||||
db.session.commit()
|
||||
|
||||
# This should come from the database since it's not in presets
|
||||
assert get_config("database_only_setting") == "database_value"
|
||||
|
||||
destroy_ctfd(app)
|
||||
Reference in New Issue
Block a user