Disabled field is considered for validation in WTForms and Flask

后端 未结 4 775
别跟我提以往
别跟我提以往 2021-02-01 08:38

I have some fields in page disabled as for example:(using jinja2 templating system)



{{ form.na
相关标签:
4条回答
  • 2021-02-01 08:54

    This is actually an interesting problem, and the way WTForms solves it is intentionally something that requires explicitness, because it has to do with security and not allowing users to fake input.

    So the intent is, that "managers" cannot edit the name, while "admins" can.

    At first glance this seems obvious, just disable the field in HTML, and write your view like this:

    def edit_team():
        form = TeamForm(request.POST, obj=team)
        if request.POST and form.validate():
            form.populate_obj(team) # <-- This is the dangerous part here
            return redirect('/teams')
        return render('edit_team.html')
    

    As written, this is a major security risk, because the disabled property in HTML forms is client-side only. Anyone with an HTML inspector (ie FireBug, webkit document inspector, etc) can remove this property, or someone could simply make a request like so:

    POST /edit_team/7 HTTP/1.0
    Content-Type: application/x-urlencoded
    
    team=EVILTEAMNAME&title=foo
    

    The issue then is of course, how do we gate this properly on the server-side, corresponding to the appropriate way of doing this? The correct approach with WTForms is to not have the field in the first place. There's a few ways to do this, one is to use form composition and have e.g. ManagerTeamForm and AdminTeamForm (sometimes this is better) but other times it's easier to use del to remove specific fields.

    So here's how you would write your view, and not have the validation issues:

    def edit_team():
        form = TeamForm(request.POST, obj=team)
        if user.role == 'manager':
            del form.name
        if request.POST and form.validate():
            form.populate_obj(team)
            return redirect('/teams')
        return render('edit_team.html')
    

    And a quick modification to the template:

    <html>
    <body>
    <form action="" method=POST>
        {% if 'name' in form %}
            {{ form.name() }}
        {% else %}
            {{ team.name|e }}
        {% endif %}
        {{ form.title }}
        -- submit button --
    </form>
    </body>
    </html>
    

    Some pieces of reference for wtforms best-practices:

    • WTForms 'Solving Specific Problems'
    • Dangers of Using forms as a backing store (WTForms google group) Post 1 / Post 2
    • StackOverflow: WTForms 'readonly' attribute
    0 讨论(0)
  • 2021-02-01 09:05

    You need to make the name field optional when defining the form.

    name = wtf.TextField("Team Name", validators=[validators.Optional()])
    

    Then in your views, pass a variable called "role" and set it to either manager or admin depending on the user.

    <form action="" method=POST>
    {% if role == 'manager' % }
        {{ form.name(disabled=True) }}
    {% else % }
        {{ form.name() }}
    {{ form.title }}
    -- submit button --
    </form>
    
    0 讨论(0)
  • 2021-02-01 09:12
    1. Create a custom validator
    from wtforms.validators import Optional
    class DisabledValidator(Optional):
        """
        do nothing
        """
        pass
    
    1. Let's create a custom rule basing on the form.rule
    from flask_admin.form.rules import Field
    class EasyCustomFieldRule(Field):
        def __init__(self, field_name, render_field='lib.render_field', field_args={}):
            super(self.__class__, self).__init__(field_name, render_field)
            self.extra_field_args = field_args
    
        def __call__(self, form, form_opts=None, field_args={}):
            field = getattr(form, self.field_name)
            if self.extra_field_args.get('disabled'):
                field.validators.append(DisabledValidator())
    
            field_args.update(self.extra_field_args)
            return super(self.__class__, self).__call__(form, form_opts, field_args)
    
    1. Override write some functions of wtforms.form
    from wtforms.form import Form
    from wtforms.compat import iteritems
    class BaseForm(Form):
        """
        重写部分方法,以适应disabled的Field
        """
    
        def validate(self):
            """
            Validates the form by calling `validate` on each field, passing any
            extra `Form.validate_<fieldname>` validators to the field validator.
            """
            extra = {}
            for name in self._fields:
                inline = getattr(self.__class__, 'validate_%s' % name, None)
                if inline is not None:
                    extra[name] = [inline]
    
            return self.validate_(extra)
    
        def validate_(self, extra_validators=None):
            self._errors = None
            success = True
            for name, field in iteritems(self._fields):
                is_disabled = False
                for v in field.validators:
                    if isinstance(v, DisabledValidator):
                        field.flags.disabled = True
                        is_disabled = True
                        break
                if is_disabled:
                    continue
    
                if extra_validators is not None and name in extra_validators:
                    extra = extra_validators[name]
                else:
                    extra = tuple()
                if not field.validate(self, extra):
                    success = False
            return success
    
        def populate_obj(self, obj):
            for name, field in self._fields.items():
                if not field.flags.disabled:
                    field.populate_obj(obj, name)
    
    1. set the form_base_class in your ModelView, and set the form_edit_rules or the form_create_rules with EasyCustomFieldRule
    from flask_admin.contrib.sqla import ModelView
    class MyTestModelView(ModelView):
        ...
        form_base_class = BaseForm
        ...
    
        form_edit_rules = (
            EasyCustomFieldRule('column0', field_args={'disabled': True}),
            'column1', 'column2'
        )
    
    1. Just testing...
    0 讨论(0)
  • 2021-02-01 09:16

    I defined my own validator for this problem:

    from wtforms.validators import Optional
    
    class OptionalIfDisabled(Optional):
    
        def __call__(self, form, field):
            if field.render_kw is not None and field.render_kw.get('disabled', False):
                field.flags.disabled = True
                super(OptionalIfDisabled, self).__call__(form, field)
    

    And then I defined a new base for my forms:

    from wtforms.form import Form
    
    class BaseForm(Form):
    
        def populate_obj(self, obj):
            for name, field in self._fields.items():
                if not field.flags.disabled:
                    field.populate_obj(obj, name)
    

    Now every form can extend the BaseForm and disable fields like this:

    from wtforms.fields import StringField, SubmitField
    
    class TeamForm(BaseForm):
        team = StringField(label='Team Name', 
                           validators=[OptionalIfDisabled(), InputRequired()]
        submit = SubmitField(label='Submit')
    
        def __init__(self, *args, **kwargs):
            super(TeamForm, self).__init__(*args, **kwargs)
            # disable the fields if you want to
            if some_condition:
                self.team.render_kw = {'disabled': True}
    

    After validation of the TeamForm, you can use populate_obj to copy the enabled form data in any object. It will ignore the disabled fields.

    0 讨论(0)
提交回复
热议问题