Security hardening, multi-site portability, and a documentation and API-surface sweep
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import glob
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, current_app, send_from_directory
|
||||
from flask import Blueprint, request, current_app
|
||||
from flask_jwt_extended import jwt_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
@@ -112,6 +112,7 @@ def _require_computer_models():
|
||||
|
||||
|
||||
from shopdb.utils.authz import require_permission
|
||||
from shopdb.utils.uploads import send_upload
|
||||
|
||||
applications_bp = Blueprint('applications', __name__)
|
||||
|
||||
@@ -550,7 +551,7 @@ def upload_application_image(app_id: int):
|
||||
@applications_bp.route('/image/<path:filename>', methods=['GET'])
|
||||
def serve_application_image(filename):
|
||||
"""Serve an uploaded application image (public - lists and tiles read it)."""
|
||||
return send_from_directory(_appimage_dir(), filename)
|
||||
return send_upload(_appimage_dir(), filename)
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>/image', methods=['DELETE'])
|
||||
@@ -635,7 +636,7 @@ def serve_application_package(filename):
|
||||
open URL would publish it to anything that can reach the site. Always sent
|
||||
as an attachment so a browser saves it rather than trying to render it.
|
||||
"""
|
||||
return send_from_directory(_apppackage_dir(), filename, as_attachment=True)
|
||||
return send_upload(_apppackage_dir(), filename, as_attachment=True)
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>/package', methods=['DELETE'])
|
||||
|
||||
@@ -768,14 +768,23 @@ def create_asset_relationship():
|
||||
'An asset cannot be related to itself'
|
||||
)
|
||||
|
||||
# Check for duplicate relationship
|
||||
# Check for duplicate relationship.
|
||||
#
|
||||
# Deleting a relationship is SOFT (isactive = False, see
|
||||
# delete_asset_relationship). The row therefore survives, and this lookup
|
||||
# found it - so deleting a link and adding it back answered 409 "this
|
||||
# relationship already exists" about a link the page no longer shows, with
|
||||
# no way forward from the UI at all. The row cannot simply be inserted
|
||||
# again either: assetrelationships is unique on (source, target, type).
|
||||
#
|
||||
# Reactivating IS the create, for an inactive row.
|
||||
existing = AssetRelationship.query.filter_by(
|
||||
sourceassetid=source_id,
|
||||
targetassetid=target_id,
|
||||
relationshiptypeid=type_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
if existing and existing.isactive:
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
'This relationship already exists',
|
||||
@@ -798,7 +807,11 @@ def create_asset_relationship():
|
||||
targetassetid=source_id,
|
||||
relationshiptypeid=type_id
|
||||
).first()
|
||||
if inverse is not None:
|
||||
# isactive, for the same reason as the duplicate check above: a soft
|
||||
# deleted inverse is a link somebody already removed, and blocking on it
|
||||
# made "remove the existing one first" - the instruction in this very
|
||||
# message - fail to unblock anything.
|
||||
if inverse is not None and inverse.isactive:
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"The reverse of this relationship already exists "
|
||||
@@ -808,14 +821,20 @@ def create_asset_relationship():
|
||||
http_code=409
|
||||
)
|
||||
|
||||
rel = AssetRelationship(
|
||||
sourceassetid=source_id,
|
||||
targetassetid=target_id,
|
||||
relationshiptypeid=type_id,
|
||||
notes=data.get('notes')
|
||||
)
|
||||
|
||||
db.session.add(rel)
|
||||
if existing is not None:
|
||||
# The soft-deleted row from the duplicate check. Reuse it: the unique
|
||||
# index on (source, target, type) means there is no second row to add.
|
||||
rel = existing
|
||||
rel.isactive = True
|
||||
rel.notes = data.get('notes')
|
||||
else:
|
||||
rel = AssetRelationship(
|
||||
sourceassetid=source_id,
|
||||
targetassetid=target_id,
|
||||
relationshiptypeid=type_id,
|
||||
notes=data.get('notes')
|
||||
)
|
||||
db.session.add(rel)
|
||||
apply_import_timestamps(rel, data)
|
||||
db.session.flush()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import glob
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, current_app, send_from_directory
|
||||
from flask import Blueprint, request, current_app
|
||||
from flask_jwt_extended import jwt_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
@@ -19,6 +19,7 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.uploads import send_upload
|
||||
|
||||
models_bp = Blueprint('models', __name__)
|
||||
|
||||
@@ -228,7 +229,7 @@ def upload_model_image(model_id: int):
|
||||
@models_bp.route('/image/<path:filename>', methods=['GET'])
|
||||
def serve_model_image(filename):
|
||||
"""Serve an uploaded model image (public - asset detail pages read it)."""
|
||||
return send_from_directory(_modelimage_dir(), filename)
|
||||
return send_upload(_modelimage_dir(), filename)
|
||||
|
||||
|
||||
@models_bp.route('/<int:model_id>/image', methods=['DELETE'])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, current_app, send_from_directory
|
||||
from flask import Blueprint, request, current_app
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
@@ -11,6 +11,7 @@ from shopdb.core.models import Setting, AuditLog
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
|
||||
from shopdb.utils.authz import require_permission, require_role
|
||||
from shopdb.utils.uploads import send_upload
|
||||
|
||||
settings_bp = Blueprint('settings', __name__)
|
||||
|
||||
@@ -189,7 +190,7 @@ def upload_map_blueprint():
|
||||
@settings_bp.route('/map-blueprint/<path:filename>', methods=['GET'])
|
||||
def serve_map_blueprint(filename):
|
||||
"""Serve an uploaded blueprint image (public - kiosks read it)."""
|
||||
return send_from_directory(_map_dir(), filename)
|
||||
return send_upload(_map_dir(), filename)
|
||||
|
||||
|
||||
@settings_bp.route('/branding-logo', methods=['POST'])
|
||||
@@ -234,7 +235,7 @@ def upload_branding_logo():
|
||||
@settings_bp.route('/branding/<path:filename>', methods=['GET'])
|
||||
def serve_branding_logo(filename):
|
||||
"""Serve an uploaded branding logo (public - kiosks/print pages read it)."""
|
||||
return send_from_directory(_branding_dir(), filename)
|
||||
return send_upload(_branding_dir(), filename)
|
||||
|
||||
|
||||
@settings_bp.route('', methods=['GET'])
|
||||
@@ -520,10 +521,10 @@ def build_default_settings():
|
||||
},
|
||||
{
|
||||
'key': 'pc_access_domain',
|
||||
'value': 'device.geaerospace.net',
|
||||
'value': 'device.geaerospace.net', # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
'valuetype': 'string',
|
||||
'category': 'site',
|
||||
'description': 'Domain appended to a PC hostname to build remote-access links (host.device.geaerospace.net). Blank = use the hostname as-is.'
|
||||
'description': 'Domain appended to a PC hostname to build remote-access links (host.device.geaerospace.net). Blank = use the hostname as-is.' # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
},
|
||||
{
|
||||
'key': 'employeeid_pattern',
|
||||
@@ -541,7 +542,7 @@ def build_default_settings():
|
||||
},
|
||||
{
|
||||
'key': 'printer_hostname_template',
|
||||
'value': 'Printer-{ip}.printer.geaerospace.net',
|
||||
'value': 'Printer-{ip}.printer.geaerospace.net', # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
||||
'valuetype': 'string',
|
||||
'category': 'site',
|
||||
'description': 'Template for a printer hostname built from its IP. {ip} is the dash-separated IP address.'
|
||||
|
||||
Reference in New Issue
Block a user