Make peewee admin work with flask security - python

I like both peewee and Flask security, and they both work well together, but I can't get the admin functionality of peewee to work. I am really not sure about how to do this, but I really want to use both together. I have tried a lot of things, but nothing seems to work.
My current code looks like this:
import datetime
from flask import Flask, abort, redirect, render_template, url_for, flash, request
from flask_peewee.auth import Auth, BaseUser
from flask_peewee.db import Database
from flask_peewee.admin import Admin, ModelAdmin
from flask_peewee.rest import RestAPI, UserAuthentication
from peewee import *
from flask.ext.security import *
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['DATABASE'] = {
'name': 'ee.db',
'engine': 'peewee.SqliteDatabase',
}
# Create database connection object
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class RoleAdmin(ModelAdmin):
columns = ('name', 'description')
class User(db.Model, UserMixin):
username = TextField()
password = TextField()
email = TextField()
role = ForeignKeyField(Role)
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserAdmin(ModelAdmin):
columns = ('username', 'email', 'role', 'active', 'confirmed_at')
class UserRoles(db.Model):
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
class UserRolesAdmin(ModelAdmin):
columns = ('user', 'role', 'name', 'description')
class Note(db.Model):
user = ForeignKeyField(User, related_name='notes')
title = CharField()
message = TextField()
created = DateTimeField(default=datetime.datetime.now)
def __unicode__(self):
return '%s: %s %s' % (self.user.username, self.message, self.created)
class NoteAdmin(ModelAdmin):
columns = ("user", "message", "created")
foreign_key_lookups = {'user': 'username'}
class CustomAuth(Auth):
def get_user_model(self):
return User
def get_model_admin(self):
return UserAdmin
class CustomAdmin(Admin):
def check_user_permission(self, user):
if has_role(current_user).name == "admin":
return True
# Setup Flask-Security
user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
security = Security(app, user_datastore)
#Setup Flask-Peewee admin
auth = CustomAuth(app, db)
admin = CustomAdmin(app, auth)
admin.register(Note, NoteAdmin)
admin.register(User, UserAdmin)
admin.setup()
#create a RestAPI container
api = RestAPI(app)
#register the Note model
api.register(Note)
api.setup()
#instantiate the user auth
user_auth = UserAuthentication(auth)
# Views
#app.route("/")
#login_required
def index():
messages = Note.select()
return render_template("base.html",
notes = messages,
username = current_user.username,
role = current_user.role.name
)
#app.route("/add", methods = ['post'])
#login_required
def add():
user = current_user
now = datetime.datetime.now()
Note.create(user = current_user.get_id(), title = request.form["title"], message = request.form["message"], created = now)
flash('New entry was successfully posted')
return redirect(url_for('index'))
#app.route("/delete/<id>")
#login_required
def delete(id):
note = Note.get(Note.id == id)
note.delete_instance()
flash('Entry was successfully removed')
return redirect(url_for('index'))
if __name__ == '__main__':
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table(fail_silently=True)
user_datastore.create_role(name="admin", description="admin")
user_datastore.create_user(username="Ben", email='hello', password='password', active=True, confirmed_at=datetime.datetime(1935,9,9,9,9), role=1)
user_datastore.add_role_to_user('hello', "admin")
Note.create_table(fail_silently=True)
app.run(port = 8080)
When I try to log in into the Peewee Admin, I get the following error: `AttributeError: 'User' object has no attribute 'check_password'
EDIT:
Full debug:
File "/anaconda/lib/python2.7/site-packages/flask_peewee/auth.py", line 170, in login
form.password.data,
File "/anaconda/lib/python2.7/site-packages/flask_peewee/auth.py", line 128, in authenticate
if not user.check_password(password):
AttributeError: 'User' object has no attribute 'check_password
I really do not understand why this raises an AttributeError. The password is registered, and the BaseUser class is imported (required for check_password). What am I doing wrong?

Your User class needs to implement the check_password and set_password methods, because Flask-Peewee calls those to authenticate requests and update models.
Flask-Peewee provides a class, BaseUser, with simple implementations you could mix in to User, but this probably isn't useful when you're also using Flask-Security. You'll need to implement the methods yourself in a way that they call into Flask-Security to compare and hash passwords, because the two projects' approaches to password hashing are different, and you're only saving one hash in the database.

Related

Why is Django check_password=True but authenticate=None

I'm trying to write a unit test for a login endpoint in Django using as much of the built in functionality as possible.
There are existing tests that confirm that the account create endpoint is functioning properly.
In the login view, however, the check_password() function will return True for this test, but the authenticate() function returns None.
Is it safe to use the check_password() function instead?
Otherwise, how do I update this code to use the authenticate() function?
accounts.py
class Account(AbstractUser):
username = models.CharField(max_length=150, unique=True, null=False, default='')
password = models.CharField(max_length=100)
...
REQUIRED_FIELDS = ['username', 'password']
class Meta:
app_label = 'accounts'
db_table = 'accounts_account'
objects = models.Manager()
test_login.py
def test_login(self):
# Create account
request_create = self.factory.post('/accounts/account',
self.request_data,
content_type='application/json')
view = AccountView.as_view()
response_create = view(request_create)
# Login account
request_login = self.factory.post('/accounts/login',
self.request_data,
content_type='application/json')
view = LoginView.as_view()
response = view(request_login)
views.py
class LoginView(View):
def post(self, request):
r = json.loads(request.body)
username = r.get('username')
password = r.get('password')
cp = check_password(password, Account.objects.get(username=username).password)
user = authenticate(username=username, password=password)
P.S. I've checked this thread and is_active is set to true.
It is safe. The main difference is that by using check_password() you are manually checking an User from an authentication backend, thus you have to retrieve the user object and compare its password with the plain text like you do at:
check_password(password, Account.objects.get(username=username).password)
While, with authenticate() it is possible to check credentials against several authentication backends. Meaning that with the former you couldn't hook your application with other authentication sources
You are missing some code in tests.py and views.py. That being said, here is a full test of this LoginView:
tests.py
class TestClientLogin(TestCase):
def setUp(self):
User = get_user_model()
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='test_user',
email='test_user#example.com',
password='super_secret'
)
def test_user_login(self):
request_data = {'username': 'test_user', 'password': 'super_secret'}
request = self.factory.post(
'/accounts/login/',
request_data ,
content_type='application/json'
)
response = LoginView.as_view()(request)
data = json.loads(response.content)
self.assertEqual(self.user.username, data['username'])
self.assertEqual(response.status_code, 200)
views.py
class LoginView(View):
def post(self, request):
data = json.loads(request.body)
username = data.get('username')
password = data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
return JsonResponse({'username': user.username})
else:
return JsonResponse({'username': None})
If you want use as much of the built in functionality as possible:
settings.py:
AUTH_USER_MODEL = 'accounts.Account'
accounts/models.py:
class Account(AbstractUser):
# you dont need to define username or password again
...
REQUIRED_FIELDS = ['username', 'password']
class Meta:
# you don't need to define default app_label, db_table
objects = UserManager() # otherwise don't work "python manage.py createsuperuser"
accounts/views.py:
from django.contrib.auth.views import LoginView
class ClientLogin(LoginView):
pass
accounts/tests.py:
from django.test import TestCase, Client
from django.utils.crypto import get_random_string
class TestClientLogin(TestCase):
def setUp(self):
User = get_user_model()
self.username, self.password = get_random_string(20), get_random_string(20) # don't use constants in tests
self.user = User.objects.create_user(username=self.username, password=self.password )
def test_user_login(self):
request_data = {'username': self.username, 'password': self.password}
response = Client().post('/accounts/login/', request_data)
self.assertEqual(response.status_code, 302)
BUT:
you don't need to test ClientLogin, you don't have any code in view.

sqlalchemy no such table: user

I've setup a flask site and I'm trying to get a signup page to work. The page itself renders but when I enter information in the form and submit I get a sqlalchemy.exc.OperationalError. Any help here would be greatly appreciated!!
Here is my signup function:
#auth.route('/signup', methods=['POST'])
def signup_post():
email = request.form.get('email')
name = request.form.get('name')
password = request.form.get('password')
user = User.query.filter_by(email=email).first() # if this returns a user,
# then the email already exists in database
if user: # if a user is found, we want to redirect back to signup page so user can try again
flash('email address already exists')
return redirect(url_for('auth.signup'))
# create a new user with the form data. Hash the password so the plaintext version isn't saved.
new_user = user(email=email, name=name, password=generate_password_hash(password, method='sha256'))
# add the new user to the database
db.session.add(new_user)
db.session.commit()
return redirect(url_for('auth.login'))
The specific error says no such table: user then:
[SQL: SELECT user.id AS user_id, user.email AS user_email, user.password AS user_password, user.name AS user_name
FROM user
WHERE user.email = ?
LIMIT ? OFFSET ?]
[parameters: ('', 1, 0)]
(Background on this error at: http://sqlalche.me/e/13/e3q8)
Here is where I initialize the DB and create the app:
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = 'UMGC-SDEV300-Key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
db.init_app(app)
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.init_app(app)
from .models import User
#login_manager.user_loader
def load_user(user_id):
# since the user_id is just the primary key of our user table, use it in the query for the user
return User.query.get(int(user_id))
# blueprint for auth routes in our app
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
# blueprint for non-auth parts of app
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
Check the models.py and add the line: tablename = 'user'
class User(UserMixin, db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
firstname = db.Column(db.String(30))
lastname = db.Column(db.String(30))
email = db.Column(db.String(100), uni
And in the init.py change to
#login_manager.user_loader
def load_user(user_id):
return User.query.filter_by(id=user_id).first()
And make sure that file exits,and have the table: user
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
I figured it out. I just needed to create the database. I went through this step once when I first created the application but I guess I missed something. I deleted the database and created a new one using the below command and that did the trick.
from project import db, create_app
db.create_all(app=create_app())
This was done from the Python REPL.

Flask-Admin's inaccessible_callback() not working properly

I want to control who can access admin pages on my flask application.
I've been trying to overwrite the flask_admin.ModelView's methods, 'is_accessible' and 'inaccessible_callback' to handle the situation.
This is the AdminView class I'm creating:
class AdminView(ModelView):
def is_accessible(self):
return current_user.admin
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
return redirect(url_for('auth.login', next=request.url))
and the model:
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True)
email = db.Column(db.String(120), unique=True)
password_hash = db.Column(db.String(128))
admin = db.Column(db.Boolean, default=False)
def __init__(self, username=None, email=None, password=None):
self.username = username
self.email = email
self.password_hash = password_hash
self.admin = admin
and the AdminView initialization:
def init_admin(admin):
from app.models import User
admin.add_view(AdminView(User, db.session))
which is called in the application factory:
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
if test_config is None:
# load the isntance config, if it exists, when not testing
app.config.from_object(Config)
else:
# load the test config passed in
app.config.from_object(test_config)
db.init_app(app)
migrate = Migrate(app, db)
login_manager.init_app(app)
mail.init_app(app)
bootstrap.init_app(app)
admin = Admin(app, name='app', template_mode='bootstrap3')
from app.auth import auth_bp
app.register_blueprint(auth_bp)
from app.tables import tables_bp
app.register_blueprint(tables_bp)
init_admin(admin)
try:
os.makedirs(app.instance_path)
except OSError:
pass
return app
When I login with a user that has it's admin attribute set to True it returns the correct admin page with the User model ready to be used. When I login with a user that has a false admin attribute, it still shows the admin page, without the User model attached. I would rather that it redirect them to a login page, with a warning that they are forbidden from that page.
I figured out how to get this to work thanks to a youtube video!
Flask_Admin Accessibility
Check it out for a more in-depth explanation!
The issue was that the '/admin' page is loaded by flask_admin.AdminIndexView
Therefore I had to create my own child class of AdminIndexView and set this as the index_view parameter when initializing Admin()
Here is the updated code:
I added a MyIndexView class to my admin file:
# ...
from flask_admin import AdminIndexView
# ...
class MyIndexView(AdminIndexView):
def is_accessible(self):
return current_user.admin
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
return redirect(url_for('auth.login', next=request.url))
And then I set the index_view parameter in my application factory
# ...
from app.admin import AdminView, MyIndexView
# ...
admin = Admin(app, name='app', template_mode='bootstrap3',
index_view=MyIndexView())
# ...
It now works perfectly!

update state with flask sqlalchemy with postgres will not commit to database

I have read quite a bit of documentation and I can't see what is wrong with these lines
update_this = User.query.filter_by(email=email).first()
update_this.emailconfirmed = True
db.session.commit()
...and yet when I deploy the boolean column 'emailconfirmed' never is update to True. I have confirmed with print statements that update_this.emailconfirmed is False at the exact point in the code shown above... I just can't seem to update that value. Does anybody know what tests I can do, what imports I should check etc. etc.
Right now this is the top of my main .py file where the above code appears
from flask import Flask, render_template, request, session, redirect, url_for, make_response
# the following line imports from models.py
from models import db, User
# the following line imports SignupForm from forms.py
from forms import SignupForm, LoginForm
from flask_mail import Mail, Message
from itsdangerous import URLSafeTimedSerializer
# Production (causes Heroku to redirect to SSL)
from flask_sslify import SSLify
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
sslify = SSLify(app)
sslify = SSLify(app, subdomains=True)
app.config.from_pyfile('config_file.cfg')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL']
db = SQLAlchemy(app)
mail = Mail(app)
ts = URLSafeTimedSerializer(app.config['SECRET_KEY'], salt=app.config['SALT'])
and this is my models.py file
from flask_sqlalchemy import SQLAlchemy
from werkzeug import generate_password_hash, check_password_hash
db = SQLAlchemy()
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
firstname = db.Column(db.String(100))
lastname = db.Column(db.String(100))
role = db.Column(db.String(20))
roleapproved = db.Column(db.Boolean)
school = db.Column(db.String(100))
email = db.Column(db.String(120), unique=True)
emailconfirmed = db.Column(db.Boolean)
pwdhash = db.Column(db.String(100))
def __init__(self, firstname, lastname, role, school, email, password):
self.firstname = firstname.title()
self.lastname = lastname.title()
self.role = role.lower()
if role.lower() == 'student':
self.roleapproved = True
if role.lower() == 'teacher':
self.roleapproved = False
self.school = school.title()
self.email = email.lower()
self.set_password(password)
self.emailconfirmed = False
def set_password(self, password):
self.pwdhash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.pwdhash, password)
def __repr__(self):
return '<User {0}>'.format(self.email)
Any help on doing the update I mentioned above would be greatly appreciated!!
Ideally you want to maintain a single session throughout your application lifecycle. This way it makes it easy to reason about and you avoid binding sessions to individual models.
Thanks #Ilja Everila
In main.py instead of initializing SQLAlchemy
you should write,
db.init_app(app)
Define a save instance method for your User model.
def save(self):
"""Saves model object instance
"""
db.session.add(self)
db.session.commit()
You can call this method to save the instance as
update_this.save()
Another way to update the entity is to get the specific object session before committing
from sqlachemy.orm import session
...
session = session.object_session(update_this)
session.commit()

Override Flask-Security's /login endpoint

I'm working on a site where the users login via OAuth, not a password-based system.
Because of this, Flask-Security's default login page doesn't actually work for my use case, because I need the /login endpoint for the OAuth setup. I was able to make it so that my /login route wasn't being overridden by Flask-Security's by changing the SECURITY_LOGIN_URL setting option.
This is all working fine, the OAuth login page shows up and returns all the information needed.
The problem kicks in because I'm also trying to utilize the #login_required decorator.
If the user isn't logged in, instead of redirecting to my /login page, the #login_required decorator is redirecting to Flask-Security's page.
Obviously, the config endpoint doesn't help in this situation.
Is it possible to force Flask-Security to use my login route (OAuth) instead of its page?
Here's some example code that shows what I'm talking about with Flask-Security overriding defined routes:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, request
from flask_security import (Security, SQLAlchemyUserDatastore,
UserMixin, RoleMixin, login_required,
login_user, logout_user, current_user)
from passlib.context import CryptContext
import os
class Config:
basedir = os.path.abspath(os.path.dirname(__file__))
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
SQLALCHEMY_TRACK_MODIFICATIONS = False
DEBUG = True
PORT = 8000
HOST = "0.0.0.0"
SECRET_KEY = "foobar123"
config = Config()
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(config)
db = SQLAlchemy(app)
lm = LoginManager()
lm.init_app(app)
lm.login_view = "login"
roles_users = db.Table('roles_users',
db.Column('user_id',
db.Integer(),
db.ForeignKey('user.id')),
db.Column('role_id',
db.Integer(),
db.ForeignKey('role.id'))
)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
provider_id = db.Column(db.String(64), index=True, unique=True)
#property
def is_authenticated(self):
return True
#property
def is_active(self):
return True
#property
def is_anonymous(self):
return True
def get_id(self):
return str(self.id)
def hash_password(self, password):
self.hashed_password = pwd_context.encrypt(password)
def verify_password(self, password):
return pwd_context.verify(password, self.hashed_password)
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
pwd_context = CryptContext(
schemes=["bcrypt", "pbkdf2_sha256", "des_crypt"],
default="bcrypt",
all__vary_rounds=0.1
)
#lm.user_loader
def load_user(id):
return User.query.get(int(id))
#app.route("/")
#app.route("/index")
#login_required
def index():
"""Handles calls to / and /index, return the panel"""
return render_template("index.html")
#app.route("/login")
def login():
"""Would include a bunch of OAuth stuff, not required for this example
If you try going to this endpoint, you'll get the Flask-Security /login
instead."""
return render_template("login.html")
app.run(debug=True)
Funnily enough, I've come across a very similar problem today.
If I manage to resolve it in a way I'd consider elegant enough for an SO answer, I'll update this. In the meantime, my thoughts on strategies for approaching the problem are:
Subclass flask-security and overload only the #login_required decorator to redirect you to the right place. Probably quickest, if the redirection is the only issue you're having.
If you're solely using Oauth, then use an alternative decorator to replace #login_required. Flask-OAuthlib is a useful library for oath stuff and the documents show you how to protect a resource using the `#oauth.require_oauth' decorator.
Flask-Stormpath is a commercial solution and I'm not familiar enough to comment on whether it covers this particular ground, so I shan't recommend it as a possible approach. However for background reading they have a useful overview of the authentication hornets nest associated with Flask.
You may want to override the original Flask-Security's login view. Something like this:
my_blueprint = Blueprint('my_blueprint', __name__)
#bp.route('/login', methods=['GET', 'POST'])
#anonymous_user_required
def my_login_view():
# Do whatever you want to do here. Try to find inspiration in the original code
app = Flask(__name__)
app.register_blueprint(my_blueprint)
# You will need to define User and Role classes, see Flask-Security's documentation
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
And that's it. If you want to use the Flask-Security's original HTML template, you will need to override it as well (see Flask-Security's documentation). There you just place your own view instead of the old one:
...
<form action="{{ url_for('my_blueprint.my_login') }}" method="POST" name="login_user_form">
...

Categories

Resources