10 Commits

Author SHA1 Message Date
2de770aed0 Advanced to version 0.3.1. 2022-12-29 23:49:28 +08:00
9ab413d583 Added the test_disabled test to the FlaskLoginTestCase test case. 2022-12-29 23:44:02 +08:00
aeb93a60e5 Fixed to store the auth state in request instead of the g global object in the flask_login load_user_from_request and unauthorized handlers in the init_app method of the DigestAuth class. This is so that the auth state is always reset in the lifecycle of request even if g stays. Revised the unauthorized to create a new auth state if it is not available in the current request, in the case that the load_user_from_request handler was not run previously. 2022-12-29 23:43:35 +08:00
a07118ef9c Revised the documentation for digest_auth parameter of the open method in the test client, to be clear. 2022-12-07 18:59:41 +08:00
514e9255aa Replaced "my_user" and "my_pass" with USERNAME and PASSWORD in the examples of the test client in the documentation, to avoid GitGuardian from detecting them as real passwords. 2022-12-07 18:55:52 +08:00
79abdc9cde Fixed the documentation of the login_required decorator in the DigestAuth class. 2022-12-07 18:48:39 +08:00
038e7a8352 Removed the warnings in the documentation of the test client. It is API document now. All content, for public or not, are available. There is no need to warn now. 2022-12-07 18:45:06 +08:00
0387abb4f6 Revised the documentation in the "flask_digest_auth.auth" module. 2022-12-07 18:39:20 +08:00
10e8add9e6 Replaced the manually-added package content with the automatically-generated package content, and added the modules list to the documentation. 2022-12-07 18:11:35 +08:00
c004e28c37 Fixed the documentation of the AuthState class and the UnauthorizedException exception. 2022-12-07 16:08:07 +08:00
11 changed files with 126 additions and 50 deletions

View File

@ -350,7 +350,7 @@ A unittest Test Case
response = self.client.get("/admin")
self.assertEqual(response.status_code, 401)
response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass"))
"/admin", digest_auth=(USERNAME, PASSWORD))
self.assertEqual(response.status_code, 200)
@ -382,7 +382,7 @@ A pytest Test
response = client.get("/admin")
assert response.status_code == 401
response = client.get(
"/admin", digest_auth=("my_name", "my_pass"))
"/admin", digest_auth=(USERNAME, PASSWORD))
assert response.status_code == 200

View File

@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
project = 'Flask-Digest-Auth'
copyright = '2022, imacat'
author = 'imacat'
release = '0.3.0'
release = '0.3.1'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -229,7 +229,7 @@ A unittest Test Case
response = self.client.get("/admin")
self.assertEqual(response.status_code, 401)
response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass"))
"/admin", digest_auth=(USERNAME, PASSWORD))
self.assertEqual(response.status_code, 200)
@ -264,5 +264,5 @@ A pytest Test
response = client.get("/admin")
assert response.status_code == 401
response = client.get(
"/admin", digest_auth=("my_name", "my_pass"))
"/admin", digest_auth=(USERNAME, PASSWORD))
assert response.status_code == 200

View File

@ -1,24 +1,37 @@
flask\_digest\_auth package
===========================
The ``DigestAuth`` Class
------------------------
.. autoclass:: flask_digest_auth.DigestAuth
Submodules
----------
flask\_digest\_auth.algo module
-------------------------------
.. automodule:: flask_digest_auth.algo
:members:
:undoc-members:
:show-inheritance:
The ``make_password_hash`` Function
-----------------------------------
.. autofunction:: flask_digest_auth.make_password_hash
flask\_digest\_auth.auth module
-------------------------------
The ``calc_response`` Function
------------------------------
.. autofunction:: flask_digest_auth.calc_response
The ``Client`` Test Class
-------------------------
.. autoclass:: flask_digest_auth.Client
.. automodule:: flask_digest_auth.auth
:members:
:undoc-members:
:show-inheritance:
flask\_digest\_auth.test module
-------------------------------
.. automodule:: flask_digest_auth.test
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: flask_digest_auth
:members:
:undoc-members:
:show-inheritance:

