miguelgrinberg / Flask-HTTPAuth

Simple extension that provides Basic, Digest and Token HTTP authentication for Flask routes
MIT License
1.27k stars 228 forks source link

flask-restless/flask-admin integration #123

Closed reubano closed 3 years ago

reubano commented 3 years ago

It seems like before_request only works with app...

works as expected

@app.before_request
    @auth.login_required
    def before_request():
        pass

doesn't work

@blueprint.before_request
    @auth.login_required
    def before_request():
        pass

cr #106 and #107

reubano commented 3 years ago

Did some more investigation. It appears that rq-dashboard blueprints work, but flask-restless and the associated flask-admin blueprints don't.

Update: Here's the documentation for flask-restless authentication. It uses flask-login though, so I'll update once I figure out what changes are needed for this lib.

cr https://github.com/jfinkels/flask-restless/issues/513

reubano commented 3 years ago

I copied over the flask-restless/flask-login example and came up with this...

"""
    Authentication example using Flask-HTTPAuth
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    This provides a simple example of using Flask-HTTPAuth as the authentication
    framework which can guard access to certain API endpoints.
    This requires the following Python libraries to be installed:
    * Flask
    * Flask-HTTPAuth
    * Flask-Restless
    * Flask-SQLAlchemy

    To install them using ``pip``, do::
        pip install Flask Flask-SQLAlchemy Flask-Restless Flask-HTTPAuth

    To use this example, run this package from the command-line::
        python -m authentication

    Attempts to access the URL of the API for the :class:`User` class at
    ``http://localhost:5000/api/user`` will prompt you to authenticate with username
    ``admin`` and password ``admin``.

    :copyright: 2012 Jeffrey Finkelstein <jeffrey.finkelstein@gmail.com>
    :copyright: 2021 Reuben Cummings <rcummings@nerevu.com>
    :license: GNU AGPLv3+ or BSD
"""
from os import urandom, path as p

from flask import Flask, redirect, session, request
from flask_httpauth import HTTPBasicAuth
from flask_restless import APIManager, ProcessingException
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.hybrid import hybrid_property
from werkzeug.security import generate_password_hash, check_password_hash

# Step 0: the database in this example is at './test.db'.
PARENT_DIR = p.abspath(p.dirname(__file__))
DB_PATH = p.join(PARENT_DIR, "test.db")

# Step 1: setup the Flask application.
app = Flask(__name__)
app.config["DEBUG"] = True
app.config["TESTING"] = True
app.config["SECRET_KEY"] = urandom(24)
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}?check_same_thread=False"

# Step 2: initialize extensions.
db = SQLAlchemy(app)
api_manager = APIManager(app, flask_sqlalchemy_db=db)
auth = HTTPBasicAuth()

# Step 3: create the user database model.
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.Unicode, unique=True, nullable=False)
    _password = db.Column("password", db.Unicode, nullable=False)

    @hybrid_property
    def password(self):
        return self._password

    @password.setter
    def password(self, password):
        self._password = generate_password_hash(password)

# Step 4: create the database and add a test user.
db.drop_all()
db.create_all()
user = User(username="admin", password="admin")
db.session.add(user)
db.session.commit()

# Step 5: create password validator.
@auth.verify_password
def verify_password(username, password):
    matched_user = User.query.filter_by(username=username).first()

    if matched_user and check_password_hash(matched_user.password, password):
        return matched_user

# Step 6: create the authenticate endpoint
@app.route("/authenticate")
@auth.login_required
def authenticate():
    username = auth.current_user().username
    session["current_username"] = username
    return redirect("/api/user", 307)

@app.before_request
def check_auth():
    if request.path != "/authenticate" and not session.get("current_username"):
        return redirect("/authenticate", 307)

# Step 7: create the API for User with the authentication guard.
def auth_func(*args, **kwargs):
    current_username = session.get("current_username")
    session["current_username"] = None

    if not current_username:
        raise ProcessingException(description="Not authenticated!", code=401)

preprocessors = {"GET_SINGLE": [auth_func], "GET_MANY": [auth_func]}
api_manager.create_api(User, preprocessors=preprocessors)

# Step 8: configure and run the application
app.run()

# Step 9: visit http://localhost:5000/api/user in a Web browser. You will
# be prompted to authenticate via basic authentication.
#
# Step 10: Enter username "admin" and password "admin". You will then be
# authenticated and get a response showing the objects in the User table of
# the database.
reubano commented 3 years ago

I copied over the flask-admin/flask-login example and came up with this...

from os import urandom, path as p

from flask import Flask, url_for, redirect, request, session
from flask_httpauth import HTTPBasicAuth
from flask_sqlalchemy import SQLAlchemy
from flask_admin import expose, Admin, AdminIndexView
from flask_admin.contrib.sqla import ModelView
from sqlalchemy.ext.hybrid import hybrid_property
from werkzeug.security import generate_password_hash, check_password_hash

PARENT_DIR = p.abspath(p.dirname(__file__))
DB_PATH = p.join(PARENT_DIR, "admin.db")

# Create Flask application
app = Flask(__name__)

# Create dummy secrey key so we can use sessions
app.config["SECRET_KEY"] = urandom(24)

# Create in-memory database
app.config["DEBUG"] = True
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}?check_same_thread=False"
app.config["SQLALCHEMY_ECHO"] = True
db = SQLAlchemy(app)
auth = HTTPBasicAuth()

# Create user model.
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.Unicode, unique=True, nullable=False)
    _password = db.Column("password", db.Unicode, nullable=False)

    @hybrid_property
    def password(self):
        return self._password

    @password.setter
    def password(self, password):
        self._password = generate_password_hash(password)

    def get_id(self):
        return self.id

    # Required for administrative interface
    def __unicode__(self):
        return self.username

# create password validator.
@auth.verify_password
def verify_password(username, password):
    matched_user = User.query.filter_by(username=username).first()

    if matched_user and check_password_hash(matched_user.password, password):
        return matched_user

# Create customized model view class
class MyModelView(ModelView):
    def is_accessible(self):
        return session.get("current_username")

    def inaccessible_callback(self, name, **kwargs):
        return redirect(url_for(".authenticate", next=request.url))

# Create customized index view class that handles authenticate
class MyAdminIndexView(AdminIndexView):
    @expose("/")
    def index(self):
        if session.get("current_username"):
            return super(MyAdminIndexView, self).index()
        else:
            return redirect(url_for(".authenticate"))

    @expose("/authenticate")
    @auth.login_required
    def authenticate(self):
        session["current_username"] = username = auth.current_user().username

        if username:
            return redirect(url_for(".index"))
        else:
            return super(MyAdminIndexView, self).index()

# Flask views
@app.route("/")
def index():
    return f"Welcome {session.get('current_username')}!"

# Create admin
admin = Admin(app, "Example: Auth", index_view=MyAdminIndexView())

# Add view
admin.add_view(MyModelView(User, db.session))

if __name__ == "__main__":
    # create db
    db.drop_all()
    db.create_all()
    user = User(username="admin", password="admin")
    db.session.add(user)
    db.session.commit()

    # Start app
    app.run(debug=True)