From af1c32537131f74858828fd5c5159683a4b09d9f Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Mon, 23 Nov 2020 02:35:46 -0500 Subject: [PATCH] Improved Team Handling (#1713) * Prevent team joining while already on a team * Return 403 instead of 200 for team join/create errors * Allow team captains whose teams haven't done anything to disband their team * Closes #1588 --- CTFd/api/v1/teams.py | 56 +++++++++ CTFd/teams.py | 23 +++- .../core/assets/js/pages/teams/private.js | 28 ++++- .../core/static/js/pages/teams/private.dev.js | 2 +- .../core/static/js/pages/teams/private.min.js | 2 +- CTFd/themes/core/templates/teams/private.html | 11 ++ tests/api/v1/test_teams.py | 73 ++++++++++++ tests/teams/test_auth.py | 106 ++++++++++++++++++ tests/teams/test_teams.py | 34 ------ 9 files changed, 294 insertions(+), 41 deletions(-) diff --git a/CTFd/api/v1/teams.py b/CTFd/api/v1/teams.py index 243dde6e..23cc8081 100644 --- a/CTFd/api/v1/teams.py +++ b/CTFd/api/v1/teams.py @@ -306,6 +306,62 @@ class TeamPrivate(Resource): return {"success": True, "data": response.data} + @authed_only + @require_team + @teams_namespace.doc( + description="Endpoint to disband your current team. Can only be used if the team has performed no actions in the CTF.", + responses={200: ("Success", "APISimpleSuccessResponse")}, + ) + def delete(self): + team = get_current_team() + if team.captain_id != session["id"]: + return ( + { + "success": False, + "errors": {"": ["Only team captains can disband their team"]}, + }, + 403, + ) + + # The team must not have performed any actions in the CTF + performed_actions = any( + [ + team.solves != [], + team.fails != [], + team.awards != [], + Submissions.query.filter_by(team_id=team.id).all() != [], + Unlocks.query.filter_by(team_id=team.id).all() != [], + ] + ) + + if performed_actions: + return ( + { + "success": False, + "errors": { + "": [ + "You cannot disband your team as it has participated in the event. " + "Please contact an admin to disband your team or remove a member." + ] + }, + }, + 403, + ) + + for member in team.members: + member.team_id = None + clear_user_session(user_id=member.id) + + db.session.delete(team) + db.session.commit() + + clear_team_session(team_id=team.id) + clear_standings() + + db.session.close() + + return {"success": True} + @teams_namespace.route("//members") @teams_namespace.param("team_id", "Team ID") diff --git a/CTFd/teams.py b/CTFd/teams.py index fecbd874..0189e733 100644 --- a/CTFd/teams.py +++ b/CTFd/teams.py @@ -11,7 +11,7 @@ from CTFd.utils.decorators.visibility import ( check_score_visibility, ) from CTFd.utils.helpers import get_errors, get_infos -from CTFd.utils.user import get_current_user +from CTFd.utils.user import get_current_user, get_current_user_attrs teams = Blueprint("teams", __name__) @@ -57,6 +57,11 @@ def listing(): def join(): infos = get_infos() errors = get_errors() + + user = get_current_user_attrs() + if user.team_id: + errors.append("You are already in a team. You cannot join another.") + if request.method == "GET": team_size_limit = get_config("team_size", default=0) if team_size_limit: @@ -74,6 +79,12 @@ def join(): team = Teams.query.filter_by(name=teamname).first() + if errors: + return ( + render_template("teams/join_team.html", infos=infos, errors=errors), + 403, + ) + if team and verify_password(passphrase, team.password): team_size_limit = get_config("team_size", default=0) if team_size_limit and len(team.members) >= team_size_limit: @@ -109,6 +120,11 @@ def join(): def new(): infos = get_infos() errors = get_errors() + + user = get_current_user_attrs() + if user.team_id: + errors.append("You are already in a team. You cannot join another.") + if request.method == "GET": team_size_limit = get_config("team_size", default=0) if team_size_limit: @@ -118,12 +134,11 @@ def new(): limit=team_size_limit, plural=plural ) ) - return render_template("teams/new_team.html", infos=infos, errors=errors) + elif request.method == "POST": teamname = request.form.get("name", "").strip() passphrase = request.form.get("password", "").strip() - errors = get_errors() website = request.form.get("website") affiliation = request.form.get("affiliation") @@ -177,7 +192,7 @@ def new(): errors.append("Please provide a shorter affiliation") if errors: - return render_template("teams/new_team.html", errors=errors) + return render_template("teams/new_team.html", errors=errors), 403 team = Teams(name=teamname, password=passphrase, captain_id=user.id) diff --git a/CTFd/themes/core/assets/js/pages/teams/private.js b/CTFd/themes/core/assets/js/pages/teams/private.js index 57439a1b..f8166c59 100644 --- a/CTFd/themes/core/assets/js/pages/teams/private.js +++ b/CTFd/themes/core/assets/js/pages/teams/private.js @@ -3,7 +3,7 @@ import "../../utils"; import CTFd from "../../CTFd"; import "bootstrap/js/dist/modal"; import $ from "jquery"; -import { ezBadge } from "../../ezq"; +import { ezBadge, ezQuery, ezAlert } from "../../ezq"; $(() => { if (window.team_captain) { @@ -14,6 +14,32 @@ $(() => { $(".edit-captain").click(function() { $("#team-captain-modal").modal(); }); + + $(".disband-team").click(function() { + ezQuery({ + title: "Disband Team", + body: "Are you sure you want to disband your team?", + success: function() { + CTFd.fetch("/api/v1/teams/me", { + method: "DELETE" + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { + if (response.success) { + window.location.reload(); + } else { + ezAlert({ + title: "Error", + body: response.errors[""].join(" "), + button: "Got it!" + }); + } + }); + } + }); + }); } let form = $("#team-info-form"); diff --git a/CTFd/themes/core/static/js/pages/teams/private.dev.js b/CTFd/themes/core/static/js/pages/teams/private.dev.js index 28c5b4c1..a12e95b1 100644 --- a/CTFd/themes/core/static/js/pages/teams/private.dev.js +++ b/CTFd/themes/core/static/js/pages/teams/private.dev.js @@ -162,7 +162,7 @@ /***/ (function(module, exports, __webpack_require__) { ; -eval("\n\n__webpack_require__(/*! ../main */ \"./CTFd/themes/core/assets/js/pages/main.js\");\n\n__webpack_require__(/*! ../../utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! ../../CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\n__webpack_require__(/*! bootstrap/js/dist/modal */ \"./node_modules/bootstrap/js/dist/modal.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! ../../ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n(0, _jquery[\"default\"])(function () {\n if (window.team_captain) {\n (0, _jquery[\"default\"])(\".edit-team\").click(function () {\n (0, _jquery[\"default\"])(\"#team-edit-modal\").modal();\n });\n (0, _jquery[\"default\"])(\".edit-captain\").click(function () {\n (0, _jquery[\"default\"])(\"#team-captain-modal\").modal();\n });\n }\n\n var form = (0, _jquery[\"default\"])(\"#team-info-form\");\n form.submit(function (e) {\n e.preventDefault();\n (0, _jquery[\"default\"])(\"#results\").empty();\n var params = (0, _jquery[\"default\"])(this).serializeJSON();\n params.fields = [];\n\n for (var property in params) {\n if (property.match(/fields\\[\\d+\\]/)) {\n var field = {};\n var id = parseInt(property.slice(7, -1));\n field[\"field_id\"] = id;\n field[\"value\"] = params[property];\n params.fields.push(field);\n delete params[property];\n }\n }\n\n var method = \"PATCH\";\n var url = \"/api/v1/teams/me\";\n\n _CTFd[\"default\"].fetch(url, {\n method: method,\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n if (response.status === 400) {\n response.json().then(function (object) {\n if (!object.success) {\n var error_template = '
\\n' + ' Error:\\n' + \" {0}\\n\" + ' \\n' + \"
\";\n Object.keys(object.errors).map(function (error) {\n var i = form.find(\"input[name={0}]\".format(error));\n var input = (0, _jquery[\"default\"])(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n var error_msg = object.errors[error];\n var alert = error_template.format(error_msg);\n (0, _jquery[\"default\"])(\"#results\").append(alert);\n });\n }\n });\n } else if (response.status === 200) {\n response.json().then(function (object) {\n if (object.success) {\n window.location.reload();\n }\n });\n }\n });\n });\n (0, _jquery[\"default\"])(\"#team-captain-form\").submit(function (e) {\n e.preventDefault();\n var params = (0, _jquery[\"default\"])(\"#team-captain-form\").serializeJSON(true);\n\n _CTFd[\"default\"].fetch(\"/api/v1/teams/me\", {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _jquery[\"default\"])(\"#team-captain-form > #results\").empty();\n Object.keys(response.errors).forEach(function (key, _index) {\n (0, _jquery[\"default\"])(\"#team-captain-form > #results\").append((0, _ezq.ezBadge)({\n type: \"error\",\n body: response.errors[key]\n }));\n var i = (0, _jquery[\"default\"])(\"#team-captain-form\").find(\"select[name={0}]\".format(key));\n var input = (0, _jquery[\"default\"])(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n });\n }\n });\n });\n});\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/pages/teams/private.js?"); +eval("\n\n__webpack_require__(/*! ../main */ \"./CTFd/themes/core/assets/js/pages/main.js\");\n\n__webpack_require__(/*! ../../utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! ../../CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\n__webpack_require__(/*! bootstrap/js/dist/modal */ \"./node_modules/bootstrap/js/dist/modal.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! ../../ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n(0, _jquery[\"default\"])(function () {\n if (window.team_captain) {\n (0, _jquery[\"default\"])(\".edit-team\").click(function () {\n (0, _jquery[\"default\"])(\"#team-edit-modal\").modal();\n });\n (0, _jquery[\"default\"])(\".edit-captain\").click(function () {\n (0, _jquery[\"default\"])(\"#team-captain-modal\").modal();\n });\n (0, _jquery[\"default\"])(\".disband-team\").click(function () {\n (0, _ezq.ezQuery)({\n title: \"Disband Team\",\n body: \"Are you sure you want to disband your team?\",\n success: function success() {\n _CTFd[\"default\"].fetch(\"/api/v1/teams/me\", {\n method: \"DELETE\"\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _ezq.ezAlert)({\n title: \"Error\",\n body: response.errors[\"\"].join(\" \"),\n button: \"Got it!\"\n });\n }\n });\n }\n });\n });\n }\n\n var form = (0, _jquery[\"default\"])(\"#team-info-form\");\n form.submit(function (e) {\n e.preventDefault();\n (0, _jquery[\"default\"])(\"#results\").empty();\n var params = (0, _jquery[\"default\"])(this).serializeJSON();\n params.fields = [];\n\n for (var property in params) {\n if (property.match(/fields\\[\\d+\\]/)) {\n var field = {};\n var id = parseInt(property.slice(7, -1));\n field[\"field_id\"] = id;\n field[\"value\"] = params[property];\n params.fields.push(field);\n delete params[property];\n }\n }\n\n var method = \"PATCH\";\n var url = \"/api/v1/teams/me\";\n\n _CTFd[\"default\"].fetch(url, {\n method: method,\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n if (response.status === 400) {\n response.json().then(function (object) {\n if (!object.success) {\n var error_template = '
\\n' + ' Error:\\n' + \" {0}\\n\" + ' \\n' + \"
\";\n Object.keys(object.errors).map(function (error) {\n var i = form.find(\"input[name={0}]\".format(error));\n var input = (0, _jquery[\"default\"])(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n var error_msg = object.errors[error];\n var alert = error_template.format(error_msg);\n (0, _jquery[\"default\"])(\"#results\").append(alert);\n });\n }\n });\n } else if (response.status === 200) {\n response.json().then(function (object) {\n if (object.success) {\n window.location.reload();\n }\n });\n }\n });\n });\n (0, _jquery[\"default\"])(\"#team-captain-form\").submit(function (e) {\n e.preventDefault();\n var params = (0, _jquery[\"default\"])(\"#team-captain-form\").serializeJSON(true);\n\n _CTFd[\"default\"].fetch(\"/api/v1/teams/me\", {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _jquery[\"default\"])(\"#team-captain-form > #results\").empty();\n Object.keys(response.errors).forEach(function (key, _index) {\n (0, _jquery[\"default\"])(\"#team-captain-form > #results\").append((0, _ezq.ezBadge)({\n type: \"error\",\n body: response.errors[key]\n }));\n var i = (0, _jquery[\"default\"])(\"#team-captain-form\").find(\"select[name={0}]\".format(key));\n var input = (0, _jquery[\"default\"])(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n });\n }\n });\n });\n});\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/pages/teams/private.js?"); /***/ }) diff --git a/CTFd/themes/core/static/js/pages/teams/private.min.js b/CTFd/themes/core/static/js/pages/teams/private.min.js index 6ab96e02..828ebc3a 100644 --- a/CTFd/themes/core/static/js/pages/teams/private.min.js +++ b/CTFd/themes/core/static/js/pages/teams/private.min.js @@ -1 +1 @@ -!function(l){function e(e){for(var t,o,n=e[0],s=e[1],a=e[2],i=0,r=[];i".concat(e.body,"

")):o.find(".modal-body").append((0,r.default)(e.body));var n=(0,r.default)(c.format(e.button));return e.success&&(0,r.default)(n).click(function(){e.success()}),e.large&&o.find(".modal-dialog").addClass("modal-lg"),o.find(".modal-footer").append(n),(0,r.default)("main").append(o),o.modal("show"),(0,r.default)(o).on("hidden.bs.modal",function(){(0,r.default)(this).modal("dispose")}),o}function f(e){(0,r.default)("#ezq--notifications-toast-container").length||(0,r.default)("body").append((0,r.default)("
").attr({id:"ezq--notifications-toast-container"}).css({position:"fixed",bottom:"0",right:"0","min-width":"20%"}));var t,o=l.format(e.title,e.body),n=(0,r.default)(o);e.onclose&&(0,r.default)(n).find("button[data-dismiss=toast]").click(function(){e.onclose()}),e.onclick&&((t=(0,r.default)(n).find(".toast-body")).addClass("cursor-pointer"),t.click(function(){e.onclick()}));var s=!1!==e.autohide,a=!1!==e.animation,i=e.delay||1e4;return(0,r.default)("#ezq--notifications-toast-container").prepend(n),n.toast({autohide:s,delay:i,animation:a}),n.toast("show"),n}function j(e){var t=a.format(e.title),o=(0,r.default)(t);"string"==typeof e.body?o.find(".modal-body").append("

".concat(e.body,"

")):o.find(".modal-body").append((0,r.default)(e.body));var n=(0,r.default)(u),s=(0,r.default)(m);return o.find(".modal-footer").append(s),o.find(".modal-footer").append(n),(0,r.default)("main").append(o),(0,r.default)(o).on("hidden.bs.modal",function(){(0,r.default)(this).modal("dispose")}),(0,r.default)(n).click(function(){e.success()}),o.modal("show"),o}function _(e){if(e.target){var t=(0,r.default)(e.target);return t.find(".progress-bar").css("width",e.width+"%"),t}var o=i.format(e.width),n=a.format(e.title),s=(0,r.default)(n);return s.find(".modal-body").append((0,r.default)(o)),(0,r.default)("main").append(s),s.modal("show")}function h(e){var t={success:d,error:s}[e.type].format(e.body);return(0,r.default)(t)}var v={ezAlert:p,ezToast:f,ezQuery:j,ezProgressBar:_,ezBadge:h};t.default=v},"./CTFd/themes/core/assets/js/fetch.js":function(e,t,o){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0,o("./node_modules/whatwg-fetch/fetch.js");var n,s=(n=o("./CTFd/themes/core/assets/js/config.js"))&&n.__esModule?n:{default:n};var a=window.fetch;t.default=function(e,t){return void 0===t&&(t={method:"GET",credentials:"same-origin",headers:{}}),e=s.default.urlRoot+e,void 0===t.headers&&(t.headers={}),t.credentials="same-origin",t.headers.Accept="application/json",t.headers["Content-Type"]="application/json",t.headers["CSRF-Token"]=s.default.csrfNonce,a(e,t)}},"./CTFd/themes/core/assets/js/pages/main.js":function(e,t,o){var n=p(o("./CTFd/themes/core/assets/js/CTFd.js")),s=p(o("./node_modules/jquery/dist/jquery.js")),a=p(o("./node_modules/moment/moment.js")),i=p(o("./node_modules/nunjucks/browser/nunjucks.js")),r=o("./node_modules/howler/dist/howler.js"),l=p(o("./CTFd/themes/core/assets/js/events.js")),d=p(o("./CTFd/themes/core/assets/js/config.js")),c=p(o("./CTFd/themes/core/assets/js/styles.js")),m=p(o("./CTFd/themes/core/assets/js/times.js")),u=p(o("./CTFd/themes/core/assets/js/helpers.js"));function p(e){return e&&e.__esModule?e:{default:e}}n.default.init(window.init),window.CTFd=n.default,window.helpers=u.default,window.$=s.default,window.Moment=a.default,window.nunjucks=i.default,window.Howl=r.Howl,(0,s.default)(function(){(0,c.default)(),(0,m.default)(),(0,l.default)(d.default.urlRoot)})},"./CTFd/themes/core/assets/js/pages/teams/private.js":function(e,t,o){o("./CTFd/themes/core/assets/js/pages/main.js"),o("./CTFd/themes/core/assets/js/utils.js");var a=n(o("./CTFd/themes/core/assets/js/CTFd.js"));o("./node_modules/bootstrap/js/dist/modal.js");var l=n(o("./node_modules/jquery/dist/jquery.js")),i=o("./CTFd/themes/core/assets/js/ezq.js");function n(e){return e&&e.__esModule?e:{default:e}}(0,l.default)(function(){window.team_captain&&((0,l.default)(".edit-team").click(function(){(0,l.default)("#team-edit-modal").modal()}),(0,l.default)(".edit-captain").click(function(){(0,l.default)("#team-captain-modal").modal()}));var r=(0,l.default)("#team-info-form");r.submit(function(e){e.preventDefault(),(0,l.default)("#results").empty();var t,o,n,s=(0,l.default)(this).serializeJSON();for(t in s.fields=[],s){t.match(/fields\[\d+\]/)&&(o={},n=parseInt(t.slice(7,-1)),o.field_id=n,o.value=s[t],s.fields.push(o),delete s[t])}a.default.fetch("/api/v1/teams/me",{method:"PATCH",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(s)}).then(function(e){400===e.status?e.json().then(function(a){var i;a.success||(i='',Object.keys(a.errors).map(function(e){var t=r.find("input[name={0}]".format(e)),o=(0,l.default)(t);o.addClass("input-filled-invalid"),o.removeClass("input-filled-valid");var n=a.errors[e],s=i.format(n);(0,l.default)("#results").append(s)}))}):200===e.status&&e.json().then(function(e){e.success&&window.location.reload()})})}),(0,l.default)("#team-captain-form").submit(function(e){e.preventDefault();var t=(0,l.default)("#team-captain-form").serializeJSON(!0);a.default.fetch("/api/v1/teams/me",{method:"PATCH",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(t)}).then(function(e){return e.json()}).then(function(s){s.success?window.location.reload():((0,l.default)("#team-captain-form > #results").empty(),Object.keys(s.errors).forEach(function(e,t){(0,l.default)("#team-captain-form > #results").append((0,i.ezBadge)({type:"error",body:s.errors[e]}));var o=(0,l.default)("#team-captain-form").find("select[name={0}]".format(e)),n=(0,l.default)(o);n.addClass("input-filled-invalid"),n.removeClass("input-filled-valid")}))})})})},"./CTFd/themes/core/assets/js/patch.js":function(e,t,o){var n,r=(n=o("./node_modules/q/q.js"))&&n.__esModule?n:{default:n},s=o("./CTFd/themes/core/assets/js/api.js");function i(t,e){var o,n=Object.keys(t);return Object.getOwnPropertySymbols&&(o=Object.getOwnPropertySymbols(t),e&&(o=o.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),n.push.apply(n,o)),n}function a(s){for(var e=1;e>4*s&255).toString(16)).substr(-2)}return n},t.htmlEntities=function(e){return(0,i.default)("
").text(e).html()},t.cumulativeSum=function(e){for(var t=e.concat(),o=0;o'),(0,i.default)("th.sort-col").click(function(){var s,e=(0,i.default)(this).parents("table").eq(0),t=e.find("tr:gt(0)").toArray().sort((s=(0,i.default)(this).index(),function(e,t){var o=a(e,s),n=a(t,s);return i.default.isNumeric(o)&&i.default.isNumeric(n)?o-n:o.toString().localeCompare(n)}));this.asc=!this.asc,this.asc||(t=t.reverse());for(var o=0;o".concat(e.body,"

")):o.find(".modal-body").append((0,r.default)(e.body));var n=(0,r.default)(c.format(e.button));return e.success&&(0,r.default)(n).click(function(){e.success()}),e.large&&o.find(".modal-dialog").addClass("modal-lg"),o.find(".modal-footer").append(n),(0,r.default)("main").append(o),o.modal("show"),(0,r.default)(o).on("hidden.bs.modal",function(){(0,r.default)(this).modal("dispose")}),o}function f(e){(0,r.default)("#ezq--notifications-toast-container").length||(0,r.default)("body").append((0,r.default)("
").attr({id:"ezq--notifications-toast-container"}).css({position:"fixed",bottom:"0",right:"0","min-width":"20%"}));var t,o=l.format(e.title,e.body),n=(0,r.default)(o);e.onclose&&(0,r.default)(n).find("button[data-dismiss=toast]").click(function(){e.onclose()}),e.onclick&&((t=(0,r.default)(n).find(".toast-body")).addClass("cursor-pointer"),t.click(function(){e.onclick()}));var s=!1!==e.autohide,a=!1!==e.animation,i=e.delay||1e4;return(0,r.default)("#ezq--notifications-toast-container").prepend(n),n.toast({autohide:s,delay:i,animation:a}),n.toast("show"),n}function j(e){var t=a.format(e.title),o=(0,r.default)(t);"string"==typeof e.body?o.find(".modal-body").append("

".concat(e.body,"

")):o.find(".modal-body").append((0,r.default)(e.body));var n=(0,r.default)(u),s=(0,r.default)(m);return o.find(".modal-footer").append(s),o.find(".modal-footer").append(n),(0,r.default)("main").append(o),(0,r.default)(o).on("hidden.bs.modal",function(){(0,r.default)(this).modal("dispose")}),(0,r.default)(n).click(function(){e.success()}),o.modal("show"),o}function _(e){if(e.target){var t=(0,r.default)(e.target);return t.find(".progress-bar").css("width",e.width+"%"),t}var o=i.format(e.width),n=a.format(e.title),s=(0,r.default)(n);return s.find(".modal-body").append((0,r.default)(o)),(0,r.default)("main").append(s),s.modal("show")}function h(e){var t={success:d,error:s}[e.type].format(e.body);return(0,r.default)(t)}var v={ezAlert:p,ezToast:f,ezQuery:j,ezProgressBar:_,ezBadge:h};t.default=v},"./CTFd/themes/core/assets/js/fetch.js":function(e,t,o){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0,o("./node_modules/whatwg-fetch/fetch.js");var n,s=(n=o("./CTFd/themes/core/assets/js/config.js"))&&n.__esModule?n:{default:n};var a=window.fetch;t.default=function(e,t){return void 0===t&&(t={method:"GET",credentials:"same-origin",headers:{}}),e=s.default.urlRoot+e,void 0===t.headers&&(t.headers={}),t.credentials="same-origin",t.headers.Accept="application/json",t.headers["Content-Type"]="application/json",t.headers["CSRF-Token"]=s.default.csrfNonce,a(e,t)}},"./CTFd/themes/core/assets/js/pages/main.js":function(e,t,o){var n=p(o("./CTFd/themes/core/assets/js/CTFd.js")),s=p(o("./node_modules/jquery/dist/jquery.js")),a=p(o("./node_modules/moment/moment.js")),i=p(o("./node_modules/nunjucks/browser/nunjucks.js")),r=o("./node_modules/howler/dist/howler.js"),l=p(o("./CTFd/themes/core/assets/js/events.js")),d=p(o("./CTFd/themes/core/assets/js/config.js")),c=p(o("./CTFd/themes/core/assets/js/styles.js")),m=p(o("./CTFd/themes/core/assets/js/times.js")),u=p(o("./CTFd/themes/core/assets/js/helpers.js"));function p(e){return e&&e.__esModule?e:{default:e}}n.default.init(window.init),window.CTFd=n.default,window.helpers=u.default,window.$=s.default,window.Moment=a.default,window.nunjucks=i.default,window.Howl=r.Howl,(0,s.default)(function(){(0,c.default)(),(0,m.default)(),(0,l.default)(d.default.urlRoot)})},"./CTFd/themes/core/assets/js/pages/teams/private.js":function(e,t,o){o("./CTFd/themes/core/assets/js/pages/main.js"),o("./CTFd/themes/core/assets/js/utils.js");var a=n(o("./CTFd/themes/core/assets/js/CTFd.js"));o("./node_modules/bootstrap/js/dist/modal.js");var l=n(o("./node_modules/jquery/dist/jquery.js")),i=o("./CTFd/themes/core/assets/js/ezq.js");function n(e){return e&&e.__esModule?e:{default:e}}(0,l.default)(function(){window.team_captain&&((0,l.default)(".edit-team").click(function(){(0,l.default)("#team-edit-modal").modal()}),(0,l.default)(".edit-captain").click(function(){(0,l.default)("#team-captain-modal").modal()}),(0,l.default)(".disband-team").click(function(){(0,i.ezQuery)({title:"Disband Team",body:"Are you sure you want to disband your team?",success:function(){a.default.fetch("/api/v1/teams/me",{method:"DELETE"}).then(function(e){return e.json()}).then(function(e){e.success?window.location.reload():(0,i.ezAlert)({title:"Error",body:e.errors[""].join(" "),button:"Got it!"})})}})}));var r=(0,l.default)("#team-info-form");r.submit(function(e){e.preventDefault(),(0,l.default)("#results").empty();var t,o,n,s=(0,l.default)(this).serializeJSON();for(t in s.fields=[],s){t.match(/fields\[\d+\]/)&&(o={},n=parseInt(t.slice(7,-1)),o.field_id=n,o.value=s[t],s.fields.push(o),delete s[t])}a.default.fetch("/api/v1/teams/me",{method:"PATCH",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(s)}).then(function(e){400===e.status?e.json().then(function(a){var i;a.success||(i='',Object.keys(a.errors).map(function(e){var t=r.find("input[name={0}]".format(e)),o=(0,l.default)(t);o.addClass("input-filled-invalid"),o.removeClass("input-filled-valid");var n=a.errors[e],s=i.format(n);(0,l.default)("#results").append(s)}))}):200===e.status&&e.json().then(function(e){e.success&&window.location.reload()})})}),(0,l.default)("#team-captain-form").submit(function(e){e.preventDefault();var t=(0,l.default)("#team-captain-form").serializeJSON(!0);a.default.fetch("/api/v1/teams/me",{method:"PATCH",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(t)}).then(function(e){return e.json()}).then(function(s){s.success?window.location.reload():((0,l.default)("#team-captain-form > #results").empty(),Object.keys(s.errors).forEach(function(e,t){(0,l.default)("#team-captain-form > #results").append((0,i.ezBadge)({type:"error",body:s.errors[e]}));var o=(0,l.default)("#team-captain-form").find("select[name={0}]".format(e)),n=(0,l.default)(o);n.addClass("input-filled-invalid"),n.removeClass("input-filled-valid")}))})})})},"./CTFd/themes/core/assets/js/patch.js":function(e,t,o){var n,r=(n=o("./node_modules/q/q.js"))&&n.__esModule?n:{default:n},s=o("./CTFd/themes/core/assets/js/api.js");function i(t,e){var o,n=Object.keys(t);return Object.getOwnPropertySymbols&&(o=Object.getOwnPropertySymbols(t),e&&(o=o.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),n.push.apply(n,o)),n}function a(s){for(var e=1;e>4*s&255).toString(16)).substr(-2)}return n},t.htmlEntities=function(e){return(0,i.default)("
").text(e).html()},t.cumulativeSum=function(e){for(var t=e.concat(),o=0;o'),(0,i.default)("th.sort-col").click(function(){var s,e=(0,i.default)(this).parents("table").eq(0),t=e.find("tr:gt(0)").toArray().sort((s=(0,i.default)(this).index(),function(e,t){var o=a(e,s),n=a(t,s);return i.default.isNumeric(o)&&i.default.isNumeric(n)?o-n:o.toString().localeCompare(n)}));this.asc=!this.asc,this.asc||(t=t.reverse());for(var o=0;o + + + + {% else %} + + + {% endif %} {% if team.website and (team.website.startswith('http://') or team.website.startswith('https://')) %} diff --git a/tests/api/v1/test_teams.py b/tests/api/v1/test_teams.py index 09b05cef..eac683d1 100644 --- a/tests/api/v1/test_teams.py +++ b/tests/api/v1/test_teams.py @@ -695,6 +695,79 @@ def test_api_team_patch_password(): ) +def test_api_team_captain_disbanding(): + """Test that only team captains can disband teams""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + user = gen_user(app.db, name="user") + team = gen_team(app.db) + team.members.append(user) + user.team_id = team.id + team.captain_id = 2 + user2 = gen_user(app.db, name="user2", email="user2@ctfd.io") + team.members.append(user2) + app.db.session.commit() + with login_as_user(app, name="user2") as client: + r = client.delete("/api/v1/teams/me", json="") + assert r.status_code == 403 + assert r.get_json() == { + "success": False, + "errors": {"": ["Only team captains can disband their team"]}, + } + with login_as_user(app) as client: + r = client.delete("/api/v1/teams/me", json="") + assert r.status_code == 200 + assert r.get_json() == { + "success": True, + } + destroy_ctfd(app) + + +def test_api_team_captain_disbanding_only_inactive_teams(): + """Test that only teams that haven't conducted any actions can be disbanded""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + user = gen_user(app.db, name="user") + team = gen_team(app.db) + team.members.append(user) + user.team_id = team.id + team.captain_id = 2 + user2 = gen_user(app.db, name="user2", email="user2@ctfd.io") + team.members.append(user2) + app.db.session.commit() + + gen_challenge(app.db) + gen_flag(app.db, 1) + gen_solve(app.db, user_id=3, team_id=1, challenge_id=1) + + with login_as_user(app) as client: + r = client.delete("/api/v1/teams/me", json="") + assert r.status_code == 403 + assert r.get_json() == { + "success": False, + "errors": { + "": [ + "You cannot disband your team as it has participated in the event. " + "Please contact an admin to disband your team or remove a member." + ] + }, + } + + user = gen_user(app.db, name="user3", email="user3@ctfd.io") + team = gen_team(app.db, name="team2", email="team2@ctfd.io") + print(user.id) + team.members.append(user) + user.team_id = team.id + team.captain_id = user.id + app.db.session.commit() + with login_as_user(app, name="user3") as client: + r = client.delete("/api/v1/teams/me", json="") + print(r.get_json()) + assert r.status_code == 200 + assert r.get_json() == {"success": True} + destroy_ctfd(app) + + def test_api_accessing_hidden_banned_users(): """Hidden/Banned users should not be visible to normal users, only to admins""" app = create_ctfd(user_mode="teams") diff --git a/tests/teams/test_auth.py b/tests/teams/test_auth.py index c0b310ce..67d46b44 100644 --- a/tests/teams/test_auth.py +++ b/tests/teams/test_auth.py @@ -58,10 +58,47 @@ def test_teams_join_post(): } r = client.post("/teams/join", data=data) assert r.status_code == 302 + + # Cannot join a team with an incorrect password incorrect_data = data incorrect_data["password"] = "" r = client.post("/teams/join", data=incorrect_data) + assert r.status_code == 403 + destroy_ctfd(app) + + +def test_teams_join_when_already_on_team(): + """Test that a user cannot join another team""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + gen_user(app.db, name="user") + gen_team(app.db, email="team1@ctfd.io", name="team1") + gen_team(app.db, email="team2@ctfd.io", name="team2") + with login_as_user(app) as client: + r = client.get("/teams/join") assert r.status_code == 200 + with client.session_transaction() as sess: + data = { + "name": "team1", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/teams/join", data=data) + assert r.status_code == 302 + + # Try to join another team while on a team + r = client.get("/teams/join") + assert r.status_code == 200 + with client.session_transaction() as sess: + data = { + "name": "team2", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/teams/join", data=data) + assert r.status_code == 403 + user = Users.query.filter_by(name="user").first() + assert user.team.name == "team1" destroy_ctfd(app) @@ -104,3 +141,72 @@ def test_team_join_ratelimited(): assert r.status_code == 429 assert Users.query.filter_by(id=2).first().team_id is None destroy_ctfd(app) + + +def test_teams_new_get(): + """Can a user get /teams/new""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + register_user(app) + with login_as_user(app) as client: + r = client.get("/teams/new") + assert r.status_code == 200 + destroy_ctfd(app) + + +def test_teams_new_post(): + """Can a user post /teams/new""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + gen_user(app.db, name="user") + with login_as_user(app) as client: + with client.session_transaction() as sess: + data = { + "name": "team", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/teams/new", data=data) + assert r.status_code == 302 + + # You can't create a team with a duplicate name + r = client.post("/teams/new", data=data) + assert r.status_code == 403 + + # You can't create a team with an empty name + incorrect_data = data + incorrect_data["name"] = "" + r = client.post("/teams/new", data=incorrect_data) + assert r.status_code == 403 + destroy_ctfd(app) + + +def test_teams_new_post_when_already_on_team(): + """Test that a user cannot create a new team while on a team""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + gen_user(app.db, name="user") + with login_as_user(app) as client: + with client.session_transaction() as sess: + data = { + "name": "team1", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/teams/new", data=data) + assert r.status_code == 302 + + # Try to create another team while on a team + r = client.get("/teams/new") + assert r.status_code == 200 + with client.session_transaction() as sess: + data = { + "name": "team2", + "password": "password", + "nonce": sess.get("nonce"), + } + r = client.post("/teams/join", data=data) + assert r.status_code == 403 + user = Users.query.filter_by(name="user").first() + assert user.team.name == "team1" + destroy_ctfd(app) diff --git a/tests/teams/test_teams.py b/tests/teams/test_teams.py index 27d34349..eb09e762 100644 --- a/tests/teams/test_teams.py +++ b/tests/teams/test_teams.py @@ -116,40 +116,6 @@ def test_teams_get_user_mode(): destroy_ctfd(app) -def test_teams_new_get(): - """Can a user get /teams/new""" - app = create_ctfd(user_mode="teams") - with app.app_context(): - register_user(app) - with login_as_user(app) as client: - r = client.get("/teams/new") - assert r.status_code == 200 - destroy_ctfd(app) - - -def test_teams_new_post(): - """Can a user post /teams/new""" - app = create_ctfd(user_mode="teams") - with app.app_context(): - gen_user(app.db, name="user") - with login_as_user(app) as client: - with client.session_transaction() as sess: - data = { - "name": "team", - "password": "password", - "nonce": sess.get("nonce"), - } - r = client.post("/teams/new", data=data) - assert r.status_code == 302 - r = client.post("/teams/new", data=data) - assert r.status_code == 200 - incorrect_data = data - incorrect_data["name"] = "" - r = client.post("/teams/new", data=incorrect_data) - assert r.status_code == 200 - destroy_ctfd(app) - - def test_team_get(): """Can a user get /team""" app = create_ctfd(user_mode="teams")