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
4 changes: 4 additions & 0 deletions server/controllers/api/auth/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
allow_when_password_required,
api_authenticated,
no_live_session,
redact_data_paths,
require_admin,
)


@ApiRoute("auth/create", ApiVersion.V1)
class UserCreateController(BaseAPIController):
@redact_data_paths(paths=["/password", "/confirmPassword"])
async def post(self):
with self.make_session() as session:
# If there are no users, allow creation without authentication, otherwise require admin.
Expand Down Expand Up @@ -139,6 +141,7 @@ async def post(self):

@ApiRoute("auth/login", ApiVersion.V1)
class LoginHandler(BaseAPIController):
@redact_data_paths(paths=["/password"])
async def post(self):
data = escape.json_decode(self.request.body)

Expand Down Expand Up @@ -274,6 +277,7 @@ class PasswordChangeController(BaseAPIController):

@api_authenticated
@allow_when_password_required
@redact_data_paths(paths=["/old_password", "/new_password"])
async def patch(self):
"""
Change authenticated user's password.
Expand Down
8 changes: 8 additions & 0 deletions server/digi_server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,14 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
self._application.regen_logging,
display_name="Log Backups",
)
self.define(
"log_redaction",
bool,
False,
True,
display_name="Enable Log Redaction",
help_text="When enabled, potentially sensitive information will be redacted from logs.",
)
self.define(
"db_log_enabled",
bool,
Expand Down
3 changes: 2 additions & 1 deletion server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ marshmallow<5
pyjwt[crypto]==2.11.0
setuptools==80.10.2
xkcdpass==1.30.0
zeroconf==0.148.0
zeroconf==0.148.0
python-jsonpath==2.0.2
22 changes: 17 additions & 5 deletions server/utils/web/base_controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from copy import deepcopy
from typing import TYPE_CHECKING, Any, Awaitable, Optional

import bcrypt
Expand Down Expand Up @@ -179,14 +180,25 @@ def on_finish(self):
log_method = get_logger().debug

if self.request.body:
method_name = self.request.method.lower()
handler_method = getattr(self, method_name, None)
redacted_data_paths = getattr(handler_method, "_redacted_data_paths", None)
try:
log_method(
f"{self.request.method} "
f"{self.request.path} "
f"{escape.json_decode(self.request.body)}"
)
body = escape.json_decode(self.request.body)
except BaseException:
get_logger().debug(
f"{self.request.method} {self.request.path} {self.request.body}"
)
else:
if (
redacted_data_paths
and self.application.digi_settings.settings[
"log_redaction"
].get_value()
):
body = deepcopy(body)
redacted_data_paths.apply(body)

log_method(f"{self.request.method} {self.request.path} {body}")

super().on_finish()
27 changes: 26 additions & 1 deletion server/utils/web/web_decorators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import functools
from typing import Awaitable, Callable, Optional
from typing import Awaitable, Callable, List, Optional

from jsonpath import JSONPatch
from tornado.web import HTTPError

from utils.web.base_controller import BaseController
Expand Down Expand Up @@ -77,3 +78,27 @@ def wrapper(self: BaseController, *args, **kwargs) -> Optional[Awaitable[None]]:
# Mark the wrapper with an attribute so prepare() can detect it
wrapper._allow_when_password_required = True # type: ignore
return wrapper


def redact_data_paths(
paths: List[str],
) -> Callable[
[Callable[..., Optional[Awaitable[None]]]], Callable[..., Optional[Awaitable[None]]]
]:
patch = None
if paths:
patch = JSONPatch()
for path in paths:
patch.replace(path, "<-- REDACTED -->")

def decorator(
method: Callable[..., Optional[Awaitable[None]]],
) -> Callable[..., Optional[Awaitable[None]]]:
@functools.wraps(method)
def wrapper(self: BaseController, *args, **kwargs) -> Optional[Awaitable[None]]:
return method(self, *args, **kwargs)

wrapper._redacted_data_paths = patch
return wrapper

return decorator
Loading