Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 39 additions & 18 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,63 @@

Unreleased

- x
## Version 6.3.0

Released 2026-04-19

- fix missing `raise` on `scope_import is None` check in `process_folder_file_scope`
- use context manager for `multiprocessing.Pool` in `authenticate_password` to stop worker leak
- fix `disable_default_fail` semantics on `checkpoint_callable` — default now aborts with `fail_status`
when no fail handler is set (previously it silently called the protected view)
- use `secrets.compare_digest` for bearer token comparison in `BearerCheckpoint`
- mix `secrets.token_hex` entropy into `generate_csrf_token` output
- `ImpBlueprint.import_resources` now routes through `cast_to_import_str`, fixing import paths for
nested resource folders
- preserve order when deduping scoped import results (`dict.fromkeys` instead of `set`)
- URL-encode username/password when building database URIs to avoid breakage on special characters
- chain `ImportError` re-raises with `from e` to preserve the original traceback
- add testing for checkpoints

## Version 6.2.0

Released 2026-04-14

- auth.generate_private_key
- auth.authenticate_password
- auth.encrypt_password

The function argument `encryption_level` has been changed to `algorithm` and has a new type of **Literal**

This was done to allow for more algorithms to be added in the future.

## Version 6.1.5

Released 2026-02-22

- bugfix

## Version 6.1.4
## Version 6.1.4 - YANKED

Released 2026-02-21

- add disable_default_fail option to checkpoints

## Version 6.1.3

Released 2026-02-21

- adjust fail_response to accept a callable to avoid out of context error

## Version 6.1.2

Released 2026-02-21

- switch checkpoint type checking to protocol matching

## Version 6.1.1

Released 2026-02-21

- checkpoint bug fix, docs fix, bump version

## Version 6.1.0
Expand Down Expand Up @@ -51,16 +88,12 @@ Released 2025-10-21

## Version 6.0.0



Released 2025-10-16

- beta-3 + beta-2 + beta-1

## Version 6.0.0-beta.3



Released 2025-10-16

- Replaced `import_app_resources` with
Expand All @@ -80,34 +113,26 @@ Released 2025-10-16

## Version 6.0.0-beta.2



Released 2025-05-27

- bug fixes
- move checkpoints to package

## Version 6.0.0-beta.1



Released 2025-05-27

- Simplify `flask_imp.security.checkpoint` decorator by adding checkpoint types.

## Version 5.7.0



Released 2025-02-10

- add new method: `FlaskConfig.as_object`
- refactored _flask_config.py

## Version 5.6.0



Released 2025-02-04

- New method added to register ImpBlueprints
Expand All @@ -118,8 +143,6 @@ Released 2025-02-04

## Version 5.5.1



Released 2024-12-04

- switched logo for emoji
Expand All @@ -128,8 +151,6 @@ Released 2024-12-04

## Version 5.5.0



Released 2024-11-21

- updated project structure.
Expand Down
4 changes: 2 additions & 2 deletions src/flask_imp/_imp.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def new():
# add the module to the set of imported modules
imported_modules.add(module)
except ImportError as e:
raise ImportError(f"Error when importing {cast_import}: {e}")
raise ImportError(f"Error when importing {cast_import}: {e}") from e

# check if each module has any valid factories, if so, pass the blueprint
for instance_factory in factories:
Expand Down Expand Up @@ -392,7 +392,7 @@ def _process_model(self, path: Path) -> None:
self.model_registry.add(name, value)

except ImportError as e:
raise ImportError(f"Error when importing {import_string}: {e}")
raise ImportError(f"Error when importing {import_string}: {e}") from e

def _init_session(self) -> None:
if isinstance(self.config.IMP_INIT_SESSION, dict):
Expand Down
10 changes: 4 additions & 6 deletions src/flask_imp/_imp_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,15 @@ def new():
): # skip hidden files / folders
continue

cast_import = cast_to_import_str(self.package, module_path)

try:
# attempt to import the module
module = import_module(
f"{self.package}.{module_path.parent.name}.{module_path.stem}"
)
module = import_module(cast_import)
# add the module to the set of imported modules
imported_modules.add(module)
except ImportError as e:
raise ImportError(
f"Error when importing {self.package}.{module_path.parent.name}.{module_path.stem}: {e}"
)
raise ImportError(f"Error when importing {cast_import}: {e}") from e

