Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion sdk/tables/azure-data-tables/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Release History

## 12.2.1 (2022-03-08)
## 12.2.1 (2022-03-10)

### Bugs Fixed
* Fixed hard-coded URL scheme in batch requests (#21953)
* Improved documentation for query formatting in `query_entities` APIs (#23235)
* Removed unsecure debug logging
* Remove client validation of table names (#23106)

### Other Changes
* Python 2.7 is no longer supported. Please use Python version 3.6 or later.
Expand Down
19 changes: 14 additions & 5 deletions sdk/tables/azure-data-tables/azure/data/tables/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@
STORAGE_OAUTH_SCOPE,
SERVICE_HOST_BASE,
)
from ._error import RequestTooLargeError, TableTransactionError, _decode_error
from ._error import (
RequestTooLargeError,
TableTransactionError,
_decode_error,
_validate_tablename_error
)
from ._models import LocationMode
from ._authentication import SharedKeyCredentialPolicy
from ._policies import (
Expand Down Expand Up @@ -256,8 +261,8 @@ def _configure_credential(self, credential):
elif credential is not None:
raise TypeError("Unsupported credential: {}".format(credential))

def _batch_send(self, *reqs, **kwargs):
# type: (List[HttpRequest], Any) -> List[Mapping[str, Any]]
def _batch_send(self, table_name, *reqs, **kwargs):
# type: (str, List[HttpRequest], Any) -> List[Mapping[str, Any]]
"""Given a series of request, do a Storage batch call."""
# Pop it here, so requests doesn't feel bad about additional kwarg
policies = [StorageHeadersPolicy()]
Expand Down Expand Up @@ -290,7 +295,9 @@ def _batch_send(self, *reqs, **kwargs):
error_message="The transaction request was too large",
error_type=RequestTooLargeError)
if response.status_code != 202:
raise _decode_error(response)
decoded = _decode_error(response)
_validate_tablename_error(decoded, table_name)
raise decoded

parts = list(response.parts())
error_parts = [p for p in parts if not 200 <= p.status_code < 300]
Expand All @@ -300,10 +307,12 @@ def _batch_send(self, *reqs, **kwargs):
response,
error_message="The transaction request was too large",
error_type=RequestTooLargeError)
raise _decode_error(
decoded = _decode_error(
response=error_parts[0],
error_type=TableTransactionError
)
_validate_tablename_error(decoded, table_name)
raise decoded
return [extract_batch_part_metadata(p) for p in parts]

def close(self):
Expand Down
75 changes: 62 additions & 13 deletions sdk/tables/azure-data-tables/azure/data/tables/_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# license information.
# --------------------------------------------------------------------------
import sys
from re import match
import re
from enum import Enum

from azure.core.exceptions import (
Expand All @@ -28,7 +28,6 @@ def _str(value):
_str = str



def _to_str(value):
return _str(value) if value is not None else None

Expand All @@ -39,6 +38,19 @@ def _to_str(value):
_ERROR_VALUE_NONE = "{0} should not be None."
_ERROR_UNKNOWN_KEY_WRAP_ALGORITHM = "Unknown key wrap algorithm."

# Storage table validation regex breakdown:
# ^ Match start of string.
# [a-zA-Z]{1} Match an letter for exactly 1 character.
# [a-zA-Z0-9]{2,62} Match any alphanumeric character for between 2 and 62 characters.
# $ End of string
_STORAGE_VALID_TABLE = re.compile(r"^[a-zA-Z]{1}[a-zA-Z0-9]{2,62}$")

# Cosmos table validation regex breakdown:
# ^ Match start of string.
# [^/\#?]{0,254} Match any character that is not /\#? for between 0-253 characters.
# [^ /\#?]{1} Match any character that is not /\#? or a space for exactly 1 character.
# $ End of string
_COSMOS_VALID_TABLE = re.compile(r"^[^/\\#?]{0,253}[^ /\\#?]{1}$")

def _validate_not_none(param_name, param):
if param is None:
Expand All @@ -60,13 +72,56 @@ def _wrap_exception(ex, desired_type):
return desired_type("{}: {}".format(ex.__class__.__name__, msg))


def _validate_table_name(table_name):
if match("^[a-zA-Z]{1}[a-zA-Z0-9]{2,62}$", table_name) is None:
def _validate_storage_tablename(table_name):
if _STORAGE_VALID_TABLE.match(table_name) is None:
raise ValueError(
"Table names must be alphanumeric, cannot begin with a number, and must be between 3-63 characters long."
)


def _validate_cosmos_tablename(table_name):
if _COSMOS_VALID_TABLE.match(table_name) is None:
raise ValueError(
"Table names names must contain from 1-255 characters, and they cannot contain /, \\, #, ?, or a trailing space." # pylint: disable=line-too-long
)


def _validate_tablename_error(decoded_error, table_name):
if (decoded_error.error_code == 'InvalidResourceName' and
'The specifed resource name contains invalid characters' in decoded_error.message):
# This error is raised by Storage for any table/entity operations where the table name contains
# forbidden characters.
_validate_storage_tablename(table_name)
elif (decoded_error.error_code == 'OutOfRangeInput' and
'The specified resource name length is not within the permissible limits' in decoded_error.message):
# This error is raised by Storage for any table/entity operations where the table name is < 3 or > 63
# characters long
_validate_storage_tablename(table_name)
elif (decoded_error.error_code == 'InternalServerError' and
('The resource name presented contains invalid character' in decoded_error.message or
'The resource name can\'t end with space'in decoded_error.message)):
# This error is raised by Cosmos during create_table if the table name contains forbidden
# characters or ends in a space.
_validate_cosmos_tablename(table_name)
elif (decoded_error.error_code == 'BadRequest' and
'The input name is invalid.' in decoded_error.message):
# This error is raised by Cosmos specifically during create_table if the table name is 255 or more
# characters. Entity operations on a too-long-table name simply result in a ResourceNotFoundError.
_validate_cosmos_tablename(table_name)
elif (decoded_error.error_code == 'InvalidInput' and
('Request url is invalid.' in decoded_error.message or
'One of the input values is invalid.' in decoded_error.message)):
# This error is raised by Cosmos for any entity operations or delete_table if the table name contains
# forbidden characters (except in the case of trailing space and backslash).
_validate_cosmos_tablename(table_name)
elif (decoded_error.error_code == 'Unauthorized' and
('The input authorization token can\'t serve the request.' in decoded_error.message or
'The MAC signature found in the HTTP request' in decoded_error.message)):
# This error is raised by Cosmos specifically on entity operations where the table name contains
# some forbidden characters, and seems to be a bug in the service authentication.
_validate_cosmos_tablename(table_name)


def _decode_error(response, error_message=None, error_type=None, **kwargs): # pylint: disable=too-many-branches
error_code = response.headers.get("x-ms-error-code")
additional_data = {}
Expand All @@ -80,14 +135,6 @@ def _decode_error(response, error_message=None, error_type=None, **kwargs): # p
error_message = error_body["odata.error"][info]["value"]
else:
additional_data[info.tag] = info.text

# Special case: there was a playback error during test execution (test proxy only)
message = error_body.get("Message")
if message and message.startswith("Unable to find a record for the request"):
error = ResourceNotFoundError(message=error_message, response=response)
error.error_code = 404
error.additional_info = additional_data
return error
else:
if error_body:
for info in error_body.iter():
Expand Down Expand Up @@ -156,8 +203,10 @@ def _reraise_error(decoded_error):
raise decoded_error


def _process_table_error(storage_error):
def _process_table_error(storage_error, table_name=None):
decoded_error = _decode_error(storage_error.response, storage_error.message)
if table_name:
_validate_tablename_error(decoded_error, table_name)
_reraise_error(decoded_error)


Expand Down
26 changes: 13 additions & 13 deletions sdk/tables/azure-data-tables/azure/data/tables/_table_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
from ._entity import TableEntity
from ._error import (
_process_table_error,
_validate_table_name,
_reraise_error,
_decode_error
_decode_error,
_validate_tablename_error
)
from ._generated.models import (
SignedIdentifier,
Expand Down Expand Up @@ -77,7 +77,6 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential
"""
if not table_name:
raise ValueError("Please specify a table name.")
_validate_table_name(table_name)
self.table_name = table_name
super(TableClient, self).__init__(endpoint, **kwargs)

Expand Down Expand Up @@ -182,7 +181,7 @@ def get_table_access_policy(
**kwargs
)
except HttpResponseError as error:
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)
output = {} # type: Dict[str, Optional[TableAccessPolicy]]
for identifier in cast(List[SignedIdentifier], identifiers):
if identifier.access_policy:
Expand Down Expand Up @@ -227,7 +226,7 @@ def set_table_access_policy(
)
except HttpResponseError as error:
try:
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)
except HttpResponseError as table_error:
if (table_error.error_code == 'InvalidXmlDocument' # type: ignore
and len(signed_identifiers) > 5):
Expand Down Expand Up @@ -261,7 +260,7 @@ def create_table(
try:
result = self._client.table.create(table_properties, **kwargs)
except HttpResponseError as error:
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)
return TableItem(name=result.table_name) # type: ignore

@distributed_trace
Expand Down Expand Up @@ -290,7 +289,7 @@ def delete_table(
except HttpResponseError as error:
if error.status_code == 404:
return
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)

@overload
def delete_entity(self, partition_key, row_key, **kwargs):
Expand Down Expand Up @@ -367,7 +366,7 @@ def delete_entity(self, *args, **kwargs):
except HttpResponseError as error:
if error.status_code == 404:
return
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)

@distributed_trace
def create_entity(
Expand Down Expand Up @@ -408,7 +407,8 @@ def create_entity(
raise ValueError("PartitionKey must be present in an entity")
if entity.get("RowKey") is None:
raise ValueError("RowKey must be present in an entity")
_reraise_error(error)
_validate_tablename_error(decoded, self.table_name)
_reraise_error(decoded)
return _trim_service_metadata(metadata, content=content) # type: ignore

@distributed_trace
Expand Down Expand Up @@ -483,7 +483,7 @@ def update_entity(
else:
raise ValueError("Mode type '{}' is not supported.".format(mode))
except HttpResponseError as error:
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)
return _trim_service_metadata(metadata, content=content) # type: ignore

@distributed_trace
Expand Down Expand Up @@ -612,7 +612,7 @@ def get_entity(
**kwargs
)
except HttpResponseError as error:
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)
return _convert_to_entity(entity)

@distributed_trace
Expand Down Expand Up @@ -674,7 +674,7 @@ def upsert_entity(
)
)
except HttpResponseError as error:
_process_table_error(error)
_process_table_error(error, table_name=self.table_name)
return _trim_service_metadata(metadata, content=content) # type: ignore

def submit_transaction(
Expand Down Expand Up @@ -725,4 +725,4 @@ def submit_transaction(
"The value of 'operations' must be an iterator "
"of Tuples. Please check documentation for correct Tuple format."
)
return self._batch_send(*batched_requests.requests, **kwargs) # type: ignore
return self._batch_send(self.table_name, *batched_requests.requests, **kwargs) # type: ignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
from .._base_client import AccountHostsMixin, get_api_version, extract_batch_part_metadata
from .._authentication import SharedKeyCredentialPolicy
from .._constants import STORAGE_OAUTH_SCOPE
from .._error import RequestTooLargeError, TableTransactionError, _decode_error
from .._error import (
RequestTooLargeError,
TableTransactionError,
_decode_error,
_validate_tablename_error
)
from .._policies import StorageHosts, StorageHeadersPolicy
from .._sdk_moniker import SDK_MONIKER
from ._policies_async import AsyncTablesRetryPolicy
Expand Down Expand Up @@ -102,7 +107,7 @@ def _configure_policies(self, **kwargs):
HttpLoggingPolicy(**kwargs),
]

async def _batch_send(self, *reqs: "HttpRequest", **kwargs) -> List[Mapping[str, Any]]:
async def _batch_send(self, table_name: str, *reqs: "HttpRequest", **kwargs) -> List[Mapping[str, Any]]:
"""Given a series of request, do a Storage batch call."""
# Pop it here, so requests doesn't feel bad about additional kwarg
policies = [StorageHeadersPolicy()]
Expand Down Expand Up @@ -137,7 +142,9 @@ async def _batch_send(self, *reqs: "HttpRequest", **kwargs) -> List[Mapping[str,
error_message="The transaction request was too large",
error_type=RequestTooLargeError)
if response.status_code != 202:
raise _decode_error(response)
decoded = _decode_error(response)
_validate_tablename_error(decoded, table_name)
raise decoded

parts_iter = response.parts()
parts = []
Expand All @@ -150,10 +157,12 @@ async def _batch_send(self, *reqs: "HttpRequest", **kwargs) -> List[Mapping[str,
response,
error_message="The transaction request was too large",
error_type=RequestTooLargeError)
raise _decode_error(
decoded = _decode_error(
response=error_parts[0],
error_type=TableTransactionError,
)
_validate_tablename_error(decoded, table_name)
raise decoded
return [extract_batch_part_metadata(p) for p in parts]


Expand Down
Loading