Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancing async access for courses with registered students #533

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions dojo_plugin/api/v1/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import docker.types
from flask import abort, request, current_app
from flask_restx import Namespace, Resource
from CTFd.plugins import bypass_csrf_protection
from CTFd.exceptions import UserNotFoundException, UserTokenExpiredException
from CTFd.utils.user import get_current_user
from CTFd.utils.decorators import authed_only
Expand Down Expand Up @@ -336,20 +337,11 @@ def start_challenge(user, dojo_challenge, practice, *, as_user=None):


@docker_namespace.route("")
class RunDocker(Resource):
@authed_only
def post(self):
data = request.get_json()
dojo_id = data.get("dojo")
module_id = data.get("module")
challenge_id = data.get("challenge")
practice = data.get("practice")

user = get_current_user()
as_user = None
class RunDocker(Resource):

# https://github.com/CTFd/CTFd/blob/3.6.0/CTFd/utils/initialization/__init__.py#L286-L296
workspace_token = request.headers.get("X-Workspace-Token")
def launch_container(self, user, dojo_id, module_id, challenge_id, practice, workspace_token):
as_user = None
if workspace_token:
try:
token_user = lookup_workspace_token(workspace_token)
Expand Down Expand Up @@ -393,9 +385,34 @@ def post(self):
logger.exception(f"ERROR: Docker failed for {user.id}:")
return {"success": False, "error": "Docker failed"}
return {"success": True}

@authed_only
def post(self):
data = request.get_json()
dojo_id = data.get("dojo")
module_id = data.get("module")
challenge_id = data.get("challenge")
practice = data.get("practice")

user = get_current_user()
as_user = None

# https://github.com/CTFd/CTFd/blob/3.6.0/CTFd/utils/initialization/__init__.py#L286-L296
workspace_token = request.headers.get("X-Workspace-Token")

return self.launch_container(user, dojo_id, module_id, challenge_id, practice, workspace_token)

@authed_only
def get(self):
def get(self):
if request.args.get('dojo') and request.args.get('module') and request.args.get('challenge'):
user = get_current_user()
dojo_id = request.args.get("dojo")
module_id = request.args.get("module")
challenge_id = request.args.get("challenge")
workspace_token = request.args.get("X-Workspace-Token", None)
practice = request.args.get("practice", "false").lower() in ('true', '1','yes','y')
return self.launch_container(user, dojo_id, module_id, challenge_id, practice, workspace_token)

dojo_challenge = get_current_dojo_challenge()
if not dojo_challenge:
return {"success": False, "error": "No active challenge"}
Expand All @@ -405,3 +422,4 @@ def get(self):
"module": dojo_challenge.module.id,
"challenge": dojo_challenge.id,
}

3 changes: 2 additions & 1 deletion dojo_plugin/api/v1/workspace_tokens.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from flask import request
from flask_restx import Namespace, Resource
from CTFd.models import ma
Expand Down Expand Up @@ -51,4 +52,4 @@ def post(self):

if response.errors:
return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data}
return {"success": True, "data": response.data}
117 changes: 103 additions & 14 deletions dojo_plugin/pages/course.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import collections
import datetime
import re
import logging

from flask import Blueprint, Response, render_template, request, abort, stream_with_context
from sqlalchemy import and_, cast
Expand All @@ -9,8 +10,10 @@
from CTFd.utils.user import get_current_user, is_admin
from CTFd.utils.decorators import authed_only, admins_only, ratelimit

from ..models import DiscordUsers, DojoChallenges, DojoUsers, DojoStudents, DojoModules, DojoStudents
from ..utils import module_visible, module_challenges_visible, is_dojo_admin
from ..api.v1.workspace_tokens import WorkspaceTokenSchema

