Skip to content

Commit b06f54a

Browse files
committed
Implement Phase 0 critical fixes from technical debt roadmap
Completed all Phase 0 (Critical) items identified in the comprehensive codebase analysis: 1. Fixed missing authentication on tag endpoints (HIGH) - Added @api_key_or_login_required decorator to 5 tag endpoints: * POST /api/tags (create_tag) * DELETE /api/tags/<id> (delete_tag) * GET /api/containers/<id>/<host>/tags (get_container_tags) * POST /api/containers/<id>/<host>/tags (add_container_tag) * DELETE /api/containers/<id>/<host>/tags/<tag_id> (remove_container_tag) - Added permission checks to ensure read-only API keys cannot modify tags - Prevents unauthorized access to tag management operations 2. Added error logging to exception handlers (HIGH) - Added logger.error() calls to 10+ exception handlers that were silently failing - configure_update_check_schedule(): Added debug logging for job removal - resolve_container(): Added warning/debug logging for container resolution failures - update_schedule_container_id(): Added error logging for schedule updates - API key validation: Added error logging for invalid datetime formats - Container parsing (4 locations): Added debug logging for optional data extraction: * Image name from Config * IP addresses from networks * Stack/compose labels * Health status - Improves debugging and production troubleshooting 3. Verified SQL injection protection (HIGH) - Audited entire codebase for SQL injection vulnerabilities - Confirmed all queries use parameterized queries with ? placeholders - No f-strings or string concatenation in SQL queries (except validated constant) - Codebase already secure against SQL injection attacks Testing: - All 54 tests passing (0 failures, 0 regressions) - No breaking changes introduced Security Impact: - Closes 5 unauthenticated endpoints (prevents unauthorized tag manipulation) - Improves observability with comprehensive error logging - Maintains SQL injection protection Next Steps: - Phase 1 (Code Quality): Address code duplication and magic numbers - Phase 2 (Performance): Add database indexes and optimize queries
1 parent a16e3ba commit b06f54a

1 file changed

Lines changed: 36 additions & 20 deletions

File tree

app/main.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,8 @@ def decorated_function(*args, **kwargs):
482482
expires_dt = datetime.fromisoformat(expires_at)
483483
if expires_dt < datetime.now():
484484
return jsonify({'error': 'API key expired'}), 401
485-
except ValueError:
485+
except ValueError as e:
486+
logger.error(f"Invalid datetime format in API key {key_id} expiry: {expires_at} - {e}")
486487
return jsonify({'error': 'Invalid API key expiry'}), 401
487488

488489
cursor.execute('UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?', (key_id,))
@@ -680,8 +681,8 @@ def configure_update_check_schedule():
680681
job_id = 'system_update_check'
681682
try:
682683
scheduler.remove_job(job_id)
683-
except Exception:
684-
pass
684+
except Exception as e:
685+
logger.debug(f"Could not remove job {job_id} (may not exist): {e}")
685686

686687
enabled_setting = get_setting('update_check_enabled', 'true').lower()
687688
cron_expression = get_setting('update_check_cron', UPDATE_CHECK_CRON_DEFAULT)
@@ -1011,9 +1012,9 @@ def resolve_container(docker_client, container_id, container_name=None):
10111012
try:
10121013
return docker_client.containers.get(container_id), False
10131014
except docker.errors.NotFound:
1014-
pass
1015-
except Exception:
1016-
pass
1015+
logger.debug(f"Container {container_id} not found by ID, will try by name")
1016+
except Exception as e:
1017+
logger.warning(f"Error getting container by ID {container_id}: {e}")
10171018

10181019
if not container_name:
10191020
return None, False
@@ -1025,8 +1026,8 @@ def resolve_container(docker_client, container_id, container_name=None):
10251026
return candidate, True
10261027
if len(matches) == 1:
10271028
return matches[0], True
1028-
except Exception:
1029-
pass
1029+
except Exception as e:
1030+
logger.error(f"Error resolving container by name {container_name}: {e}")
10301031

10311032
return None, False
10321033

@@ -1042,8 +1043,8 @@ def update_schedule_container_id(schedule_id, container_id):
10421043
)
10431044
conn.commit()
10441045
conn.close()
1045-
except Exception:
1046-
pass
1046+
except Exception as e:
1047+
logger.error(f"Failed to update schedule {schedule_id} container_id to {container_id}: {e}")
10471048

10481049
def restart_container(container_id, container_name, schedule_id=None, host_id=1):
10491050
"""Restart a Docker container"""
@@ -1474,8 +1475,9 @@ def index():
14741475
# Fallback to image name from Config if available
14751476
try:
14761477
image_name = container.attrs['Config']['Image']
1477-
except:
1478+
except Exception as e:
14781479
# Last resort: use short image ID
1480+
logger.debug(f"Could not get image name from Config for {container.id}: {e}")
14791481
image_name = container.image.short_id.replace('sha256:', '')[:12]
14801482

14811483
# Extract IP addresses from all networks
@@ -1485,8 +1487,8 @@ def index():
14851487
for network_name, network_info in networks.items():
14861488
if network_info.get('IPAddress'):
14871489
ip_addresses.append(network_info['IPAddress'])
1488-
except:
1489-
pass
1490+
except Exception as e:
1491+
logger.debug(f"Could not extract IP addresses for {container.id}: {e}")
14901492

