Handle processing field entries from admin panel and add permissions to viewing fields

This commit is contained in:
Kevin Chung
2020-08-16 18:14:55 -04:00
parent 3d390dfe86
commit cd12d5df7e
7 changed files with 148 additions and 20 deletions

View File

@@ -4,6 +4,7 @@ from wtforms.validators import InputRequired
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
@@ -40,7 +41,7 @@ class PublicUserSearchForm(BaseForm):
submit = SubmitField("Search")
class UserEditForm(BaseForm):
class UserBaseForm(BaseForm):
name = StringField("User Name", validators=[InputRequired()])
email = EmailField("Email", validators=[InputRequired()])
password = PasswordField("Password")
@@ -54,5 +55,59 @@ class UserEditForm(BaseForm):
submit = SubmitField("Submit")
class UserCreateForm(UserEditForm):
notify = BooleanField("Email account credentials to user", default=True)
def UserEditForm(*args, **kwargs):
class _UserEditForm(UserBaseForm):
pass
@property
def extra(self):
fields = []
new_fields = Fields.query.all()
user_fields = {}
for f in FieldEntries.query.filter_by(user_id=self.obj.id).all():
user_fields[f.field_id] = f.value
for field in new_fields:
form_field = getattr(self, f"fields[{field.id}]")
form_field.data = user_fields.get(field.id, "")
entry = (field.name, form_field)
fields.append(entry)
return fields
def __init__(self, *args, **kwargs):
"""
Custom init to persist the obj parameter to the rest of the form
"""
super().__init__(*args, **kwargs)
obj = kwargs.get("obj")
if obj:
self.obj = obj
new_fields = Fields.query.all()
for field in new_fields:
setattr(_UserEditForm, f"fields[{field.id}]", StringField(field.name))
return _UserEditForm(*args, **kwargs)
def UserCreateForm(*args, **kwargs):
class _UserCreateForm(UserBaseForm):
notify = BooleanField("Email account credentials to user", default=True)
@property
def extra(self):
fields = []
new_fields = Fields.query.all()
for field in new_fields:
form_field = getattr(self, f"fields[{field.id}]")
entry = (field.name, form_field)
fields.append(entry)
return fields
new_fields = Fields.query.all()
for field in new_fields:
setattr(_UserCreateForm, f"fields[{field.id}]", StringField(field.name))
return _UserCreateForm(*args, **kwargs)

View File

@@ -15,7 +15,7 @@ class FieldEntriesSchema(ma.ModelSchema):
class Meta:
model = FieldEntries
include_fk = True
load_only = ("id", )
load_only = ("id",)
exclude = ("field", "user", "user_id")
dump_only = ("user_id", "name", "description", "type")

View File

@@ -1,8 +1,8 @@
from marshmallow import ValidationError, pre_load, pre_dump, validate
from marshmallow import ValidationError, post_dump, pre_load, validate
from marshmallow.fields import Nested
from marshmallow_sqlalchemy import field_for
from CTFd.models import Fields, FieldEntries, Users, ma, db
from CTFd.models import FieldEntries, Fields, Users, ma
from CTFd.schemas.fields import FieldEntriesSchema
from CTFd.utils import get_config, string_types
from CTFd.utils.crypto import verify_password
@@ -51,7 +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)
fields = Nested(FieldEntriesSchema, partial=True, many=True)
@pre_load
def validate_name(self, data):
@@ -193,7 +193,25 @@ class UserSchema(ma.ModelSchema):
fields = data.get("fields")
if is_admin():
pass
user_id = data.get("id")
if user_id:
target_user = Users.query.filter_by(id=data["id"]).first()
for f in fields:
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=target_user.id
).first()
if entry:
f["id"] = entry.id
else:
# Marshmallow automatically links the fields to newly created users
pass
else:
for f in fields:
# Remove any existing set
@@ -201,31 +219,44 @@ class UserSchema(ma.ModelSchema):
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()
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()
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):
@post_dump()
def process_fields(self, data):
"""
Handle permissions levels for fields.
This is post_dump to manipulate JSON instead of the raw db object
Admins can see all fields.
Users (self) can see their edittable and public fields
Public (user) can only see public fields
"""
# Make the object detatched so that changes don't accidentally persist
db.session.expunge(obj)
for i, entry in enumerate(obj.fields):
# Gather all possible fields
removed_field_ids = []
fields = Fields.query.all()
# Select fields for removal based on current view and properties of the field
for field in fields:
if self.view == "user":
if entry.field.public is False:
del obj.fields[i]
if field.public is False:
removed_field_ids.append(field.id)
elif self.view == "self":
if entry.field.editable is False and entry.field.public is False:
del obj.fields[i]
if field.editable is False and field.public is False:
removed_field_ids.append(field.id)
# Rebuild fuilds
fields = data.get("fields")
if fields:
data["fields"] = [
field for field in fields if field["field_id"] not in removed_field_ids
]
views = {
"user": [

View File

@@ -11,6 +11,19 @@ function createUser(event) {
event.preventDefault();
const params = $("#user-info-create-form").serializeJSON(true);
params.fields = [];
for (const property in params) {
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];
}
}
// Move the notify value into a GET param
let url = "/api/v1/users";
let notify = params.notify;
@@ -57,6 +70,19 @@ function updateUser(event) {
event.preventDefault();
const params = $("#user-info-edit-form").serializeJSON(true);
params.fields = [];
for (const property in params) {
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];
}
}
CTFd.fetch("/api/v1/users/" + window.USER_ID, {
method: "PATCH",
credentials: "same-origin",

File diff suppressed because one or more lines are too long

View File

@@ -24,6 +24,14 @@
{{ form.country.label }}
{{ form.country(class="form-control custom-select") }}
</div>
{% for k, v in form.extra %}
<div class="form-group">
<b>{{ v.label }}</b>
{{ v(class="form-control") }}
</div>
{% endfor %}
<div class="form-group">
<div class="form-check form-check-inline">
{{ form.type(class="form-control form-inline custom-select", id="type-select") }}

View File

@@ -24,6 +24,14 @@
{{ form.country.label }}
{{ form.country(class="form-control custom-select") }}
</div>
{% for k, v in form.extra %}
<div class="form-group">
<b>{{ v.label }}</b>
{{ v(class="form-control") }}
</div>
{% endfor %}
<div class="form-group">
<div class="form-check form-check-inline">
{{ form.type(class="form-control form-inline custom-select", id="type-select") }}