diff --git a/CTFd/api/v1/users.py b/CTFd/api/v1/users.py index a50d514b..fad4b1c0 100644 --- a/CTFd/api/v1/users.py +++ b/CTFd/api/v1/users.py @@ -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() diff --git a/CTFd/auth.py b/CTFd/auth.py index 91dda34c..a43ce089 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -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() diff --git a/CTFd/forms/self.py b/CTFd/forms/self.py index ef7f1da9..50b2c2d9 100644 --- a/CTFd/forms/self.py +++ b/CTFd/forms/self.py @@ -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): diff --git a/CTFd/models/__init__.py b/CTFd/models/__init__.py index 85a428eb..f3019b73 100644 --- a/CTFd/models/__init__.py +++ b/CTFd/models/__init__.py @@ -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) diff --git a/CTFd/schemas/fields.py b/CTFd/schemas/fields.py new file mode 100644 index 00000000..ebc006a8 --- /dev/null +++ b/CTFd/schemas/fields.py @@ -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") diff --git a/CTFd/schemas/users.py b/CTFd/schemas/users.py index 07148255..4e1c165c 100644 --- a/CTFd/schemas/users.py +++ b/CTFd/schemas/users.py @@ -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) diff --git a/CTFd/themes/core/assets/js/pages/settings.js b/CTFd/themes/core/assets/js/pages/settings.js index 67a0f7de..daf03555 100644 --- a/CTFd/themes/core/assets/js/pages/settings.js +++ b/CTFd/themes/core/assets/js/pages/settings.js @@ -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]; } } diff --git a/CTFd/themes/core/static/js/pages/settings.dev.js b/CTFd/themes/core/static/js/pages/settings.dev.js index 56aa74a5..1c8c2d78 100644 --- a/CTFd/themes/core/static/js/pages/settings.dev.js +++ b/CTFd/themes/core/static/js/pages/settings.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\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 = '
\\n' + ' Error:\\n' + \" {0}\\n\" + ' \\n' + \"
\";\nvar success_template = '
\\n' + \" Success!\\n\" + \" Your profile has been updated\\n\" + ' \\n' + \"
\";\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

Please copy your API Key, it won't be shown again!

\\n
\\n \\n
\\n \\n
\\n
\\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 = '
\\n' + ' Error:\\n' + \" {0}\\n\" + ' \\n' + \"
\";\nvar success_template = '
\\n' + \" Success!\\n\" + \" Your profile has been updated\\n\" + ' \\n' + \"
\";\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

Please copy your API Key, it won't be shown again!

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