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 = '
Please copy your API Key, it won't be shown again!
\\nPlease copy your API Key, it won't be shown again!
\\n