Revised so that Flask-Login become an optional dependency.

This commit is contained in:
依瑪貓 2022-11-24 01:02:51 +11:00
parent 292d0aaf09
commit 2770e1cc12
5 changed files with 78 additions and 84 deletions

View File

@ -40,10 +40,14 @@ package_dir =
packages = find: packages = find:
python_requires = >=3.10 python_requires = >=3.10
install_requires = install_requires =
flask-login flask
tests_require = tests_require =
unittest unittest
flask-testing flask-testing
[options.packages.find] [options.packages.find]
where = src where = src
[options.extras_require]
flask_login =
flask-login

View File

@ -20,5 +20,4 @@
""" """
from flask_digest_auth.algo import make_password_hash, calc_response from flask_digest_auth.algo import make_password_hash, calc_response
from flask_digest_auth.auth import DigestAuth from flask_digest_auth.auth import DigestAuth
from flask_digest_auth.flask_login import init_login_manager
from flask_digest_auth.test import Client from flask_digest_auth.test import Client

View File

@ -27,7 +27,7 @@ from functools import wraps
from random import random from random import random
from secrets import token_urlsafe from secrets import token_urlsafe
from flask import g, request, Response, session, abort from flask import g, request, Response, session, abort, Flask, Request
from itsdangerous import URLSafeTimedSerializer, BadData from itsdangerous import URLSafeTimedSerializer, BadData
from werkzeug.datastructures import Authorization from werkzeug.datastructures import Authorization
@ -195,6 +195,63 @@ class DigestAuth:
""" """
self.__get_user = func self.__get_user = func
def init_app(self, app: Flask) -> None:
"""Initializes the Flask application.
:param app: The Flask application.
:return: None.
"""
try:
from flask_login import LoginManager, login_user
if not hasattr(app, "login_manager"):
raise AttributeError(
"Please run the Flask-Login init-app() first")
login_manager: LoginManager = getattr(app, "login_manager")
@login_manager.unauthorized_handler
def unauthorized() -> None:
"""Handles when the user is unauthorized.
:return: None.
"""
response: Response = Response()
response.status = 401
response.headers["WWW-Authenticate"] \
= self.make_response_header(g.digest_auth_state)
abort(response)
@login_manager.request_loader
def load_user_from_request(req: Request) -> t.Optional[t.Any]:
"""Loads the user from the request header.
:param req: The request.
:return: The authenticated user, or None if the
authentication fails
"""
g.digest_auth_state = AuthState()
authorization: Authorization = req.authorization
try:
if authorization is None:
raise UnauthorizedException
if authorization.type != "digest":
raise UnauthorizedException(
"Not an HTTP digest authorization")
self.authenticate(g.digest_auth_state)
user = login_manager.user_callback(
authorization.username)
login_user(user)
return user
except UnauthorizedException as e:
if str(e) != "":
app.logger.warning(str(e))
return None
except ModuleNotFoundError:
raise ModuleNotFoundError(
"init_app() is only for Flask-Login integration")
class AuthState: class AuthState:
"""The authorization state.""" """The authorization state."""

View File

@ -1,74 +0,0 @@
# The Flask HTTP Digest Authentication Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/14
# Copyright (c) 2022 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Flask-Login integration.
"""
import typing as t
from flask import Response, abort, current_app, Request, g
from flask_login import LoginManager, login_user
from werkzeug.datastructures import Authorization
from flask_digest_auth.auth import DigestAuth, AuthState
from flask_digest_auth.exception import UnauthorizedException
def init_login_manager(auth: DigestAuth, login_manager: LoginManager) -> None:
"""Initialize the login manager.
:param auth: The HTTP digest authentication.
:param login_manager: The login manager from FlaskLogin.
:return: None.
"""
@login_manager.unauthorized_handler
def unauthorized() -> None:
"""Handles when the user is unauthorized.
:return: None.
"""
response: Response = Response()
response.status = 401
response.headers["WWW-Authenticate"] = auth.make_response_header(
g.digest_auth_state)
abort(response)
@login_manager.request_loader
def load_user_from_request(request: Request) -> t.Optional[t.Any]:
"""Loads the user from the request header.
:param request: The request.
:return: The authenticated user, or None if the authentication fails
"""
g.digest_auth_state = AuthState()
authorization: Authorization = request.authorization
try:
if authorization is None:
raise UnauthorizedException
if authorization.type != "digest":
raise UnauthorizedException(
"Not an HTTP digest authorization")
auth.authenticate(g.digest_auth_state)
user = login_manager.user_callback(authorization.username)
login_user(user)
return user
except UnauthorizedException as e:
if str(e) != "":
current_app.logger.warning(str(e))
return None

View File

@ -19,15 +19,13 @@
""" """
import typing as t import typing as t
import unittest
from secrets import token_urlsafe from secrets import token_urlsafe
import flask_login
from flask import Response, Flask from flask import Response, Flask
from flask_login import LoginManager
from flask_testing import TestCase from flask_testing import TestCase
from flask_digest_auth import DigestAuth, make_password_hash, Client, \ from flask_digest_auth import DigestAuth, make_password_hash, Client
init_login_manager
_REALM: str = "testrealm@host.com" _REALM: str = "testrealm@host.com"
_USERNAME: str = "Mufasa" _USERNAME: str = "Mufasa"
@ -53,7 +51,7 @@ class User:
class FlaskLoginTestCase(TestCase): class FlaskLoginTestCase(TestCase):
"""The test case with the Flask-Login integration.""" """The test case with the Flask-Login integration."""
def create_app(self): def create_app(self) -> Flask:
"""Creates the Flask application. """Creates the Flask application.
:return: The Flask application. :return: The Flask application.
@ -65,11 +63,18 @@ class FlaskLoginTestCase(TestCase):
}) })
app.test_client_class = Client app.test_client_class = Client
login_manager: LoginManager = LoginManager() self.has_flask_login: bool = True
try:
import flask_login
except ModuleNotFoundError:
self.has_flask_login = False
return app
login_manager: flask_login.LoginManager = flask_login.LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
auth: DigestAuth = DigestAuth(realm=_REALM) auth: DigestAuth = DigestAuth(realm=_REALM)
init_login_manager(auth, login_manager) auth.init_app(app)
user_db: t.Dict[str, str] \ user_db: t.Dict[str, str] \
= {_USERNAME: make_password_hash(_REALM, _USERNAME, _PASSWORD)} = {_USERNAME: make_password_hash(_REALM, _USERNAME, _PASSWORD)}
@ -117,6 +122,9 @@ class FlaskLoginTestCase(TestCase):
:return: None. :return: None.
""" """
if not self.has_flask_login:
self.skipTest("Skipped testing Flask-Login integration without it.")
response: Response = self.client.get(self.app.url_for("auth-1")) response: Response = self.client.get(self.app.url_for("auth-1"))
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
response = self.client.get( response = self.client.get(