Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bulk key regeneration to az iot hub device-identity renew-key and az iot hub module-identity renew-key #710

Merged
merged 34 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7ee11ca
new sdk
vilit1 Jun 13, 2024
d9ed85c
computer be slow
vilit1 Jun 18, 2024
456dcfa
make swap only support one device for now
vilit1 Jul 1, 2024
8964d7b
change error txt
vilit1 Jul 10, 2024
9623164
fix up sdk
vilit1 Jul 11, 2024
205ca28
work in progress; sdk update to make things work
vilit1 Jul 11, 2024
13744cd
fixing sdk and stuff
vilit1 Jul 17, 2024
863a23e
more general replace sdk fix
vilit1 Jul 17, 2024
caf27fb
final handcrafted sdk changes
vilit1 Jul 17, 2024
0300fd4
UNIT TEST
vilit1 Jul 17, 2024
4e57182
helapriogdgf
vilit1 Jul 17, 2024
a052b00
Merge branch 'dev' into hub_secret_update
vilit1 Jul 17, 2024
c36ac80
whoops
vilit1 Jul 17, 2024
81a6f12
Merge branch 'hub_secret_update' of https://github.com/vilit1/azure-i…
vilit1 Jul 17, 2024
2948c2d
FREEZE
vilit1 Jul 17, 2024
e4e146f
lazy fix
vilit1 Jul 17, 2024
abc5285
pylint
vilit1 Jul 17, 2024
33a8dd3
tests
vilit1 Aug 5, 2024
776612c
maybe
vilit1 Aug 5, 2024
d12deb9
maybe
vilit1 Aug 5, 2024
a9fd835
maybe mac
vilit1 Aug 5, 2024
299db6d
maybe mac
vilit1 Aug 5, 2024
6ecd964
maybe mac
vilit1 Aug 5, 2024
8662c7e
fixaroos
vilit1 Aug 7, 2024
b9e91fe
Set explicit agent images for tox workflow
c-ryan-k Aug 7, 2024
ff0b2b5
pr comemtns
vilit1 Aug 8, 2024
abc1d9c
Merge branch 'hub_secret_update' of https://github.com/vilit1/azure-i…
vilit1 Aug 8, 2024
a67057d
helapdjgdf
vilit1 Aug 8, 2024
5ad8203
testing for ryan and his chickens
vilit1 Aug 8, 2024
bac38b8
update macos agent version for ADO merge pipeline
c-ryan-k Aug 8, 2024
87092a7
Merge branch 'hub_secret_update' of https://github.com/vilit1/azure-i…
c-ryan-k Aug 8, 2024
389685c
pr comments
vilit1 Aug 8, 2024
316fe59
Merge branch 'hub_secret_update' of https://github.com/vilit1/azure-i…
vilit1 Aug 8, 2024
5a8d3f8
forgot to save constants
vilit1 Aug 8, 2024
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
1 change: 1 addition & 0 deletions .azure-devops/templates/run-tests-parallel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ steps:

- ${{ if eq(parameters.runUnitTests, 'true') }}:
- script: |
pip freeze
vilit1 marked this conversation as resolved.
Show resolved Hide resolved
pytest -vv ${{ parameters.path }} -k "_unit.py" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-unit-${{ parameters.name }}.xml
displayName: '${{ parameters.name }} unit tests'
env:
Expand Down
10 changes: 10 additions & 0 deletions azext_iot/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,16 @@
] = """
type: command
short-summary: Renew target keys of an IoT Hub device with sas authentication.
vilit1 marked this conversation as resolved.
Show resolved Hide resolved
long-summary: Currently etags and key type `swap` are not supported for bulk key regeneration.
examples:
- name: Renew the primary key.
text: az iot hub device-identity renew-key -d {device_id} -n {iothub_name} --kt primary
- name: Swap the primary and secondary keys.
text: az iot hub device-identity renew-key -d {device_id} -n {iothub_name} --kt swap
- name: Renew the secondary key for two devices and their modules.
text: az iot hub device-identity renew-key -d {device_id} {device_id} -n {iothub_name} --kt secondary --include-modules
- name: Renew the both keys for all devices within the hub.
text: az iot hub device-identity renew-key -d * -n {iothub_name} --kt both
"""

