mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 22:14:25 +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:
|
||||
return {"success": False, "errors": response.errors}, 400
|
||||
|
||||
from CTFd.models import FieldEntries
|
||||
fields = data.get("fields")
|
||||
for field_id, value in fields.items():
|
||||
e = FieldEntries.query.filter_by(field_id=field_id, user_id=user.id).first()
|
||||
e.value = value
|
||||
# from CTFd.models import FieldEntries
|
||||
# fields = data.get("fields")
|
||||
# for field_id, value in fields.items():
|
||||
# e = FieldEntries.query.filter_by(field_id=field_id, user_id=user.id).first()
|
||||
# e.value = value
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -289,8 +289,11 @@ def register():
|
||||
db.session.flush()
|
||||
|
||||
from CTFd.models import FieldEntries
|
||||
|
||||
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.commit()
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ from wtforms.fields.html5 import DateField, URLField
|
||||
|
||||
from CTFd.forms import BaseForm
|
||||
from CTFd.forms.fields import SubmitField
|
||||
from CTFd.models import FieldEntries, Fields
|
||||
from CTFd.utils.countries import SELECT_COUNTRIES_LIST
|
||||
from CTFd.models import Fields, FieldEntries
|
||||
|
||||
|
||||
def SettingsForm(*args, **kwargs):
|
||||
|
||||
@@ -276,7 +276,9 @@ class Users(db.Model):
|
||||
# Relationship for Teams
|
||||
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)
|
||||
|
||||
|
||||
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 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.crypto import verify_password
|
||||
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])
|
||||
password = field_for(Users, "password")
|
||||
fields = Nested(FieldEntriesSchema(), partial=True, many=True)
|
||||
|
||||
@pre_load
|
||||
def validate_name(self, data):
|
||||
@@ -180,6 +183,48 @@ class UserSchema(ma.ModelSchema):
|
||||
data.pop("password", 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 = {
|
||||
"user": [
|
||||
"website",
|
||||
@@ -189,6 +234,7 @@ class UserSchema(ma.ModelSchema):
|
||||
"bracket",
|
||||
"id",
|
||||
"oauth_id",
|
||||
"fields",
|
||||
],
|
||||
"self": [
|
||||
"website",
|
||||
@@ -200,6 +246,7 @@ class UserSchema(ma.ModelSchema):
|
||||
"id",
|
||||
"oauth_id",
|
||||
"password",
|
||||
"fields",
|
||||
],
|
||||
"admin": [
|
||||
"website",
|
||||
@@ -217,6 +264,7 @@ class UserSchema(ma.ModelSchema):
|
||||
"password",
|
||||
"type",
|
||||
"verified",
|
||||
"fields",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -226,5 +274,6 @@ class UserSchema(ma.ModelSchema):
|
||||
kwargs["only"] = self.views[view]
|
||||
elif isinstance(view, list):
|
||||
kwargs["only"] = view
|
||||
self.view = view
|
||||
|
||||
super(UserSchema, self).__init__(*args, **kwargs)
|
||||
|
||||
@@ -24,12 +24,15 @@ function profileUpdate(event) {
|
||||
const $form = $(this);
|
||||
let params = $form.serializeJSON(true);
|
||||
|
||||
params.fields = {}
|
||||
params.fields = [];
|
||||
|
||||
for (const property in params) {
|
||||
if( property.match(/fields\[\d+\]/)) {
|
||||
let id = property.slice(7, -1);
|
||||
params.fields[id] = params[property];
|
||||
if (property.match(/fields\[\d+\]/)) {
|
||||
let field = {};
|
||||
let id = parseInt(property.slice(7, -1));
|
||||
field["field_id"] = id;
|
||||
field["value"] = params[property];
|
||||
params.fields.push(field);
|
||||
delete params[property];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
/***/ (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