Fix routing conflicts: move admin endpoints to reserved paths
This commit is contained in:
@@ -278,7 +278,7 @@ def create_app(
|
||||
return render_template("404.html"), 404
|
||||
return error
|
||||
|
||||
@app.get("/healthz")
|
||||
@app.get("/myfsio/health")
|
||||
def healthcheck() -> Dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -590,6 +590,7 @@ def _generate_presigned_url(
|
||||
bucket_name: str,
|
||||
object_key: str,
|
||||
expires_in: int,
|
||||
api_base_url: str | None = None,
|
||||
) -> str:
|
||||
region = current_app.config["AWS_REGION"]
|
||||
service = current_app.config["AWS_SERVICE"]
|
||||
@@ -610,7 +611,7 @@ def _generate_presigned_url(
|
||||
}
|
||||
canonical_query = _encode_query_params(query_params)
|
||||
|
||||
api_base = current_app.config.get("API_BASE_URL")
|
||||
api_base = api_base_url or current_app.config.get("API_BASE_URL")
|
||||
if api_base:
|
||||
parsed = urlparse(api_base)
|
||||
host = parsed.netloc
|
||||
@@ -940,6 +941,7 @@ def _maybe_handle_bucket_subresource(bucket_name: str) -> Response | None:
|
||||
"notification": _bucket_notification_handler,
|
||||
"logging": _bucket_logging_handler,
|
||||
"uploads": _bucket_uploads_handler,
|
||||
"policy": _bucket_policy_handler,
|
||||
}
|
||||
requested = [key for key in handlers if key in request.args]
|
||||
if not requested:
|
||||
@@ -2642,9 +2644,9 @@ def _list_parts(bucket_name: str, object_key: str) -> Response:
|
||||
return _xml_response(root)
|
||||
|
||||
|
||||
@s3_api_bp.route("/bucket-policy/<bucket_name>", methods=["GET", "PUT", "DELETE"])
|
||||
@limiter.limit("30 per minute")
|
||||
def bucket_policy_handler(bucket_name: str) -> Response:
|
||||
def _bucket_policy_handler(bucket_name: str) -> Response:
|
||||
if request.method not in {"GET", "PUT", "DELETE"}:
|
||||
return _method_not_allowed(["GET", "PUT", "DELETE"])
|
||||
principal, error = _require_principal()
|
||||
if error:
|
||||
return error
|
||||
@@ -2676,51 +2678,6 @@ def bucket_policy_handler(bucket_name: str) -> Response:
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
@s3_api_bp.post("/presign/<bucket_name>/<path:object_key>")
|
||||
@limiter.limit("45 per minute")
|
||||
def presign_object(bucket_name: str, object_key: str):
|
||||
payload = request.get_json(silent=True) or {}
|
||||
method = str(payload.get("method", "GET")).upper()
|
||||
allowed_methods = {"GET", "PUT", "DELETE"}
|
||||
if method not in allowed_methods:
|
||||
return _error_response("InvalidRequest", "Method must be GET, PUT, or DELETE", 400)
|
||||
try:
|
||||
expires = int(payload.get("expires_in", 900))
|
||||
except (TypeError, ValueError):
|
||||
return _error_response("InvalidRequest", "expires_in must be an integer", 400)
|
||||
expires = max(1, min(expires, 7 * 24 * 3600))
|
||||
action = "read" if method == "GET" else ("delete" if method == "DELETE" else "write")
|
||||
principal, error = _require_principal()
|
||||
if error:
|
||||
return error
|
||||
try:
|
||||
_authorize_action(principal, bucket_name, action, object_key=object_key)
|
||||
except IamError as exc:
|
||||
return _error_response("AccessDenied", str(exc), 403)
|
||||
storage = _storage()
|
||||
if not storage.bucket_exists(bucket_name):
|
||||
return _error_response("NoSuchBucket", "Bucket does not exist", 404)
|
||||
if action != "write":
|
||||
try:
|
||||
storage.get_object_path(bucket_name, object_key)
|
||||
except StorageError:
|
||||
return _error_response("NoSuchKey", "Object not found", 404)
|
||||
secret = _iam().secret_for_key(principal.access_key)
|
||||
url = _generate_presigned_url(
|
||||
principal=principal,
|
||||
secret_key=secret,
|
||||
method=method,
|
||||
bucket_name=bucket_name,
|
||||
object_key=object_key,
|
||||
expires_in=expires,
|
||||
)
|
||||
current_app.logger.info(
|
||||
"Presigned URL generated",
|
||||
extra={"bucket": bucket_name, "key": object_key, "method": method},
|
||||
)
|
||||
return jsonify({"url": url, "method": method, "expires_in": expires})
|
||||
|
||||
|
||||
@s3_api_bp.route("/<bucket_name>", methods=["HEAD"])
|
||||
@limiter.limit("100 per minute")
|
||||
def head_bucket(bucket_name: str) -> Response:
|
||||
|
||||
62
app/ui.py
62
app/ui.py
@@ -36,6 +36,7 @@ from .extensions import limiter, csrf
|
||||
from .iam import IamError
|
||||
from .kms import KMSManager
|
||||
from .replication import ReplicationManager, ReplicationRule
|
||||
from .s3_api import _generate_presigned_url
|
||||
from .secret_store import EphemeralSecretStore
|
||||
from .storage import ObjectStorage, StorageError
|
||||
|
||||
@@ -1135,42 +1136,43 @@ def object_presign(bucket_name: str, object_key: str):
|
||||
principal = _current_principal()
|
||||
payload = request.get_json(silent=True) or {}
|
||||
method = str(payload.get("method", "GET")).upper()
|
||||
allowed_methods = {"GET", "PUT", "DELETE"}
|
||||
if method not in allowed_methods:
|
||||
return jsonify({"error": "Method must be GET, PUT, or DELETE"}), 400
|
||||
action = "read" if method == "GET" else ("delete" if method == "DELETE" else "write")
|
||||
try:
|
||||
_authorize_ui(principal, bucket_name, action, object_key=object_key)
|
||||
except IamError as exc:
|
||||
return jsonify({"error": str(exc)}), 403
|
||||
|
||||
try:
|
||||
expires = int(payload.get("expires_in", 900))
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"error": "expires_in must be an integer"}), 400
|
||||
expires = max(1, min(expires, 7 * 24 * 3600))
|
||||
storage = _storage()
|
||||
if not storage.bucket_exists(bucket_name):
|
||||
return jsonify({"error": "Bucket does not exist"}), 404
|
||||
if action != "write":
|
||||
try:
|
||||
storage.get_object_path(bucket_name, object_key)
|
||||
except StorageError:
|
||||
return jsonify({"error": "Object not found"}), 404
|
||||
secret = _iam().secret_for_key(principal.access_key)
|
||||
api_base = current_app.config.get("API_BASE_URL") or "http://127.0.0.1:5000"
|
||||
api_base = api_base.rstrip("/")
|
||||
encoded_key = quote(object_key, safe="/")
|
||||
url = f"{api_base}/presign/{bucket_name}/{encoded_key}"
|
||||
|
||||
parsed_api = urlparse(api_base)
|
||||
headers = _api_headers()
|
||||
headers["X-Forwarded-Host"] = parsed_api.netloc or "127.0.0.1:5000"
|
||||
headers["X-Forwarded-Proto"] = parsed_api.scheme or "http"
|
||||
headers["X-Forwarded-For"] = request.remote_addr or "127.0.0.1"
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=5)
|
||||
except requests.RequestException as exc:
|
||||
return jsonify({"error": f"API unavailable: {exc}"}), 502
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
text = response.text or ""
|
||||
if text.strip().startswith("<"):
|
||||
import xml.etree.ElementTree as ET
|
||||
try:
|
||||
root = ET.fromstring(text)
|
||||
message = root.findtext(".//Message") or root.findtext(".//Code") or "Unknown S3 error"
|
||||
body = {"error": message}
|
||||
except ET.ParseError:
|
||||
body = {"error": text or "API returned an empty response"}
|
||||
else:
|
||||
body = {"error": text or "API returned an empty response"}
|
||||
return jsonify(body), response.status_code
|
||||
url = _generate_presigned_url(
|
||||
principal=principal,
|
||||
secret_key=secret,
|
||||
method=method,
|
||||
bucket_name=bucket_name,
|
||||
object_key=object_key,
|
||||
expires_in=expires,
|
||||
api_base_url=api_base,
|
||||
)
|
||||
current_app.logger.info(
|
||||
"Presigned URL generated",
|
||||
extra={"bucket": bucket_name, "key": object_key, "method": method},
|
||||
)
|
||||
return jsonify({"url": url, "method": method, "expires_in": expires})
|
||||
|
||||
|
||||
@ui_bp.get("/buckets/<bucket_name>/objects/<path:object_key>/metadata")
|
||||
|
||||
Reference in New Issue
Block a user