helps[
Expand Down Expand Up @@ -558,11 +563,16 @@
] = """
type: command
short-summary: Renew target keys of an IoT Hub device module with sas authentication.
vilit1 marked this conversation as resolved.
Show resolved Hide resolved
long-summary: Currently etags and key type `swap` are not supported for bulk key regeneration.
examples:
- name: Renew the primary key.
text: az iot hub module-identity renew-key -m {module_name} -d {device_id} -n {iothub_name} --kt primary
- name: Swap the primary and secondary keys.
text: az iot hub module-identity renew-key -m {module_name} -d {device_id} -n {iothub_name} --kt swap
- name: Renew the secondary key for two modules.
text: az iot hub module-identity renew-key -m {module_name} {module_name} -d {device_id} -n {iothub_name} --kt secondary
- name: Renew both keys for all modules in the device.
text: az iot hub module-identity renew-key -m * -d {device_id} -n {iothub_name} --kt both
"""

helps[
Expand Down
32 changes: 32 additions & 0 deletions azext_iot/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,25 @@ def load_arguments(self, _):
arg_type=get_enum_type(RenewKeyType),
help="Target key type to regenerate.",
)
context.argument(
"device_ids",
options_list=["--device-id", "-d"],
help="Space seperated list of target Device Ids. Use `*` for all devices.",
nargs="+",
action="extend"
)
context.argument(
"include_modules",
options_list=["--include-modules", "--im"],
help="Flag to include device modules during key regeneration.",
arg_type=get_three_state_flag()
)
context.argument(
"etag",
options_list=["--etag", "-e"],
help="Etag or entity tag corresponding to the last state of the resource. "
"If no etag is provided the value '*' is used. This arguement only applies to `swap`.",
)

with self.argument_context("iot hub device-identity export") as context:
context.argument(
Expand Down Expand Up @@ -614,6 +633,19 @@ def load_arguments(self, _):
arg_type=get_enum_type(RenewKeyType),
help="Target key type to regenerate.",
)
context.argument(
"module_ids",
options_list=["--module-id", "-m"],
help="Space seperated list of target Module Ids. Use `*` for all modules.",
nargs="+",
action="extend"
)
context.argument(
"etag",
options_list=["--etag", "-e"],
help="Etag or entity tag corresponding to the last state of the resource. "
"If no etag is provided the value '*' is used. This arguement only applies to `swap`.",
)

