mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Python: Percent-encode OpenAPI path params & pin azure-search-documents (#13967)
Apply `urllib.parse.quote(safe='')` to path parameter values before substitution in `build_path()`, consistent with the .NET implementation which uses `HttpUtility.UrlEncode`. ### Changes - `rest_api_operation.py`: encode path parameter values per RFC 3986 - `test_sk_openapi.py`: add tests for encoded path parameters - `pyproject.toml` / `uv.lock`: pin `azure-search-documents >= 11.6.0b4, < 12.0.0` ### Pin azure-search-documents < 12.0.0 Version 12.0.0 removed the internal `_endpoint` attribute from `SearchIndexClient`, which breaks `AzureAISearchCollection` initialization at `azure_ai_search.py:158`. Pinning to `< 12.0.0` until the code is updated to use the new public API. ### Tests All existing and new `build_path` tests pass. --------- Co-authored-by: Copilot <[email protected]>
This commit is contained in:
co-authored by
Copilot
parent
590003243a
commit
3f0e2d58fa
@@ -73,7 +73,7 @@ aws = [
|
||||
azure = [
|
||||
"azure-ai-inference >= 1.0.0b6",
|
||||
"azure-core-tracing-opentelemetry >= 1.0.0b11",
|
||||
"azure-search-documents >= 11.6.0b4",
|
||||
"azure-search-documents >= 11.6.0b4, < 12.0.0",
|
||||
"azure-cosmos ~= 4.7"
|
||||
]
|
||||
chroma = [
|
||||
@@ -118,7 +118,9 @@ ollama = [
|
||||
onnx = [
|
||||
# onnxruntime>=1.24.0 dropped Python 3.10 support; pin to last compatible version for 3.10.
|
||||
"onnxruntime==1.22.1; python_version == '3.10'",
|
||||
"onnxruntime>=1.24.3; python_version > '3.10'",
|
||||
# onnxruntime 1.26.0 has no macOS ARM64 wheel; pin to last compatible version.
|
||||
# https://github.com/microsoft/onnxruntime/issues/28441
|
||||
"onnxruntime>=1.24.3, <1.26.0; python_version > '3.10'",
|
||||
"onnxruntime-genai==0.9.0"
|
||||
]
|
||||
oracledb = [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import re
|
||||
from typing import Any, Final
|
||||
from urllib.parse import ParseResult, ParseResultBytes, urlencode, urljoin, urlparse, urlunparse
|
||||
from urllib.parse import ParseResult, ParseResultBytes, quote, urlencode, urljoin, urlparse, urlunparse
|
||||
|
||||
from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import (
|
||||
RestApiExpectedResponse,
|
||||
@@ -288,7 +288,7 @@ class RestApiOperation:
|
||||
f"required parameter of the operation - `{self.id}`."
|
||||
)
|
||||
continue
|
||||
path_template = path_template.replace(f"{{{parameter.name}}}", str(argument))
|
||||
path_template = path_template.replace(f"{{{parameter.name}}}", quote(str(argument), safe=""))
|
||||
return path_template
|
||||
|
||||
def build_query_string(self, arguments: dict[str, Any]) -> str:
|
||||
|
||||
@@ -412,6 +412,56 @@ def test_build_path_with_optional_and_required_parameters():
|
||||
assert operation.build_path(operation.path, arguments) == expected_path
|
||||
|
||||
|
||||
def test_build_path_encodes_special_characters():
|
||||
parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)]
|
||||
operation = RestApiOperation(
|
||||
id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters
|
||||
)
|
||||
# Characters like /, ?, #, and spaces must be percent-encoded to prevent traversal
|
||||
arguments = {"id": "foo/bar?q=1#frag data"}
|
||||
result = operation.build_path(operation.path, arguments)
|
||||
encoded_part = result.split("/resource/")[1]
|
||||
assert "/" not in encoded_part
|
||||
assert "?" not in encoded_part
|
||||
assert "#" not in encoded_part
|
||||
assert " " not in encoded_part
|
||||
# Python's quote(safe="") encodes all except unreserved chars (letters, digits, _, ., -, ~)
|
||||
assert result == "/resource/foo%2Fbar%3Fq%3D1%23frag%20data"
|
||||
|
||||
|
||||
def test_build_path_prevents_path_traversal():
|
||||
parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)]
|
||||
operation = RestApiOperation(
|
||||
id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters
|
||||
)
|
||||
arguments = {"id": "../../admin"}
|
||||
result = operation.build_path(operation.path, arguments)
|
||||
# The slashes must be encoded so ../../admin becomes a single path segment, not a traversal
|
||||
assert result == "/resource/..%2F..%2Fadmin"
|
||||
|
||||
|
||||
def test_build_path_double_encodes_pre_encoded_values():
|
||||
"""Arguments must be raw/unencoded values. Pre-encoded values are double-encoded by design."""
|
||||
parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)]
|
||||
operation = RestApiOperation(
|
||||
id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters
|
||||
)
|
||||
arguments = {"id": "hello%2Fworld"}
|
||||
result = operation.build_path(operation.path, arguments)
|
||||
# %2F in input becomes %252F — the % is encoded, preventing decode-based bypass
|
||||
assert result == "/resource/hello%252Fworld"
|
||||
|
||||
|
||||
def test_build_path_encodes_unicode_characters():
|
||||
parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)]
|
||||
operation = RestApiOperation(
|
||||
id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters
|
||||
)
|
||||
arguments = {"id": "café résumé"}
|
||||
result = operation.build_path(operation.path, arguments)
|
||||
assert result == "/resource/caf%C3%A9%20r%C3%A9sum%C3%A9"
|
||||
|
||||
|
||||
def test_build_query_string_with_required_parameter():
|
||||
parameters = [
|
||||
RestApiParameter(name="query", type="string", location=RestApiParameterLocation.QUERY, is_required=True)
|
||||
|
||||
Generated
+2
-2
@@ -6480,7 +6480,7 @@ requires-dist = [
|
||||
{ name = "azure-core-tracing-opentelemetry", marker = "extra == 'azure'", specifier = ">=1.0.0b11" },
|
||||
{ name = "azure-cosmos", marker = "extra == 'azure'", specifier = "~=4.7" },
|
||||
{ name = "azure-identity", specifier = ">=1.13" },
|
||||
{ name = "azure-search-documents", marker = "extra == 'azure'", specifier = ">=11.6.0b4" },
|
||||
{ name = "azure-search-documents", marker = "extra == 'azure'", specifier = ">=11.6.0b4,<12.0.0" },
|
||||
{ name = "boto3", marker = "extra == 'aws'", specifier = ">=1.36.4,<1.41.0" },
|
||||
{ name = "chromadb", marker = "extra == 'chroma'", specifier = ">=0.5,<1.4" },
|
||||
{ name = "cloudevents", specifier = "~=1.0" },
|
||||
@@ -6502,7 +6502,7 @@ requires-dist = [
|
||||
{ name = "numpy", marker = "python_full_version >= '3.12'", specifier = ">=1.26.0" },
|
||||
{ name = "ollama", marker = "extra == 'ollama'", specifier = "~=0.4" },
|
||||
{ name = "onnxruntime", marker = "python_full_version == '3.10.*' and extra == 'onnx'", specifier = "==1.22.1" },
|
||||
{ name = "onnxruntime", marker = "python_full_version >= '3.11' and extra == 'onnx'", specifier = ">=1.24.3" },
|
||||
{ name = "onnxruntime", marker = "python_full_version >= '3.11' and extra == 'onnx'", specifier = ">=1.24.3,<1.26.0" },
|
||||
{ name = "onnxruntime-genai", marker = "extra == 'onnx'", specifier = "==0.9.0" },
|
||||
{ name = "openai", specifier = ">=2.0.0" },
|
||||
{ name = "openapi-core", specifier = ">=0.18,<0.20" },
|
||||
|
||||
Reference in New Issue
Block a user