from ..models import DiscordUsers, DojoChallenges, DojoUsers, DojoStudents, DojoModules, DojoStudents, WorkspaceTokens
from ..utils import module_visible, module_challenges_visible, is_dojo_admin, generate_workspace_token
from ..utils.dojo import dojo_route
from ..utils.discord import add_role, get_discord_member
from .writeups import WriteupComments, writeup_weeks, all_writeups
Expand Down Expand Up @@ -47,6 +50,7 @@ def grade(dojo, users_query, *, ignore_pending=False):
continue
assessment_dates[assessment["id"]][assessment["type"]] = (
datetime.datetime.fromisoformat(assessment["date"]).astimezone(datetime.timezone.utc),
datetime.datetime.fromisoformat(assessment.get("extra_late_date","3000-01-01T16:59:59-07:00")).astimezone(datetime.timezone.utc),
assessment.get("extensions", {}),
)

Expand All @@ -57,12 +61,21 @@ def dated_count(label, date_type):
def query(module_id):
if date_type not in assessment_dates[module_id]:
return None
date, extensions = assessment_dates[module_id][date_type]
date, extra_late_date, extensions = assessment_dates[module_id][date_type]
if label == "extra_late_solves":
if extra_late_date is None:
return False
date = extra_late_date
user_date = db.case(
[(Solves.user_id == int(user_id), date + datetime.timedelta(days=days))
for user_id, days in extensions.items()],
else_=date
) if extensions else date
if label == "late_solves":