# check if each module has any valid factories, if so, pass the blueprint
for instance_factory in factories:
Expand Down
6 changes: 3 additions & 3 deletions src/flask_imp/_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def process_folder_file_scope(
"""

if scope_import is None:
Exception("scope_import cannot be None")
raise ValueError("scope_import cannot be None")

result: list[Path] = []

Expand All @@ -394,5 +394,5 @@ def process_folder_file_scope(
if named_scopes := process_scope(resource, scope_import[resource.name]):
result.extend(named_scopes)

# clear duplicates
return list(set(result))
# clear duplicates while preserving order
return list(dict.fromkeys(result))
38 changes: 19 additions & 19 deletions src/flask_imp/auth/_authenticate_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,26 @@ def authenticate_password(

return False

thread_pool = multiprocessing.Pool(processes=pepper_length)
threads = []

for batch in batched(_guesses, 1000):
threads.append(
thread_pool.apply_async(
_guess_block,
args=(
batch,
input_password,
database_password,
database_salt,
algorithm,
pepper_position,
),
with multiprocessing.Pool(processes=pepper_length) as thread_pool:
threads = []

for batch in batched(_guesses, 1000):
threads.append(
thread_pool.apply_async(
_guess_block,
args=(
batch,
input_password,
database_password,
database_salt,
algorithm,
pepper_position,
),
)
)
)

for thread in threads:
if thread.get():
return True
for thread in threads:
if thread.get():
return True

return False
8 changes: 5 additions & 3 deletions src/flask_imp/auth/_generate_csrf_token.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import secrets
from datetime import datetime
from hashlib import sha1


def generate_csrf_token() -> str:
"""
Generates a SHA1 using the current date and time.
Generates a SHA1 using the current date and time combined with
cryptographically secure random bytes.

For use in Cross-Site Request Forgery.

:return: sha1 hash of the current date and time
:return: sha1 hash of the current date and time plus random entropy
"""
sha = sha1()
sha.update(str(datetime.now()).encode("utf-8"))
sha.update(f"{datetime.now()}{secrets.token_hex(16)}".encode("utf-8"))
return sha.hexdigest()
5 changes: 3 additions & 2 deletions src/flask_imp/config/_database_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typing as t
from pathlib import Path
from urllib.parse import quote


class DatabaseConfig:
Expand Down Expand Up @@ -114,7 +115,7 @@ def uri(self, app_instance_path: Path) -> str:
return f"{self.dialect}:///{filepath}"

return (
f"{self.dialect}://{self.username}:"
f"{self.password}@{self.location}:"
f"{self.dialect}://{quote(self.username, safe='')}:"
f"{quote(self.password, safe='')}@{self.location}:"
f"{self.port}/{self.database_name}"
)
5 changes: 3 additions & 2 deletions src/flask_imp/config/_sql_database_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typing as t
from urllib.parse import quote


class SQLDatabaseConfig:
Expand Down Expand Up @@ -66,7 +67,7 @@ def as_dict(self) -> t.Dict[str, t.Any]:

def uri(self) -> str:
return (
f"{self.dialect}://{self.username}:"
f"{self.password}@{self.location}:"
f"{self.dialect}://{quote(self.username, safe='')}:"
f"{quote(self.password, safe='')}@{self.location}:"
f"{self.port}/{self.database_name}"
)
2 changes: 1 addition & 1 deletion src/flask_imp/security/_checkpoint_callable.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def inner(*args: t.Any, **kwargs: t.Any) -> t.Any:

raise TypeError("Pass URL must either be a string or a partial")

if not disable_default_fail:
if disable_default_fail:
return func(*args, **kwargs)

return abort(fail_status)
Expand Down
7 changes: 6 additions & 1 deletion src/flask_imp/security/_checkpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

"""

import secrets
import typing as t

from flask import request
Expand Down Expand Up @@ -152,7 +153,11 @@ def __init__(

def pass_(self) -> bool:
if auth := request.authorization:
if auth.type == "bearer" and auth.token == self.token:
if (
auth.type == "bearer"
and auth.token is not None
and secrets.compare_digest(auth.token, self.token)
):
return True

return False
Expand Down
Loading
Loading