mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-18 22:44:24 +01:00
Add Marshmallow handling of fields and add permissions for fields
This commit is contained in:
@@ -305,11 +305,11 @@ class UserPrivate(Resource):
|
|||||||
if response.errors:
|
if response.errors:
|
||||||
return {"success": False, "errors": response.errors}, 400
|
return {"success": False, "errors": response.errors}, 400
|
||||||
|
|
||||||
from CTFd.models import FieldEntries
|
# from CTFd.models import FieldEntries
|
||||||
fields = data.get("fields")
|
# fields = data.get("fields")
|
||||||
for field_id, value in fields.items():
|
# for field_id, value in fields.items():
|
||||||
e = FieldEntries.query.filter_by(field_id=field_id, user_id=user.id).first()
|
# e = FieldEntries.query.filter_by(field_id=field_id, user_id=user.id).first()
|
||||||
e.value = value
|
# e.value = value
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -289,8 +289,11 @@ def register():
|
|||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
from CTFd.models import FieldEntries
|
from CTFd.models import FieldEntries
|
||||||
|
|
||||||
for field_id, value in entries.items():
|
for field_id, value in entries.items():
|
||||||
entry = FieldEntries(field_id=field_id, value=value, user_id=user.id)
|
entry = FieldEntries(
|
||||||
|
field_id=field_id, value=value, user_id=user.id
|
||||||
|
)
|
||||||
db.session.add(entry)
|
db.session.add(entry)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from wtforms.fields.html5 import DateField, URLField
|
|||||||
|
|
||||||
from CTFd.forms import BaseForm
|
from CTFd.forms import BaseForm
|
||||||
from CTFd.forms.fields import SubmitField
|
from CTFd.forms.fields import SubmitField
|
||||||
|
from CTFd.models import FieldEntries, Fields
|
||||||
from CTFd.utils.countries import SELECT_COUNTRIES_LIST
|
from CTFd.utils.countries import SELECT_COUNTRIES_LIST
|
||||||
from CTFd.models import Fields, FieldEntries
|
|
||||||
|
|
||||||
|
|
||||||
def SettingsForm(*args, **kwargs):
|
def SettingsForm(*args, **kwargs):
|
||||||
|
|||||||
@@ -276,7 +276,9 @@ class Users(db.Model):
|
|||||||
# Relationship for Teams
|
# Relationship for Teams
|
||||||
team_id = db.Column(db.Integer, db.ForeignKey("teams.id"))
|
team_id = db.Column(db.Integer, db.ForeignKey("teams.id"))
|
||||||
|
|
||||||
fields = db.relationship("FieldEntries", foreign_keys="FieldEntries.user_id", lazy="select")
|
fields = db.relationship(
|
||||||
|
"FieldEntries", foreign_keys="FieldEntries.user_id", lazy="joined"
|
||||||
|
)
|
||||||
|
|
||||||
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
|||||||
24
CTFd/schemas/fields.py
Normal file
24
CTFd/schemas/fields.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from marshmallow import fields, pre_load
|
||||||
|
|
||||||
|
from CTFd.models import FieldEntries, Fields, db, ma
|
||||||
|
from CTFd.utils.user import get_current_user, is_admin
|
||||||
|
|
||||||
|
|
||||||
|
class FieldSchema(ma.ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = Fields
|
||||||
|
include_fk = True
|
||||||
|
dump_only = ("id",)
|
||||||
|
|
||||||
|
|
||||||
|
class FieldEntriesSchema(ma.ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = FieldEntries
|
||||||
|
include_fk = True
|
||||||
|
load_only = ("id", )
|
||||||
|
exclude = ("field", "user", "user_id")
|
||||||
|
dump_only = ("user_id", "name", "description", "type")
|
||||||
|
|
||||||
|
name = fields.Nested(FieldSchema, only=("name"), attribute="field")
|
||||||
|
description = fields.Nested(FieldSchema, only=("description"), attribute="field")
|
||||||
|
type = fields.Nested(FieldSchema, only=("field_type"), attribute="field")
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from marshmallow import ValidationError, pre_load, validate
|
from marshmallow import ValidationError, pre_load, pre_dump, validate
|
||||||
|
from marshmallow.fields import Nested
|
||||||
from marshmallow_sqlalchemy import field_for
|
from marshmallow_sqlalchemy import field_for
|
||||||
|
|
||||||
from CTFd.models import Users, ma
|
from CTFd.models import Fields, FieldEntries, Users, ma
|
||||||
|
from CTFd.schemas.fields import FieldEntriesSchema
|
||||||
from CTFd.utils import get_config, string_types
|
from CTFd.utils import get_config, string_types
|
||||||
from CTFd.utils.crypto import verify_password
|
from CTFd.utils.crypto import verify_password
|
||||||
from CTFd.utils.email import check_email_is_whitelisted
|
from CTFd.utils.email import check_email_is_whitelisted
|
||||||
@@ -49,6 +51,7 @@ class UserSchema(ma.ModelSchema):
|
|||||||
)
|
)
|
||||||
country = field_for(Users, "country", validate=[validate_country_code])
|
country = field_for(Users, "country", validate=[validate_country_code])
|
||||||
password = field_for(Users, "password")
|
password = field_for(Users, "password")
|
||||||
|
fields = Nested(FieldEntriesSchema(), partial=True, many=True)
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_name(self, data):
|
def validate_name(self, data):
|
||||||
@@ -180,6 +183,48 @@ class UserSchema(ma.ModelSchema):
|
|||||||
data.pop("password", None)
|
data.pop("password", None)
|
||||||
data.pop("confirm", None)
|
data.pop("confirm", None)
|
||||||
|
|
||||||
|
@pre_load
|
||||||
|
def validate_fields(self, data):
|
||||||
|
"""
|
||||||
|
This validator is used to only allow users to update the field entry for their user.
|
||||||
|
It's not possible to exclude it because without the PK Marshmallow cannot load the right instance
|
||||||
|
"""
|
||||||
|
current_user = get_current_user()
|
||||||
|
fields = data.get("fields")
|
||||||
|
|
||||||
|
if is_admin():
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for f in fields:
|
||||||
|
# Remove any existing set
|
||||||
|
f.pop("id", None)
|
||||||
|
field_id = f.get("field_id")
|
||||||
|
|
||||||
|
# # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce
|
||||||
|
# field = Fields.query.filter_by(id=field_id).first_or_404()
|
||||||
|
|
||||||
|
# Get the existing field entry if one exists
|
||||||
|
entry = FieldEntries.query.filter_by(field_id=field.id, user_id=current_user.id).first()
|
||||||
|
if entry:
|
||||||
|
f["id"] = entry.id
|
||||||
|
|
||||||
|
@pre_dump
|
||||||
|
def process_fields(self, obj):
|
||||||
|
"""
|
||||||
|
Handle permissions levels for fields.
|
||||||
|
|
||||||
|
Admins can see all fields.
|
||||||
|
Users (self) can see their edittable and public fields
|
||||||
|
Public (user) can only see public fields
|
||||||
|
"""
|
||||||
|
for i, entry in enumerate(obj.fields):
|
||||||
|
if self.view == "user":
|
||||||
|
if entry.field.public is False:
|
||||||
|
del obj.fields[i]
|
||||||
|
elif self.view == "self":
|
||||||
|
if entry.field.editable is False and entry.field.public is False:
|
||||||
|
del obj.fields[i]
|
||||||
|
|
||||||
views = {
|
views = {
|
||||||
"user": [
|
"user": [
|
||||||
"website",
|
"website",
|
||||||
@@ -189,6 +234,7 @@ class UserSchema(ma.ModelSchema):
|
|||||||
"bracket",
|
"bracket",
|
||||||
"id",
|
"id",
|
||||||
"oauth_id",
|
"oauth_id",
|
||||||
|
"fields",
|
||||||
],
|
],
|
||||||
"self": [
|
"self": [
|
||||||
"website",
|
"website",
|
||||||
@@ -200,6 +246,7 @@ class UserSchema(ma.ModelSchema):
|
|||||||
"id",
|
"id",
|
||||||
"oauth_id",
|
"oauth_id",
|
||||||
"password",
|
"password",
|
||||||
|
"fields",
|
||||||
],
|
],
|
||||||
"admin": [
|
"admin": [
|
||||||
"website",
|
"website",
|
||||||
@@ -217,6 +264,7 @@ class UserSchema(ma.ModelSchema):
|
|||||||
"password",
|
"password",
|
||||||
"type",
|
"type",
|
||||||
"verified",
|
"verified",
|
||||||
|
"fields",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,5 +274,6 @@ class UserSchema(ma.ModelSchema):
|
|||||||
kwargs["only"] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs["only"] = view
|
kwargs["only"] = view
|
||||||
|
self.view = view
|
||||||
|
|
||||||
super(UserSchema, self).__init__(*args, **kwargs)
|
super(UserSchema, self).__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -24,12 +24,15 @@ function profileUpdate(event) {
|
|||||||
const $form = $(this);
|
const $form = $(this);
|
||||||
let params = $form.serializeJSON(true);
|
let params = $form.serializeJSON(true);
|
||||||
|
|
||||||
params.fields = {}
|
params.fields = [];
|
||||||
|
|
||||||
for (const property in params) {
|
for (const property in params) {
|
||||||
if (property.match(/fields\[\d+\]/)) {
|
if (property.match(/fields\[\d+\]/)) {
|
||||||
let id = property.slice(7, -1);
|
let field = {};
|
||||||
params.fields[id] = params[property];
|
let id = parseInt(property.slice(7, -1));
|
||||||
|
field["field_id"] = id;
|
||||||
|
field["value"] = params[property];
|
||||||
|
params.fields.push(field);
|
||||||
delete params[property];
|
delete params[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
/***/ (function(module, exports, __webpack_require__) {
|
/***/ (function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
;
|
;
|
||||||
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/core/assets/js/pages/main.js\");\n\nvar _utils = __webpack_require__(/*! ../utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! ../CTFd */ \"./CTFd/themes/core/assets/js/CTFd.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\nvar error_template = '<div class=\"alert alert-danger alert-dismissable\" role=\"alert\">\\n' + ' <span class=\"sr-only\">Error:</span>\\n' + \" {0}\\n\" + ' <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\\n' + \"</div>\";\nvar success_template = '<div class=\"alert alert-success alert-dismissable submit-row\" role=\"alert\">\\n' + \" <strong>Success!</strong>\\n\" + \" Your profile has been updated\\n\" + ' <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\\n' + \"</div>\";\n\nfunction profileUpdate(event) {\n event.preventDefault();\n (0, _jquery.default)(\"#results\").empty();\n var $form = (0, _jquery.default)(this);\n var params = $form.serializeJSON(true);\n\n _CTFd.default.api.patch_user_private({}, params).then(function (response) {\n if (response.success) {\n (0, _jquery.default)(\"#results\").html(success_template);\n } else if (\"errors\" in response) {\n Object.keys(response.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 = response.errors[error];\n (0, _jquery.default)(\"#results\").append(error_template.format(error_msg));\n });\n }\n });\n}\n\nfunction tokenGenerate(event) {\n event.preventDefault();\n var $form = (0, _jquery.default)(this);\n var params = $form.serializeJSON(true);\n\n _CTFd.default.fetch(\"/api/v1/tokens\", {\n method: \"POST\",\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n var body = (0, _jquery.default)(\"\\n <p>Please copy your API Key, it won't be shown again!</p>\\n <div class=\\\"input-group mb-3\\\">\\n <input type=\\\"text\\\" id=\\\"user-token-result\\\" class=\\\"form-control\\\" value=\\\"\".concat(response.data.value, \"\\\" readonly>\\n <div class=\\\"input-group-append\\\">\\n <button class=\\\"btn btn-outline-secondary\\\" type=\\\"button\\\">\\n <i class=\\\"fas fa-clipboard\\\"></i>\\n </button>\\n </div>\\n </div>\\n \"));\n body.find(\"button\").click(function (event) {\n (0, _utils.copyToClipboard)(event, \"#user-token-result\");\n });\n (0, _ezq.ezAlert)({\n title: \"API Key Generated\",\n body: body,\n button: \"Got it!\",\n large: true\n });\n }\n });\n}\n\nfunction deleteToken(event) {\n event.preventDefault();\n var $elem = (0, _jquery.default)(this);\n var id = $elem.data(\"token-id\");\n (0, _ezq.ezQuery)({\n title: \"Delete Token\",\n body: \"Are you sure you want to delete this token?\",\n success: function success() {\n _CTFd.default.fetch(\"/api/v1/tokens/\" + id, {\n method: \"DELETE\"\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n $elem.parent().parent().remove();\n }\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\"#user-profile-form\").submit(profileUpdate);\n (0, _jquery.default)(\"#user-token-form\").submit(tokenGenerate);\n (0, _jquery.default)(\".delete-token\").click(deleteToken);\n (0, _jquery.default)(\".nav-pills a\").click(function (_event) {\n window.location.hash = this.hash;\n }); // Load location hash\n\n var hash = window.location.hash;\n\n if (hash) {\n hash = hash.replace(\"<>[]'\\\"\", \"\");\n (0, _jquery.default)('.nav-pills a[href=\"' + hash + '\"]').tab(\"show\");\n }\n});\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/pages/settings.js?");
|
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/core/assets/js/pages/main.js\");\n\nvar _utils = __webpack_require__(/*! ../utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! ../CTFd */ \"./CTFd/themes/core/assets/js/CTFd.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\nvar error_template = '<div class=\"alert alert-danger alert-dismissable\" role=\"alert\">\\n' + ' <span class=\"sr-only\">Error:</span>\\n' + \" {0}\\n\" + ' <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\\n' + \"</div>\";\nvar success_template = '<div class=\"alert alert-success alert-dismissable submit-row\" role=\"alert\">\\n' + \" <strong>Success!</strong>\\n\" + \" Your profile has been updated\\n\" + ' <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\\n' + \"</div>\";\n\nfunction profileUpdate(event) {\n event.preventDefault();\n (0, _jquery.default)(\"#results\").empty();\n var $form = (0, _jquery.default)(this);\n var params = $form.serializeJSON(true);\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 _CTFd.default.api.patch_user_private({}, params).then(function (response) {\n if (response.success) {\n (0, _jquery.default)(\"#results\").html(success_template);\n } else if (\"errors\" in response) {\n Object.keys(response.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 = response.errors[error];\n (0, _jquery.default)(\"#results\").append(error_template.format(error_msg));\n });\n }\n });\n}\n\nfunction tokenGenerate(event) {\n event.preventDefault();\n var $form = (0, _jquery.default)(this);\n var params = $form.serializeJSON(true);\n\n _CTFd.default.fetch(\"/api/v1/tokens\", {\n method: \"POST\",\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n var body = (0, _jquery.default)(\"\\n <p>Please copy your API Key, it won't be shown again!</p>\\n <div class=\\\"input-group mb-3\\\">\\n <input type=\\\"text\\\" id=\\\"user-token-result\\\" class=\\\"form-control\\\" value=\\\"\".concat(response.data.value, \"\\\" readonly>\\n <div class=\\\"input-group-append\\\">\\n <button class=\\\"btn btn-outline-secondary\\\" type=\\\"button\\\">\\n <i class=\\\"fas fa-clipboard\\\"></i>\\n </button>\\n </div>\\n </div>\\n \"));\n body.find(\"button\").click(function (event) {\n (0, _utils.copyToClipboard)(event, \"#user-token-result\");\n });\n (0, _ezq.ezAlert)({\n title: \"API Key Generated\",\n body: body,\n button: \"Got it!\",\n large: true\n });\n }\n });\n}\n\nfunction deleteToken(event) {\n event.preventDefault();\n var $elem = (0, _jquery.default)(this);\n var id = $elem.data(\"token-id\");\n (0, _ezq.ezQuery)({\n title: \"Delete Token\",\n body: \"Are you sure you want to delete this token?\",\n success: function success() {\n _CTFd.default.fetch(\"/api/v1/tokens/\" + id, {\n method: \"DELETE\"\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n $elem.parent().parent().remove();\n }\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\"#user-profile-form\").submit(profileUpdate);\n (0, _jquery.default)(\"#user-token-form\").submit(tokenGenerate);\n (0, _jquery.default)(\".delete-token\").click(deleteToken);\n (0, _jquery.default)(\".nav-pills a\").click(function (_event) {\n window.location.hash = this.hash;\n }); // Load location hash\n\n var hash = window.location.hash;\n\n if (hash) {\n hash = hash.replace(\"<>[]'\\\"\", \"\");\n (0, _jquery.default)('.nav-pills a[href=\"' + hash + '\"]').tab(\"show\");\n }\n});\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/pages/settings.js?");
|
||||||
|
|
||||||
/***/ })
|
/***/ })
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user