View File

@ -26,6 +26,7 @@ Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _HTTP Digest Authentication: https://en.wikipedia.org/wiki/Digest_access_authentication

View File

@ -63,7 +63,7 @@ The username is part of the hash. If the user changes their username,
you need to ask their password, to generate and store the new password
hash.
See :meth:`flask_digest_auth.make_password_hash`.
See :func:`flask_digest_auth.algo.make_password_hash`.
Flask-Digest-Auth Alone
@ -118,7 +118,7 @@ logging the log in event, adding the log in counter, etc.
def on_login(user: User) -> None:
user.visits = user.visits + 1
See :meth:`flask_digest_auth.DigestAuth.register_on_login`.
See :meth:`flask_digest_auth.auth.DigestAuth.register_on_login`.
Log Out
@ -127,7 +127,7 @@ Log Out
Flask-Digest-Auth supports log out. The user will be prompted for the
new username and password.
See :meth:`flask_digest_auth.DigestAuth.logout`.
See :meth:`flask_digest_auth.auth.DigestAuth.logout`.
Test Client
@ -136,7 +136,7 @@ Test Client
Flask-Digest-Auth comes with a test client that supports HTTP digest
authentication.
See :class:`flask_digest_auth.Client`.
See :class:`flask_digest_auth.test.Client`.
Also see :ref:`example-unittest` and :ref:`example-pytest`.

7
docs/source/modules.rst Normal file
View File

@ -0,0 +1,7 @@
src
===
.. toctree::
:maxdepth: 4
flask_digest_auth

View File

@ -17,7 +17,7 @@
[metadata]
name = flask-digest-auth
version = 0.3.0
version = 0.3.1
author = imacat
author_email = imacat@mail.imacat.idv.tw
description = The Flask HTTP Digest Authentication project.

View File

@ -16,8 +16,9 @@
# limitations under the License.
"""The HTTP Digest Authentication.
See RFC 2617 HTTP Authentication: Basic and Digest Access Authentication
See `RFC 2617`_ HTTP Authentication: Basic and Digest Access Authentication
.. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617
"""
from __future__ import annotations
@ -69,7 +70,7 @@ class DigestAuth:
"""The callback to run when the user logs in."""
def login_required(self, view) -> t.Callable:
"""The view decorator for HTTP digest authentication.
"""The view decorator for the HTTP digest authentication.
:Example:
@ -355,10 +356,13 @@ class DigestAuth:
:return: None.
"""
state: AuthState = request.digest_auth_state \
if hasattr(request, "digest_auth_state") \
else AuthState()
response: Response = Response()
response.status = 401
response.headers["WWW-Authenticate"] \
= self.__make_response_header(g.digest_auth_state)
= self.__make_response_header(state)
abort(response)
@login_manager.request_loader
@ -369,7 +373,7 @@ class DigestAuth:
:return: The authenticated user, or None if the
authentication fails
"""
g.digest_auth_state = AuthState()
request.digest_auth_state = AuthState()
authorization: Authorization = req.authorization
try:
if authorization is None:
@ -377,7 +381,7 @@ class DigestAuth:
if authorization.type != "digest":
raise UnauthorizedException(
"Not an HTTP digest authorization")
self.__authenticate(g.digest_auth_state)
self.__authenticate(request.digest_auth_state)
user = login_manager.user_callback(
authorization.username)
login_user(user)
@ -417,21 +421,30 @@ class DigestAuth:
class AuthState:
"""The authorization state."""
"""The authentication state. It keeps the status in the earlier
authentication stage, so that the latter response stage knows how to
response.
"""
def __init__(self):
"""Constructs the authorization state."""
self.opaque: t.Optional[str] = None
"""The opaque value specified by the client, if valid."""
self.stale: t.Optional[bool] = None
"""The stale value, if there is a previous log in attempt."""
class UnauthorizedException(Exception):
"""The exception thrown when the authentication is failed."""
pass
"""The exception thrown when the authentication fails."""
class BasePasswordHashGetter:
"""The base password hash getter."""
"""The base callback that given the username, returns the password hash,
or None if the user does not exist. The default is to raise an
:class:`UnboundLocalError` if the callback is not registered yet.
See :meth:`flask_digest_auth.auth.DigestAuth.register_get_password`
"""
@staticmethod
def __call__(username: str) -> t.Optional[str]:
@ -447,7 +460,12 @@ class BasePasswordHashGetter:
class BaseUserGetter:
"""The base user getter."""
"""The base callback that given the username, returns the user, or None if
the user does not exist. The default is to raise an
:class:`UnboundLocalError` if the callback is not registered yet.
See :meth:`flask_digest_auth.auth.DigestAuth.register_get_user`
"""
@staticmethod
def __call__(username: str) -> t.Optional[t.Any]:
@ -463,7 +481,11 @@ class BaseUserGetter:
class BaseOnLogInCallback:
"""The base callback when the user logs in."""
"""The base callback to run when the user logs in, given the logged-in
user. The default does nothing.
See :meth:`flask_digest_auth.auth.DigestAuth.register_on_login`
"""
@staticmethod
def __call__(user: t.Any) -> None:

