spitfiredd
spitfiredd

Reputation: 3135

Flask Admin - Automatically create password hash when creating a new user?

Say I have a user model like:

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(255))
    last_name = db.Column(db.String(255))
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    registered_on = db.Column(db.DateTime, nullable=True)
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))

And an admin view:

class UserView(MyModelView):

    form_columns = (
        'roles',
        'first_name',
        'last_name',
        'email',
        'password',
        'registered_on',
    )

    form_args = dict(
                registered_on=dict(default=datetime.now())
            )

When I create a new user how to I automatically generate a password hash with something like bcrypt?

Upvotes: 5

Views: 8342

Answers (5)

abathur
abathur

Reputation: 1047

I think by automatic you mean that it should automatically convert a manually-supplied plain-text password into a hash, but I am not certain. I am answering as if this assumption is correct.

We don't expose our password fields in Flask-Admin, but we handle the basic question of how and when passwords get hashed by defining password as a property with a setter responsible for computing the hash itself and tucking it away in user._password.

A naive swing at just trying to add password as a column in the admin view didn't work, but if you use sqlalchemy's hybrid_property instead of property, it looks like this works fine with "password" in your User view's list of form_columns (as you already have):

# models.py
from sqlalchemy.ext.hybrid import hybrid_property

class User(sql.Model):
    # ...

    _password = sql.Column(sql.Binary)
    _salt = sql.Column(sql.Binary, default=lambda: os.urandom(512))

    # ... 

    @hybrid_property
    def password(self):
        """Return the hashed user password."""
        return self._password

    @password.setter
    def password(self, new_pass):
        """Salt/Hash and save the user's new password."""
        new_password_hash = compute_new_password_hash(new_pass, self._salt)
        self._password = new_password_hash

# admin.py
class UserView(MyModelView):
    # ...

    form_columns = ("email", "password", ...)

The editing experience isn't exactly polished--you'd probably need to override the field to refine it. Here are some screenshots:

Creating a new user with a pre-set password:

Creating a new user with password

The edit view after creation:

User created, password hashed

Updating the user's password:

Saving a new password

The edit view after saving the new password:

User updated with new hash

Upvotes: 7

Rodolfo Ortega
Rodolfo Ortega

Reputation: 517

You can set a PasswordField widget for the password field, and override the process_formdata method to set de hash.

from flask_security.utils import hash_password
from wtforms import PasswordField

class AdminPasswordField(PasswordField):

    def process_formdata(self, valuelist):
        if valuelist and valuelist[0] != '':
            self.data = hash_password(valuelist[0])
        elif self.data is None:
            self.data = ''

class UserView(MyModelView):

    form_columns = (
        'roles',
        'first_name',
        'last_name',
        'email',
        'password',
        'registered_on',
    )

    form_args = dict(
                registered_on=dict(default=datetime.now())
            )

    form_overrides = {
        'password': AdminPasswordField,
    }

Upvotes: 2

Michael
Michael

Reputation: 721

Internally, Flask-Admin calls WTF's populate_obj to set the data from the form. So we can take advantage of that as shown below:

class CustomPasswordField(PasswordField): # If you don't want hide the password you can use a StringField

    def populate_obj(self, obj, name):
        setattr(obj, name, hash_password(self.data)) # Password function

Then we can use it in our ModelView as follows:

class UserView(ModelView):
    form_extra_fields = {
        'password': CustomPasswordField('Password', validators=[InputRequired()])
    }

The password will be hashed the moment the form is saved.

Upvotes: 1

Caumons
Caumons

Reputation: 9595

The easiest option I've found is to add an SQLAlchemy attribute event on User.password and hash the password there if changed. This way, anytime a user password is changed from anywhere, it'll be automatically hashed.

from sqlalchemy import event
from werkzeug.security import generate_password_hash


@event.listens_for(User.password, 'set', retval=True)
def hash_user_password(target, value, oldvalue, initiator):
    if value != oldvalue:
        return generate_password_hash(value)
    return value

Upvotes: 15

spitfiredd
spitfiredd

Reputation: 3135

You can use Flask-Admin's on_model-change and give it logic before it commits to the database.

from flask_admin.contrib import sqla
from flask_security import utils

class UserAdmin(sqla.ModelView):

    def on_model_change(self, form, model, is_created):
        model.password = utils.encrypt_password(model.password)
        # as another example
        if is_created:
            model.registered_on = datetime.now()  

Upvotes: 0

Related Questions