with self.argument_context("iot hub distributed-tracing update") as context:
context.argument(
Expand Down
1 change: 1 addition & 0 deletions azext_iot/common/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ class RenewKeyType(Enum):
primary = KeyType.primary.value
secondary = KeyType.secondary.value
swap = "swap"
both = "both"


class IoTHubStateType(Enum):
Expand Down
190 changes: 145 additions & 45 deletions azext_iot/operations/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from os.path import exists
from knack.log import get_logger
from enum import Enum, EnumMeta
from tqdm import tqdm
from azure.cli.core.azclierror import (
ArgumentUsageError,
CLIInternalError,
Expand Down Expand Up @@ -43,7 +44,6 @@
read_file_content,
init_monitoring,
process_json_arg,
generate_key,
generate_storage_account_sas_token,
)
from azext_iot._factory import SdkResolver, CloudError
Expand Down Expand Up @@ -498,8 +498,9 @@ def _update_device_key(target, device, auth_method, pk, sk, etag=None):
def iot_device_key_regenerate(
cmd,
hub_name_or_hostname,
device_id,
device_ids,
renew_key_type,
include_modules=False,
resource_group_name=None,
login=None,
etag=None,
Expand All @@ -512,24 +513,55 @@ def iot_device_key_regenerate(
login=login,
auth_type=auth_type_dataplane,
)
device = _iot_device_show(target, device_id)
if device["authentication"]["type"] != DeviceAuthApiType.sas.value:
raise ClientRequestError("Device authentication should be of type sas")

pk = device["authentication"]["symmetricKey"]["primaryKey"]
sk = device["authentication"]["symmetricKey"]["secondaryKey"]

if renew_key_type == RenewKeyType.primary.value:
pk = generate_key()
if renew_key_type == RenewKeyType.secondary.value:
sk = generate_key()
if renew_key_type == RenewKeyType.swap.value:
if len(device_ids) > 1 or device_ids[0] == "*":
raise InvalidArgumentValueError(
"Currently, bulk key swap is not supported."
)
device = _iot_device_show(target, device_ids[0])
if device["authentication"]["type"] != DeviceAuthApiType.sas.value:
raise ClientRequestError("Device authentication should be of type sas")

pk = device["authentication"]["symmetricKey"]["primaryKey"]
sk = device["authentication"]["symmetricKey"]["secondaryKey"]

temp = pk
pk = sk
sk = temp
return _update_device_key(
target, device, device["authentication"]["type"], pk, sk, etag
)

return _update_device_key(
target, device, device["authentication"]["type"], pk, sk, etag
resolver = SdkResolver(target=target)
service_sdk = resolver.get_sdk(SdkType.service_sdk)
if renew_key_type in [RenewKeyType.primary.value, RenewKeyType.secondary.value]:
renew_key_type += "Key"

modules = []
if device_ids[0] == "*":
devices = _iot_device_twin_list(target=target, top=None)
devices.extend(_iot_device_twin_list(target=target, edge_enabled=True, top=None))
device_ids = []
for device in devices:
if device["authenticationType"] == DeviceAuthApiType.sas.value:
device_ids.append(device["deviceId"])
# non sas devices can have sas modules...
if include_modules:
modules.extend(
_iot_key_regenerate_process_modules(
target=target, device_id=device["deviceId"], module_ids="*"
)
)
if modules:
logger.info(f"Found {len(modules)} modules.")

# call friendly format
devices = [{"id": device_ids[i]} for i in range(len(device_ids))]
vilit1 marked this conversation as resolved.
Show resolved Hide resolved
return _iot_key_regenerate_batch(
service_sdk=service_sdk,
renew_key_type=renew_key_type,
items=devices + modules,
)


Expand Down Expand Up @@ -945,7 +977,7 @@ def iot_device_module_key_regenerate(
cmd,
hub_name_or_hostname,
device_id,
module_id,
module_ids,
renew_key_type,
resource_group_name=None,
login=None,
Expand All @@ -961,42 +993,110 @@ def iot_device_module_key_regenerate(
)
resolver = SdkResolver(target=target)
service_sdk = resolver.get_sdk(SdkType.service_sdk)
try:
module = service_sdk.modules.get_identity(
id=device_id, mid=module_id, raw=True
).response.json()
except CloudError as e:
handle_service_exception(e)

if module["authentication"]["type"] != "sas":
raise ClientRequestError("Module authentication should be of type sas")
if renew_key_type == RenewKeyType.swap.value:
if len(module_ids) > 1 or module_ids[0] == "*":
raise InvalidArgumentValueError(
"Currently, bulk key swap is not supported."
)
try:
module = service_sdk.modules.get_identity(
id=device_id, mid=module_ids[0], raw=True
).response.json()
except CloudError as e:
handle_service_exception(e)
if module["authentication"]["type"] != DeviceAuthApiType.sas.value:
raise ClientRequestError("Module authentication should be of type sas")

pk = module["authentication"]["symmetricKey"]["primaryKey"]
sk = module["authentication"]["symmetricKey"]["secondaryKey"]
pk = module["authentication"]["symmetricKey"]["primaryKey"]
sk = module["authentication"]["symmetricKey"]["secondaryKey"]

if renew_key_type == RenewKeyType.primary.value:
pk = generate_key()
if renew_key_type == RenewKeyType.secondary.value:
sk = generate_key()
if renew_key_type == RenewKeyType.swap.value:
temp = pk
pk = sk
sk = temp
module["authentication"]["symmetricKey"]["primaryKey"] = sk
module["authentication"]["symmetricKey"]["secondaryKey"] = temp
try:
return service_sdk.modules.create_or_update_identity(
id=device_id,
mid=module_ids[0],
module=module,
custom_headers={
"If-Match": '"{}"'.format(etag if etag else "*")
},
)
except CloudError as e:
handle_service_exception(e)

module["authentication"]["symmetricKey"]["primaryKey"] = pk
module["authentication"]["symmetricKey"]["secondaryKey"] = sk
if renew_key_type in [RenewKeyType.primary.value, RenewKeyType.secondary.value]:
renew_key_type += "Key"
vilit1 marked this conversation as resolved.
Show resolved Hide resolved

try:
headers = {}
headers["If-Match"] = '"{}"'.format(etag if etag else "*")
return service_sdk.modules.create_or_update_identity(
id=device_id,
mid=module_id,
module=module,
custom_headers=headers,
)
except CloudError as e:
handle_service_exception(e)
modules = _iot_key_regenerate_process_modules(
target, device_id, module_ids
)
return _iot_key_regenerate_batch(
service_sdk=service_sdk,
renew_key_type=renew_key_type,
items=modules,
device_id=device_id
)


def _iot_key_regenerate_process_modules(
target,
device_id,
module_ids,
):
if module_ids[0] == "*":
modules = _iot_device_module_list(target=target, device_id=device_id, top=None)
module_ids = []
for module in modules:
# not going to question why the call the Module object - just making things
# easier for unit tests
if not isinstance(module, dict):
module = module.serialize()
if module["authentication"]["type"] == DeviceAuthApiType.sas.value:
module_ids.append(module["moduleId"])

return [{"id": device_id, "moduleId": module_ids[i]} for i in range(len(module_ids))]
vilit1 marked this conversation as resolved.
Show resolved Hide resolved


def _iot_key_regenerate_batch(
service_sdk,
renew_key_type,
items,
device_id=None,
):
overall_result = {
"policyKey": renew_key_type,
"errors": [],
"rotatedKeys": []
}
starting_msg = f"modules for device {device_id}" if device_id else "devices"
logger.info(f"Found {len(items)} {starting_msg}.")
if not items:
return {}
batches = []
while items:
if len(items) > 100:
batches.append(items[:100])
items = items[100:]
else:
batches.append(items[:])
items = []
for i in tqdm(range(len(batches)), desc="Bulk key regeneration is in progress", ascii=' #'):
vilit1 marked this conversation as resolved.
Show resolved Hide resolved
# call
try:
result = service_sdk.service.bulk_regenerate_device_key_method(
policy_key=renew_key_type,
devices=batches[i]
)
except CloudError as e:
handle_service_exception(e)
# combine result
if result.errors:
overall_result["errors"].extend(result.errors)
if result.rotated_keys:
overall_result["rotatedKeys"].extend(result.rotated_keys)
return overall_result


def iot_device_module_list(
Expand Down
10 changes: 7 additions & 3 deletions azext_iot/sdk/iothub/service/iot_hub_gateway_service_ap_is.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .operations.query_operations import QueryOperations
from .operations.jobs_operations import JobsOperations
from .operations.cloud_to_device_messages_operations import CloudToDeviceMessagesOperations
from .operations.service_operations import ServiceOperations
from .operations.modules_operations import ModulesOperations
from .operations.digital_twin_operations import DigitalTwinOperations
from . import models
Expand Down Expand Up @@ -47,6 +48,7 @@ def __init__(
super(IotHubGatewayServiceAPIsConfiguration, self).__init__(base_url)

self.add_user_agent('iothubgatewayserviceapis/{}'.format(VERSION))
self.add_user_agent('Azure-SDK-For-Python')

self.credentials = credentials

Expand All @@ -58,7 +60,6 @@ class IotHubGatewayServiceAPIs(SDKClient):
:vartype config: IotHubGatewayServiceAPIsConfiguration

:ivar configuration: Configuration operations
:vartype configuration: service.operations.ConfigurationOperations
:vartype configuration: azure.operations.ConfigurationOperations
:ivar statistics: Statistics operations
:vartype statistics: azure.operations.StatisticsOperations
Expand All @@ -72,11 +73,12 @@ class IotHubGatewayServiceAPIs(SDKClient):
:vartype jobs: azure.operations.JobsOperations
:ivar cloud_to_device_messages: CloudToDeviceMessages operations
:vartype cloud_to_device_messages: azure.operations.CloudToDeviceMessagesOperations
:ivar service: Service operations
:vartype service: azure.operations.ServiceOperations
:ivar modules: Modules operations
:vartype modules: azure.operations.ModulesOperations
:ivar digital_twin: DigitalTwin operations
:vartype digital_twin: azure.operations.DigitalTwinOperations
:vartype digital_twin: service.operations.DigitalTwinOperations

:param credentials: Credentials needed for the client to connect to Azure.
:type credentials: :mod:`A msrestazure Credentials
Expand All @@ -91,7 +93,7 @@ def __init__(
super(IotHubGatewayServiceAPIs, self).__init__(self.config.credentials, self.config)

client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)}
self.api_version = '2021-04-12'
self.api_version = '2024-03-31'
self._serialize = Serializer(client_models)
self._deserialize = Deserializer(client_models)

Expand All @@ -109,6 +111,8 @@ def __init__(
self._client, self.config, self._serialize, self._deserialize)
self.cloud_to_device_messages = CloudToDeviceMessagesOperations(
self._client, self.config, self._serialize, self._deserialize)
self.service = ServiceOperations(
self._client, self.config, self._serialize, self._deserialize)
self.modules = ModulesOperations(
self._client, self.config, self._serialize, self._deserialize)
self.digital_twin = DigitalTwinOperations(
Expand Down
Loading
Loading