Add Marshmallow handling of fields and add permissions for fields

This commit is contained in:
Kevin Chung
2020-08-15 17:40:12 -04:00
parent 682caba26f
commit acc4f3e346
8 changed files with 96 additions and 15 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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):

View File

@@ -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
View 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")

View File

@@ -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)

View File

@@ -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];
}
}

View File

@@ -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?");
/***/ })