View File

@ -51,7 +51,7 @@ class Client(WerkzeugClient):
response = self.client.get("/admin")
self.assertEqual(response.status_code, 401)
response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass"))
"/admin", digest_auth=(USERNAME, PASSWORD))
self.assertEqual(response.status_code, 200)
For pytest_:
@ -76,7 +76,7 @@ class Client(WerkzeugClient):
response = client.get("/admin")
assert response.status_code == 401
response = client.get(
"/admin", digest_auth=("my_name", "my_pass"))
"/admin", digest_auth=(USERNAME, PASSWORD))
assert response.status_code == 200
.. _unittest: https://docs.python.org/3/library/unittest.html
@ -87,12 +87,8 @@ class Client(WerkzeugClient):
**kwargs) -> TestResponse:
"""Opens a request.
.. warning::
This is to override the parent ``open`` method. You should call
the ``get``, ``post``, ``put``, and ``delete`` methods instead.
:param args: The arguments.
:param digest_auth: A tuple of the username and password for the HTTP
:param digest_auth: The (*username*, *password*) tuple for the HTTP
digest authentication.
:param kwargs: The keyword arguments.
:return: The response.
@ -115,9 +111,6 @@ class Client(WerkzeugClient):
username: str, password: str) -> Authorization:
"""Composes and returns the request authorization.
.. warning::
This method is not for public.
:param www_authenticate: The ``WWW-Authenticate`` response.
:param uri: The request URI.
:param username: The username.

View File

@ -45,7 +45,6 @@ class User:
self.password_hash: str = make_password_hash(
_REALM, username, password)
self.visits: int = 0
self.is_authenticated: bool = True
self.is_active: bool = True
self.is_anonymous: bool = False
@ -57,6 +56,16 @@ class User:
"""
return self.username
@property
def is_authenticated(self) -> bool:
"""Returns whether the user is authenticated.
This is required by Flask-Login.
This should return self.is_active.
:return: True if the user is active, or False otherwise.
"""
return self.is_active
class FlaskLoginTestCase(TestCase):
"""The test case with the Flask-Login integration."""
@ -256,3 +265,34 @@ class FlaskLoginTestCase(TestCase):
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(self.user.visits, 2)
def test_disabled(self) -> None:
"""Tests the disabled user.
:return: None.
"""
if not self.has_flask_login:
self.skipTest("Skipped without Flask-Login.")
response: Response
self.user.is_active = False
response = self.client.get(self.app.url_for("admin-1"))
self.assertEqual(response.status_code, 401)
response = self.client.get(self.app.url_for("admin-1"),
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 401)
self.user.is_active = True
response = self.client.get(self.app.url_for("admin-1"),
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 200)
response = self.client.get(self.app.url_for("admin-1"))
self.assertEqual(response.status_code, 200)
self.user.is_active = False
response = self.client.get(self.app.url_for("admin-1"))
self.assertEqual(response.status_code, 401)
response = self.client.get(self.app.url_for("admin-1"),
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 401)