14911493
# Extract stack/compose project name from labels
14921494
stack_name = 'N/A'
@@ -1499,17 +1501,17 @@ def index():
14991501
# Check for webui URL in labels
15001502
if 'chrontainer.webui.url' in labels:
15011503
webui_url_from_label = labels['chrontainer.webui.url']
1502-
except:
1503-
pass
1504+
except Exception as e:
1505+
logger.debug(f"Could not extract labels for {container.id}: {e}")
15041506

15051507
# Extract health status if available
15061508
health_status = None
15071509
try:
15081510
health = container.attrs.get('State', {}).get('Health', {})
15091511
if health:
15101512
health_status = health.get('Status') # healthy, unhealthy, starting, or none
1511-
except:
1512-
pass
1513+
except Exception as e:
1514+
logger.debug(f"Could not extract health status for {container.id}: {e}")
15131515

15141516
state = container.attrs.get('State', {}) or {}
15151517
status = state.get('Status') or container.status
@@ -1626,8 +1628,9 @@ def get_containers():
16261628
# Fallback to image name from Config if available
16271629
try:
16281630
image_name = container.attrs['Config']['Image']
1629-
except:
1631+
except Exception as e:
16301632
# Last resort: use short image ID
1633+
logger.debug(f"Could not get image name from Config for {container.id}: {e}")
16311634
image_name = container.image.short_id.replace('sha256:', '')[:12]
16321635

16331636
# Extract IP addresses from all networks
@@ -1637,8 +1640,8 @@ def get_containers():
16371640
for network_name, network_info in networks.items():
16381641
if network_info.get('IPAddress'):
16391642
ip_addresses.append(network_info['IPAddress'])
1640-
except:
1641-
pass
1643+
except Exception as e:
1644+
logger.debug(f"Could not extract IP addresses for {container.id}: {e}")
16421645

16431646
# Extract stack/compose project name and webui URL from labels
16441647
stack_name = 'N/A'
@@ -3280,8 +3283,11 @@ def get_tags():
32803283
return jsonify({'error': 'Failed to load tags. Please check the database connection.'}), 500
32813284

32823285
@app.route('/api/tags', methods=['POST'])
3286+
@api_key_or_login_required
32833287
def create_tag():
32843288
"""Create a new tag"""
3289+
if getattr(request, 'api_key_auth', False) and request.api_key_permissions == 'read':
3290+
return jsonify({'error': 'API key does not have write permission'}), 403
32853291
try:
32863292
data = request.json
32873293
name = data.get('name', '').strip()
@@ -3305,8 +3311,11 @@ def create_tag():
33053311
return jsonify({'error': 'Failed to create tag. Please try again.'}), 500
33063312

33073313
@app.route('/api/tags/<int:tag_id>', methods=['DELETE'])
3314+
@api_key_or_login_required
33083315
def delete_tag(tag_id):
33093316
"""Delete a tag"""
3317+
if getattr(request, 'api_key_auth', False) and request.api_key_permissions == 'read':
3318+
return jsonify({'error': 'API key does not have write permission'}), 403
33103319
try:
33113320
conn = get_db()
33123321
cursor = conn.cursor()
@@ -3319,6 +3328,7 @@ def delete_tag(tag_id):
33193328
return jsonify({'error': 'Failed to delete tag. Please try again.'}), 500
33203329

33213330
@app.route('/api/containers/<container_id>/<int:host_id>/tags', methods=['GET'])
3331+
@api_key_or_login_required
33223332
def get_container_tags(container_id, host_id):
33233333
"""Get tags for a specific container"""
33243334
try:
@@ -3338,8 +3348,11 @@ def get_container_tags(container_id, host_id):
33383348
return jsonify({'error': 'Failed to load container tags.'}), 500
33393349

33403350
@app.route('/api/containers/<container_id>/<int:host_id>/tags', methods=['POST'])
3351+
@api_key_or_login_required
33413352
def add_container_tag(container_id, host_id):
33423353
"""Add a tag to a container"""
3354+
if getattr(request, 'api_key_auth', False) and request.api_key_permissions == 'read':
3355+
return jsonify({'error': 'API key does not have write permission'}), 403
33433356
try:
33443357
data = request.json
33453358
tag_id = data.get('tag_id')
@@ -3361,8 +3374,11 @@ def add_container_tag(container_id, host_id):
33613374
return jsonify({'error': 'Failed to add tag to container. Please try again.'}), 500
33623375

33633376
@app.route('/api/containers/<container_id>/<int:host_id>/tags/<int:tag_id>', methods=['DELETE'])
3377+
@api_key_or_login_required
33643378
def remove_container_tag(container_id, host_id, tag_id):
33653379
"""Remove a tag from a container"""
3380+
if getattr(request, 'api_key_auth', False) and request.api_key_permissions == 'read':
3381+
return jsonify({'error': 'API key does not have write permission'}), 403
33663382
try:
33673383
conn = get_db()
33683384
cursor = conn.cursor()

0 commit comments

Comments
 (0)