Skip to content

Commit

Permalink
UI for promoting dojo members to dojo admins (#300)
Browse files Browse the repository at this point in the history
* support promoting dojo members to admins

* add confirmation functionality to form_fetch_and_show

* yolo test?

* require the user to be a dojo member before promoting to a dojo admin

* fix text issues

* print

* allow people to join official dojos

* adapt to non-tty DB output

* assert failure when appropriate

* checking

* remove debug print

* add debug print

* is the csrf token going stale somehow?

* Revert "is the csrf token going stale somehow?"

This reverts commit e42bc52.

* Revert "add debug print"

This reverts commit 7f98c56.

* Revert "checking"

This reverts commit 72e576b.

* json?

* split out the promotion test from the join test

* now what

* join before promotion

* user module-scoped user for join and promote, instead of creating two users

* fix get_user_id
  • Loading branch information
zardus authored Jan 22, 2024
1 parent 5300bb2 commit 4244055
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 8 deletions.
21 changes: 20 additions & 1 deletion dojo_plugin/api/v1/dojo.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from CTFd.utils.modes import get_model
from CTFd.utils.security.sanitize import sanitize_html

from ...models import Dojos, DojoMembers, DojoAdmins
from ...models import Dojos, DojoMembers, DojoAdmins, DojoUsers
from ...utils.dojo import dojo_accessible, dojo_clone, load_dojo_dir, dojo_route


Expand Down Expand Up @@ -75,6 +75,25 @@ def create_dojo(user, repository, public_key, private_key):

return {"success": True, "dojo": dojo.reference_id}

@dojo_namespace.route("/<dojo>/promote-admin")
class PromoteAdmin(Resource):
@authed_only
@dojo_route
def post(self, dojo):
data = request.get_json()
if 'user_id' not in data:
return {"success": False, "error": "User not specified."}, 400
new_admin_id = data['user_id']
user = get_current_user()
if not dojo.is_admin(user):
return {"success": False, "error": "Requestor is not a dojo admin."}, 403
u = DojoUsers.query.filter_by(dojo=dojo, user_id=new_admin_id).first()
if u:
u.type = 'admin'
else:
return {"success": False, "error": "User is not currently a dojo member."}, 400
db.session.commit()
return {"success": True}

@dojo_namespace.route("/create")
class CreateDojo(Resource):
Expand Down
3 changes: 0 additions & 3 deletions dojo_plugin/pages/dojos.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@ def join_dojo(dojo, password=None):
if not dojo:
abort(404)

if dojo.official:
return redirect(url_for("pwncollege_dojo.listing", dojo=dojo.reference_id))

if dojo.password and dojo.password != password:
abort(403)

Expand Down
8 changes: 7 additions & 1 deletion dojo_theme/static/js/dojo/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ var success_template =
' <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>\n' +
'</div>';

function form_fetch_and_show(name, endpoint, method, success_message) {
function form_fetch_and_show(name, endpoint, method, success_message, confirm_msg=null) {
const form = $(`#${name}-form`);
const results = $(`#${name}-results`);
form.submit(e => {
e.preventDefault();
results.empty();
const params = form.serializeJSON();

if (confirm_msg && !confirm(confirm_msg(form, params))) return;

CTFd.fetch(endpoint, {
method: method,
credentials: "same-origin",
Expand All @@ -45,6 +47,10 @@ function form_fetch_and_show(name, endpoint, method, success_message) {
$(() => {
form_fetch_and_show("ssh-key", "/pwncollege_api/v1/ssh_key", "PATCH", "Your public key has been updated");
form_fetch_and_show("dojo-create", "/pwncollege_api/v1/dojo/create", "POST", "Your dojo has been created");
form_fetch_and_show("dojo-promote-admin", `/pwncollege_api/v1/dojo/${init.dojo}/promote-admin`, "POST", "User has been promoted to admin.", confirm_msg = (form, params) => {
var user_name = form.find(`#name-for-${params["user_id"]}`)
return `Promote ${user_name.text()} (UID ${params["user_id"]}) to admin?`;
});

$(".copy-button").click((event) => {
let input = $(event.target).parents(".input-group").children("input")[0];
Expand Down
20 changes: 17 additions & 3 deletions dojo_theme/templates/dojo_admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,25 @@ <h2>{{ dojo.name }}</h2>
{% endfor %}
</ul>
<b style="font-family: 'Courier New', Courier, monospace">Members</b>
<ul>
<form method="post" id="dojo-promote-admin-form" autocomplete="off">
<div class="form-group">
{% for member in dojo.members %}
<li>{{ member.user.name }}</li>
<input type="radio" id="{{ member.user.id }}" name="user_id" value="{{ member.user.id }}">
<label for="{{ member.user.id }}"><a href="{{ url_for('pwncollege_users.view_other', user_id=member.user.id) }}"><span id="name-for-{{member.user.id}}">{{ member.user.name }}</span></a></label>
<br>
{% endfor %}
</ul>
<input class="btn btn-mini btn-primary" id="_submit" name="_submit" type="submit" value="Promote to Admin">
</div>
<div id="dojo-promote-admin-results" class="form-group">
</div>
</form>
</div>
{% endblock %}

{% block entrypoint %}
<script defer src="{{ url_for('views.themes', path='js/pages/settings.js') }}"></script>
{% endblock %}

{% block scripts %}
<script defer src="{{ url_for('views.themes', path='js/dojo/settings.js') }}"></script>
{% endblock %}
28 changes: 28 additions & 0 deletions test/test_running.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def start_challenge(dojo, module, challenge, practice=False, *, session):
assert response.status_code == 200, f"Expected status code 200, but got {response.status_code}"
assert response.json()["success"], f"Failed to start challenge: {response.json()['error']}"

def get_user_id(user_name):
sql = f"SELECT id FROM users WHERE name = '{user_name}'"
db_result = dojo_run("db", input=sql)
return int(db_result.stdout.split()[1])

@pytest.fixture
def admin_session():
Expand All @@ -65,6 +69,13 @@ def random_user():
yield random_id, session


@pytest.fixture(scope="module")
def singleton_user():
random_id = "".join(random.choices(string.ascii_lowercase, k=16))
session = login(random_id, random_id, register=True)
yield random_id, session


@pytest.mark.parametrize("endpoint", ["/", "/dojos", "/login", "/register"])
def test_unauthenticated_return_200(endpoint):
response = requests.get(f"{PROTO}://{HOST}{endpoint}")
Expand Down Expand Up @@ -116,6 +127,23 @@ def test_create_import_dojo(admin_session):
def test_start_challenge(admin_session):
start_challenge("example", "hello", "apple", session=admin_session)

@pytest.mark.dependency(depends=["test_create_dojo"])
def test_join_dojo(admin_session, singleton_user):
random_user_name, random_session = singleton_user
response = random_session.get(f"{PROTO}://{HOST}/dojo/example/join/")
assert response.status_code == 200
response = admin_session.get(f"{PROTO}://{HOST}/dojo/example/admin/")
assert response.status_code == 200
assert random_user_name in response.text and response.text.index("Members") < response.text.index(random_user_name)

@pytest.mark.dependency(depends=["test_join_dojo"])
def test_promote_dojo_member(admin_session, singleton_user):
random_user_name, _ = singleton_user
random_user_id = get_user_id(random_user_name)
response = admin_session.post(f"{PROTO}://{HOST}/pwncollege_api/v1/dojo/example/promote-admin", json={"user_id": random_user_id})
assert response.status_code == 200
response = admin_session.get(f"{PROTO}://{HOST}/dojo/example/admin/")
assert random_user_name in response.text and response.text.index("Members") > response.text.index(random_user_name)

@pytest.mark.dependency(depends=["test_start_challenge"])
@pytest.mark.parametrize("path", ["/flag", "/challenge/apple"])
Expand Down

0 comments on commit 4244055

Please sign in to comment.