Skip to content

Commit d8eada2

Browse files
committed
Fix Sonarqube warnings
1 parent 857798d commit d8eada2

3 files changed

Lines changed: 40 additions & 48 deletions

File tree

mod_api/middleware/auth.py

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,21 @@
1919
from mod_api.middleware.error_handler import make_error_response
2020
from mod_api.models.api_token import ApiToken
2121

22+
# Reused across every 401 response to keep the message consistent.
23+
_AUTH_FAILED_MSG = 'Bearer token is missing, expired, or invalid.'
24+
2225
# These endpoints bypass auth entirely.
2326
_PUBLIC_ENDPOINTS = frozenset([
2427
'api.create_token', # POST /auth/tokens (uses email/password body)
2528
'api.system_health', # GET /system/health (uptime monitoring)
2629
])
2730

2831

32+
def _unauthorized():
33+
"""Shorthand for a 401 response with the standard auth failure message."""
34+
return make_error_response('unauthorized', _AUTH_FAILED_MSG, http_status=401)
35+
36+
2937
@mod_api.before_request
3038
def authenticate_request():
3139
"""Validate Bearer token and attach user context to the request."""
@@ -36,38 +44,22 @@ def authenticate_request():
3644

3745
auth_header = request.headers.get('Authorization', '')
3846
if not auth_header:
39-
return make_error_response(
40-
'unauthorized',
41-
'Bearer token is missing, expired, or invalid.',
42-
http_status=401,
43-
)
47+
return _unauthorized()
4448

4549
parts = auth_header.split(' ', 1)
4650
if len(parts) != 2 or parts[0] != 'Bearer':
47-
return make_error_response(
48-
'unauthorized',
49-
'Bearer token is missing, expired, or invalid.',
50-
http_status=401,
51-
)
51+
return _unauthorized()
5252

5353
token_value = parts[1].strip()
5454
if not token_value or not token_value.startswith('spci_'):
55-
return make_error_response(
56-
'unauthorized',
57-
'Bearer token is missing, expired, or invalid.',
58-
http_status=401,
59-
)
55+
return _unauthorized()
6056

6157
# Look up by prefix, then verify the full hash against each candidate.
6258
prefix = ApiToken.extract_prefix(token_value)
6359
candidates = ApiToken.query.filter_by(token_prefix=prefix).all()
6460

6561
if not candidates:
66-
return make_error_response(
67-
'unauthorized',
68-
'Bearer token is missing, expired, or invalid.',
69-
http_status=401,
70-
)
62+
return _unauthorized()
7163

7264
matched_token = None
7365
for candidate in candidates:
@@ -76,18 +68,10 @@ def authenticate_request():
7668
break
7769

7870
if matched_token is None:
79-
return make_error_response(
80-
'unauthorized',
81-
'Bearer token is missing, expired, or invalid.',
82-
http_status=401,
83-
)
71+
return _unauthorized()
8472

8573
if not matched_token.is_valid:
86-
return make_error_response(
87-
'unauthorized',
88-
'Bearer token is missing, expired, or invalid.',
89-
http_status=401,
90-
)
74+
return _unauthorized()
9175