return and_(Solves.date >= user_date, Solves.date < extra_late_date)
elif label == "extra_late_solves":
return Solves.date >= user_date
return Solves.date < user_date
return db.func.sum(
db.case([(DojoModules.id == module_id, cast(query(module_id), db.Integer))
Expand All @@ -84,6 +97,8 @@ def query(module_id):
DojoModules.id.label("module_id"),
dated_count("checkpoint_solves", "checkpoint"),
dated_count("due_solves", "due"),
dated_count("late_solves", "due"),
dated_count("extra_late_solves", "due"),
dated_count("all_solves", None)
)
).subquery()
Expand All @@ -106,6 +121,8 @@ def result(user_id):
date = datetime.datetime.fromisoformat(assessment["date"]) if type in ["checkpoint", "due"] else None
if ignore_pending and date and date > now:
continue

extra_late_date = datetime.datetime.fromisoformat(assessment.get("extra_late_date",None)) if type in ["checkpoint", "due"] and "extra_late_date" in assessment else None

if type == "checkpoint":
module_id = assessment["id"]
Expand All @@ -114,7 +131,7 @@ def result(user_id):
extension = assessment.get("extensions", {}).get(str(user_id), 0)

challenge_count = challenge_counts[module_id]
checkpoint_solves, due_solves, all_solves = module_solves.get(module_id, (0, 0, 0))
checkpoint_solves, due_solves, late_solves, extra_late_solves, all_solves = module_solves.get(module_id, (0, 0, 0, 0, 0))
challenge_count_required = int(challenge_count * percent_required)
user_date = date + datetime.timedelta(days=extension)

Expand All @@ -131,32 +148,47 @@ def result(user_id):
weight = assessment["weight"]
percent_required = assessment.get("percent_required", 1.0)
late_penalty = assessment.get("late_penalty", 0.0)

extra_late_penalty = assessment.get("extra_late_penalty", 0.0)

extension = assessment.get("extensions", {}).get(str(user_id), 0)
override = assessment.get("overrides", {}).get(str(user_id), None)

challenge_count = challenge_counts[module_id]
checkpoint_solves, due_solves, all_solves = module_solves.get(module_id, (0, 0, 0))
late_solves = all_solves - due_solves
checkpoint_solves, due_solves, late_solves, extra_late_solves, all_solves = module_solves.get(module_id, (0, 0, 0, 0, 0))

challenge_count_required = int(challenge_count * percent_required)
user_date = date + datetime.timedelta(days=extension)
extra_late_user_date = None
if extra_late_date is not None:
extra_late_user_date = extra_late_date + datetime.timedelta(days=extension)
late_value = 1 - late_penalty
max_late_solves = challenge_count_required - min(due_solves, challenge_count_required)
capped_late_solves = min(max_late_solves, late_solves)
extra_late_value = 1 - extra_late_penalty

if not late_solves:
max_late_solves = challenge_count_required - min(due_solves, challenge_count_required)
capped_late_solves = min(max_late_solves, late_solves)
capped_extra_late_solves = min(max_late_solves-capped_late_solves, extra_late_solves)

if not late_solves and not extra_late_solves:
progress = f"{due_solves} / {challenge_count_required}"
else:
elif late_solves and not extra_late_solves:
progress = f"{due_solves} (+{late_solves}) / {challenge_count_required}"

elif not late_solves and extra_late_solves:
progress = f"{due_solves} (+{extra_late_solves}) / {challenge_count_required}"
else:
progress = f"{due_solves} (+{late_solves}) (+{extra_late_solves}) / {challenge_count_required}"
if override is None:
credit = min((due_solves + late_value * capped_late_solves) / challenge_count_required, 1.0)
late_points = late_value * capped_late_solves
extra_late_points = extra_late_value * capped_extra_late_solves
credit = min((due_solves + late_points + extra_late_points ) / challenge_count_required, 1.0)
else:
credit = override
progress = f"{progress} *"

assessment_grades.append(dict(
name=assessment_name(dojo, assessment),
date=str(user_date) + (" *" if extension else ""),
extra_late_date=str(extra_late_user_date) + (" *" if extension else ""),
weight=weight,
progress=progress,
credit=credit,
Expand Down Expand Up @@ -192,11 +224,12 @@ def result(user_id):
return dict(user_id=user_id,
assessment_grades=assessment_grades,
overall_grade=overall_grade,
letter_grade=letter_grade)
letter_grade=letter_grade,
show_extra_late_date= any(row.get('extra_late_date',None) is not None for row in assessments))

user_id = None
previous_user_id = None
for user_id, module_id, checkpoint_solves, due_solves, all_solves in user_solves:
for user_id, module_id, checkpoint_solves, due_solves, late_solves, extra_late_solves, all_solves in user_solves:
if user_id != previous_user_id:
if previous_user_id is not None:
yield result(previous_user_id)
Expand All @@ -206,6 +239,8 @@ def result(user_id):
module_solves[module_id] = (
int(checkpoint_solves) if checkpoint_solves is not None else 0,
int(due_solves) if due_solves is not None else 0,
int(late_solves) if late_solves is not None else 0,
int(extra_late_solves) if extra_late_solves is not None else 0,
int(all_solves) if all_solves is not None else 0,
)
if user_id:
Expand Down Expand Up @@ -439,3 +474,57 @@ def view_user_info(dojo, user_id):
user=user,
discord_member=discord_member,
**identity)


@course.route("/dojo/<dojo>/admin/create_tokens")
@dojo_route
@authed_only
def create_tokens_for_course_members(dojo):
if not dojo.course:
abort(404)

if not dojo.is_admin():
abort(403)
log = logging.getLogger(__name__)

course_students = dojo.course.get("students", [])
users = (
db.session.query(Users,DojoStudents.token)
.join(DojoStudents, DojoStudents.user_id == Users.id)
.filter(DojoStudents.dojo == dojo,
DojoStudents.token.in_(course_students))
)
out = []
import json
for user, user_extra_id in users:
log.info(f"{user.id} {user_extra_id}")
dojo_access_token = None
tokens = WorkspaceTokens.query.filter_by(user_id=user.id)
schema = WorkspaceTokenSchema(only=["id", "expiration","value"], many=True)
response = schema.dump(tokens)
if response.errors:
log.error(f"Error with token getting token info for {user.id}: {user.name}, will attempt to create a new token")
else:
for tok in response.data:
if tok.get('value','').startswith(dojo.id):
dojo_access_token = tok
break

if dojo_access_token is None:
current_date = datetime.now()

# TODO: should make more sophisticated, maybe based on a course value like course visibility if it's there, maybe visibility + 30 days or something?
august_10 = datetime(current_date.year, 8, 10)
december_31 = datetime(current_date.year, 12, 31)
if current_date < august_10:
expiration_date = august_10
else:
expiration_date = december_31

newly_generated_token = generate_workspace_token(user, expiration=expiration_date, preface=f"{dojo.id}_ws_")
out.append({"user_id": user.id, "user_extra_id": user_extra_id, "user_name": user.name,
"access_token": newly_generated_token.value, "access_expiration": newly_generated_token.expiration})
else:
out.append({"user_id": user.id, "user_extra_id": user_extra_id, "user_name": user.name,
"access_token": dojo_access_token.get('value'), "access_expiration": dojo_access_token.get('expiration')})
return out
9 changes: 4 additions & 5 deletions dojo_plugin/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,18 +296,17 @@ def lookup_workspace_token(token):


# https://github.com/CTFd/CTFd/blob/3.6.0/CTFd/utils/security/auth.py#L37-L48
def generate_workspace_token(user, expiration=None):
temp_token = True
def generate_workspace_token(user, expiration=None, preface="workspace_"):
temp_token = True
while temp_token is not None:
value = "workspace_" + hexencode(os.urandom(32))
value = preface + hexencode(os.urandom(32))
temp_token = WorkspaceTokens.query.filter_by(value=value).first()

token = WorkspaceTokens(
user_id=user.id, expiration=expiration, value=value
)
db.session.add(token)
db.session.commit()
return token
return token


# based on https://stackoverflow.com/questions/36408496/python-logging-handler-to-append-to-list
Expand Down
19 changes: 19 additions & 0 deletions dojo_theme/static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,25 @@ pre {
padding: 0px 0px 0px 0px;
}


@media (min-width: 1440px) {
.container {
max-width: 1400px;
}
}
@media (min-width: 1660px) {
.container {
max-width: 1620px;
}
}
@media (min-width: 1980px) {
.container {
max-width: 1940px;
}
}



code{
color: #61afef;
}
Expand Down
8 changes: 7 additions & 1 deletion dojo_theme/templates/course.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ <h3>{{ name }} current grade in the class: <code style="font-size: 2em">{{ lette
<thead>
<tr>
<td scope="col"><b>Name</b></td>
<td scope="col"><b>Date</b></td>
<td scope="col"><b>Date</b></td>
{% if show_extra_late_date %}
<td scope="col"><b>Extra Late Date</b></td>
{% endif %}
<td scope="col"><b>Weight</b></td>
<td scope="col"><b>Progress</b></td>
<td scope="col"><b>Credit</b></td>
Expand All @@ -79,6 +82,9 @@ <h3>{{ name }} current grade in the class: <code style="font-size: 2em">{{ lette
<tr>
<td>{{ assessment_grade.name }}</td>
<td>{{ assessment_grade.date }}</td>
{% if show_extra_late_date %}
<td>{{ assessment_grade.extra_late_date }}</td>
{% endif %}
<td>{{ assessment_grade.weight }}</td>
<td>{{ assessment_grade.progress }}</td>
<td>{{ credit }}</td>
Expand Down
6 changes: 6 additions & 0 deletions dojo_theme/templates/grades_admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ <h1>All Grades</h1>
<tr>
<td>Name</td>
<td>Date</td>
{% if user_grades.show_extra_late_date %}
<td>Extra Late After</td>
{% endif %}
<td>Weight</td>
<td>Progress</td>
<td>Credit</td>
Expand All @@ -52,6 +55,9 @@ <h1>All Grades</h1>
<tr>
<td>{{ assessment_grade.name }}</td>
<td>{{ assessment_grade.date }}</td>
{% if user_grades.show_extra_late_date %}
<td>{{ assessment_grade.extra_late_date }} </td>
{% endif %}
<td>{{ assessment_grade.weight }}</td>
<td>{{ assessment_grade.progress }}</td>
<td>
Expand Down
Loading