9276
g.api_token = matched_token
9377
g.api_user = matched_token.user
@@ -100,11 +84,7 @@ def decorator(f):
10084
def decorated_function(*args, **kwargs):
10185
token = getattr(g, 'api_token', None)
10286
if token is None:
103-
return make_error_response(
104-
'unauthorized',
105-
'Bearer token is missing, expired, or invalid.',
106-
http_status=401,
107-
)
87+
return _unauthorized()
10888
if not token.has_scope(scope):
10989
return make_error_response(
11090
'forbidden',
@@ -127,11 +107,7 @@ def decorator(f):
127107
def decorated_function(*args, **kwargs):
128108
user = getattr(g, 'api_user', None)
129109
if user is None:
130-
return make_error_response(
131-
'unauthorized',
132-
'Bearer token is missing, expired, or invalid.',
133-
http_status=401,
134-
)
110+
return _unauthorized()
135111
if user.role.value not in roles:
136112
return make_error_response(
137113
'forbidden',

mod_api/middleware/error_handler.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
from mod_api import mod_api
1616

17+
# All error handlers check this prefix before intercepting — non-API
18+
# routes (the legacy web UI) should still get their HTML error pages.
19+
_API_PREFIX = '/api/v1'
20+
1721

1822
def make_error_response(code, message, details=None, http_status=400):
1923
"""Build a JSON error response conforming to the ErrorResponse schema."""
@@ -27,10 +31,15 @@ def make_error_response(code, message, details=None, http_status=400):
2731
return response
2832

2933

34+
def _is_api_request():
35+
"""Check whether the current request targets an API endpoint."""
36+
return request.path.startswith(_API_PREFIX)
37+
38+
3039
@mod_api.app_errorhandler(400)
3140
def handle_400(error):
3241
"""Bad request."""
33-
if not request.path.startswith('/api/v1'):
42+
if not _is_api_request():
3443
raise error
3544
return make_error_response(
3645
'validation_error',
@@ -42,7 +51,7 @@ def handle_400(error):
4251
@mod_api.app_errorhandler(401)
4352
def handle_401(error):
4453
"""Unauthorized."""
45-
if not request.path.startswith('/api/v1'):
54+
if not _is_api_request():
4655
raise error
4756
return make_error_response(
4857
'unauthorized',
@@ -54,7 +63,7 @@ def handle_401(error):
5463
@mod_api.app_errorhandler(403)
5564
def handle_403(error):
5665
"""Forbidden."""
57-
if not request.path.startswith('/api/v1'):
66+
if not _is_api_request():
5867
raise error
5968
return make_error_response(
6069
'forbidden',
@@ -66,7 +75,7 @@ def handle_403(error):
6675
@mod_api.app_errorhandler(404)
6776
def handle_404(error):
6877
"""Not found."""
69-
if not request.path.startswith('/api/v1'):
78+
if not _is_api_request():
7079
raise error
7180
return make_error_response(
7281
'not_found',
@@ -78,7 +87,7 @@ def handle_404(error):
7887
@mod_api.app_errorhandler(405)
7988
def handle_405(error):
8089
"""Method not allowed."""
81-
if not request.path.startswith('/api/v1'):
90+
if not _is_api_request():
8291
raise error
8392
return make_error_response(
8493
'method_not_allowed',
@@ -90,7 +99,7 @@ def handle_405(error):
9099
@mod_api.app_errorhandler(422)
91100
def handle_422(error):
92101
"""Unprocessable entity."""
93-
if not request.path.startswith('/api/v1'):
102+
if not _is_api_request():
94103
raise error
95104
return make_error_response(
96105
'unprocessable',
@@ -102,7 +111,7 @@ def handle_422(error):
102111
@mod_api.app_errorhandler(429)
103112
def handle_429(error):
104113
"""Rate limited."""
105-
if not request.path.startswith('/api/v1'):
114+
if not _is_api_request():
106115
raise error
107116
return make_error_response(
108117
'rate_limited',
@@ -115,7 +124,7 @@ def handle_429(error):
115124
@mod_api.app_errorhandler(500)
116125
def handle_500(error):
117126
"""Internal server error."""
118-
if not request.path.startswith('/api/v1'):
127+
if not _is_api_request():
119128
raise error
120129
return make_error_response(
121130
'internal_error',

mod_api/middleware/validation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def validate_body(schema_class):
3535
def decorator(f):
3636
@wraps(f)
3737
def decorated(*args, **kwargs):
38+
content_type = request.content_type or ''
39+
if 'application/json' not in content_type:
40+
return make_error_response(
41+
'validation_error',
42+
'Content-Type must be application/json.',
43+
http_status=415,
44+
)
3845
json_data = request.get_json(silent=True)
3946
if json_data is None:
4047
return make_error_response(

0 commit comments

Comments
 (0)