From acc3e208483d580fa6c5b084c0717a4049929218 Mon Sep 17 00:00:00 2001 From: jtamma <140404599+jtamma@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:40:38 +0530 Subject: [PATCH] new extension for nexus identity feature (#7641) * new extension for nexus identity feature --- src/nexusidentity/HISTORY.rst | 8 ++ src/nexusidentity/README.rst | 5 + .../azext_nexusidentity/__init__.py | 32 ++++++ .../azext_nexusidentity/_client_factory.py | 4 + .../azext_nexusidentity/_help.py | 18 +++ .../azext_nexusidentity/_params.py | 12 ++ .../azext_nexusidentity/_validators.py | 4 + .../azext_nexusidentity/azext_metadata.json | 4 + .../azext_nexusidentity/commands.py | 13 +++ .../azext_nexusidentity/custom.py | 105 ++++++++++++++++++ .../azext_nexusidentity/tests/__init__.py | 5 + .../tests/latest/__init__.py | 5 + .../test_nexusidentity_scenario1.yaml | 78 +++++++++++++ .../latest/test_nexusidentity_scenario.py | 42 +++++++ src/nexusidentity/setup.cfg | 0 src/nexusidentity/setup.py | 59 ++++++++++ src/service_name.json | 5 + 17 files changed, 399 insertions(+) create mode 100644 src/nexusidentity/HISTORY.rst create mode 100644 src/nexusidentity/README.rst create mode 100644 src/nexusidentity/azext_nexusidentity/__init__.py create mode 100644 src/nexusidentity/azext_nexusidentity/_client_factory.py create mode 100644 src/nexusidentity/azext_nexusidentity/_help.py create mode 100644 src/nexusidentity/azext_nexusidentity/_params.py create mode 100644 src/nexusidentity/azext_nexusidentity/_validators.py create mode 100644 src/nexusidentity/azext_nexusidentity/azext_metadata.json create mode 100644 src/nexusidentity/azext_nexusidentity/commands.py create mode 100644 src/nexusidentity/azext_nexusidentity/custom.py create mode 100644 src/nexusidentity/azext_nexusidentity/tests/__init__.py create mode 100644 src/nexusidentity/azext_nexusidentity/tests/latest/__init__.py create mode 100644 src/nexusidentity/azext_nexusidentity/tests/latest/recordings/test_nexusidentity_scenario1.yaml create mode 100644 src/nexusidentity/azext_nexusidentity/tests/latest/test_nexusidentity_scenario.py create mode 100644 src/nexusidentity/setup.cfg create mode 100644 src/nexusidentity/setup.py diff --git a/src/nexusidentity/HISTORY.rst b/src/nexusidentity/HISTORY.rst new file mode 100644 index 00000000000..abbff5a61a7 --- /dev/null +++ b/src/nexusidentity/HISTORY.rst @@ -0,0 +1,8 @@ +.. :changelog: + +Release History +=============== + +1.0.0b1 +++++++ +* Initial release. \ No newline at end of file diff --git a/src/nexusidentity/README.rst b/src/nexusidentity/README.rst new file mode 100644 index 00000000000..9c0fcb69b50 --- /dev/null +++ b/src/nexusidentity/README.rst @@ -0,0 +1,5 @@ +Microsoft Azure CLI 'nexusidentity' Extension +========================================== + +This package is for the 'nexusidentity' extension. +i.e. 'az nexusidentity' \ No newline at end of file diff --git a/src/nexusidentity/azext_nexusidentity/__init__.py b/src/nexusidentity/azext_nexusidentity/__init__.py new file mode 100644 index 00000000000..5a00292e88c --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/__init__.py @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core import AzCommandsLoader +from azext_nexusidentity._help import helps # pylint: disable=unused-import + + +class NexusidentityCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + custom_command_type = CliCommandType( + operations_tmpl='azext_nexusidentity.custom#{}') + super( + NexusidentityCommandsLoader, + self).__init__( + cli_ctx=cli_ctx, + custom_command_type=custom_command_type) + + def load_command_table(self, args): + from azext_nexusidentity.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azext_nexusidentity._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = NexusidentityCommandsLoader diff --git a/src/nexusidentity/azext_nexusidentity/_client_factory.py b/src/nexusidentity/azext_nexusidentity/_client_factory.py new file mode 100644 index 00000000000..34913fb394d --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/_client_factory.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/src/nexusidentity/azext_nexusidentity/_help.py b/src/nexusidentity/azext_nexusidentity/_help.py new file mode 100644 index 00000000000..8bb095394cc --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/_help.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.help_files import helps # pylint: disable=unused-import + + +helps['nexusidentity'] = """ + type: group + short-summary: Command to manage Nexusidentity keys. +""" + +helps['nexusidentity gen-keys'] = """ + type: command + short-summary: Generate Nexusidentity keys. +""" diff --git a/src/nexusidentity/azext_nexusidentity/_params.py b/src/nexusidentity/azext_nexusidentity/_params.py new file mode 100644 index 00000000000..60c7a64d5af --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/_params.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long +# pylint: disable=unused-import + +from knack.arguments import CLIArgumentType + + +def load_arguments(_, __): + pass diff --git a/src/nexusidentity/azext_nexusidentity/_validators.py b/src/nexusidentity/azext_nexusidentity/_validators.py new file mode 100644 index 00000000000..34913fb394d --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/_validators.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/src/nexusidentity/azext_nexusidentity/azext_metadata.json b/src/nexusidentity/azext_nexusidentity/azext_metadata.json new file mode 100644 index 00000000000..2a62beffb25 --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/azext_metadata.json @@ -0,0 +1,4 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.59.0" +} \ No newline at end of file diff --git a/src/nexusidentity/azext_nexusidentity/commands.py b/src/nexusidentity/azext_nexusidentity/commands.py new file mode 100644 index 00000000000..a8643fba723 --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/commands.py @@ -0,0 +1,13 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=line-too-long +# pylint: disable=unused-import +from azure.cli.core.commands import CliCommandType + + +def load_command_table(self, _): + with self.command_group('nexusidentity') as g: + g.custom_command('gen-keys', 'generate_nexus_identity_keys') diff --git a/src/nexusidentity/azext_nexusidentity/custom.py b/src/nexusidentity/azext_nexusidentity/custom.py new file mode 100644 index 00000000000..8bc677d02b6 --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/custom.py @@ -0,0 +1,105 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.util import CLIError # pylint: disable=unused-import +from knack.log import get_logger + +import logging + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = get_logger(__name__) + + +def generate_nexus_identity_keys() -> None: + + import os + import subprocess + import asyncio + import sys + + from azure.identity import AzureCliCredential + from msgraph import GraphServiceClient + from msgraph.generated.models.open_type_extension import OpenTypeExtension + from msgraph.generated.models.extension import Extension + from azure.core.exceptions import ClientAuthenticationError, HttpResponseError + from msgraph.generated.models.o_data_errors.o_data_error import ODataError + + # Generate SSH key + if sys.platform.startswith("win"): + # Generate ed25519-sk key + subprocess.run(['ssh-keygen', + '-t', + 'ed25519-sk', + '-O', + 'resident', + '-O', + 'verify-required', + '-f', + os.path.expanduser("~\\.ssh\\id_ecdsa_sk")], + check=False) + + # currently the cryptography library does not support the ed25519-sk key + # type, so we will read the public key from the file + try: + # Read public key + with open(os.path.expanduser("~/.ssh/id_ecdsa_sk.pub"), "r") as key_file: + public_key = key_file.read() + except FileNotFoundError as e: + raise CLIError(f"Error reading public key: {e}") + except OSError as e: + raise CLIError(f"Unexpected error reading public key: {e}") + + try: + credential = AzureCliCredential() + scopes = ['https://graph.microsoft.com//.default'] + graph_client = GraphServiceClient( + credentials=credential, scopes=scopes) + + except ClientAuthenticationError as e: + logger.error("Authentication failed: %s", e) + raise CLIError(f"Authentication failed: {e}") + except Exception as e: + logger.error("An unexpected error occurred: %s", e) + raise CLIError(f"An unexpected error occurred: {e}") + + async def me(): + extension_id = "com.nexusidentity.keys" + extensions = await graph_client.me.extensions.get() + + extension_exists = any( + extension.id == extension_id for extension in extensions.value) + + try: + # Update or create extension + if extension_exists: + request_body = Extension( + odata_type="microsoft.graph.openTypeExtension", + additional_data={ + "extension_name": extension_id, + "publicKey": public_key + } + ) + await graph_client.me.extensions.by_extension_id(extension_id).patch(request_body) + else: + request_body = OpenTypeExtension( + odata_type="microsoft.graph.openTypeExtension", + extension_name=extension_id, + additional_data={ + "publicKey": public_key + } + ) + await graph_client.me.extensions.post(request_body) + except ODataError as e: + logger.error("Error updating extension: %s", e) + raise CLIError(f"Error updating extension: {e}") + except (HttpResponseError) as e: + logger.error("Failed to update or create extension: %s", e) + raise CLIError(f"Failed to update or create extension: {e}") + + asyncio.run(me()) + else: + logger.warning( + "This command is currently supported only on Windows platforms") diff --git a/src/nexusidentity/azext_nexusidentity/tests/__init__.py b/src/nexusidentity/azext_nexusidentity/tests/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/tests/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/nexusidentity/azext_nexusidentity/tests/latest/__init__.py b/src/nexusidentity/azext_nexusidentity/tests/latest/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/tests/latest/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/nexusidentity/azext_nexusidentity/tests/latest/recordings/test_nexusidentity_scenario1.yaml b/src/nexusidentity/azext_nexusidentity/tests/latest/recordings/test_nexusidentity_scenario1.yaml new file mode 100644 index 00000000000..9e92023a3a8 --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/tests/latest/recordings/test_nexusidentity_scenario1.yaml @@ -0,0 +1,78 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - graph.microsoft.com + user-agent: + - python-httpx/0.27.0 + method: GET + uri: https://graph.microsoft.com/v1.0/users/me-token-to-replace/extensions + response: + body: + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#users(''40523ce8-82a4-4362-b93a-7dfd212edd4c'')/extensions","value":[{"@odata.type":"#microsoft.graph.openTypeExtension","extension_name":"com.nexusidentity.keys","publicKey":"sk-ssh-ed25519@openssh.com + AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIJet7nknqBQpavVf6gjjjlcHUfckuGI4eZDJxG4tlSSHAAAABHNzaDo= + fareast\\jtamma@DESKTOP-QDMRQ39\n","id":"com.nexusidentity.keys"}]}' + headers: + content-length: + - '431' + content-type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + date: + - Mon, 03 Jun 2024 20:35:36 GMT + odata-version: + - '4.0' + request-id: + - 10fe3408-7303-4b72-baa2-428ee6dce930 + strict-transport-security: + - max-age=31536000 + vary: + - Accept-Encoding + x-ms-ags-diagnostic: + - '{"ServerInfo":{"DataCenter":"Central India","Slice":"E","Ring":"3","ScaleUnit":"001","RoleInstance":"PN3PEPF000002CB"}}' + status: + code: 200 + message: OK +- request: + body: '{"@odata.type": "microsoft.graph.openTypeExtension", "extension_name": + "com.nexusidentity.keys", "publicKey": "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIJet7nknqBQpavVf6gjjjlcHUfckuGI4eZDJxG4tlSSHAAAABHNzaDo= + fareast\\jtamma@DESKTOP-QDMRQ39\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '274' + content-type: + - application/json + host: + - graph.microsoft.com + user-agent: + - python-httpx/0.27.0 + method: PATCH + uri: https://graph.microsoft.com/v1.0/users/me-token-to-replace/extensions/com.nexusidentity.keys + response: + body: + string: '' + headers: + date: + - Mon, 03 Jun 2024 20:35:57 GMT + request-id: + - e5612a3d-5086-4e68-873d-7c8f4f5a573e + strict-transport-security: + - max-age=31536000 + x-ms-ags-diagnostic: + - '{"ServerInfo":{"DataCenter":"Central India","Slice":"E","Ring":"3","ScaleUnit":"001","RoleInstance":"PN3PEPF000002BF"}}' + status: + code: 204 + message: No Content +version: 1 diff --git a/src/nexusidentity/azext_nexusidentity/tests/latest/test_nexusidentity_scenario.py b/src/nexusidentity/azext_nexusidentity/tests/latest/test_nexusidentity_scenario.py new file mode 100644 index 00000000000..437491c5ea4 --- /dev/null +++ b/src/nexusidentity/azext_nexusidentity/tests/latest/test_nexusidentity_scenario.py @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +''' +Nexus Identity Ssh-Key Geneation Scenario Test +''' + +from azure.cli.testsdk import ScenarioTest, ResourceGroupPreparer + +def setup_scenario1(test): + ''' Env setup_scenario1 ''' + pass + + +def cleanup_scenario1(test): + '''Env cleanup_scenario1 ''' + pass + +def call_scenario1(test): + ''' # Testcase: scenario1''' + setup_scenario1(test) + step_gen_keys(test, checks=[]) + cleanup_scenario1(test) + +def step_gen_keys(test, checks=None): + '''Generate Nexus Identity ssh keys ''' + if checks is None: + checks = [] + test.cmd('az nexusidentity gen-keys') + +class NexusidentityScenarioTest(ScenarioTest): + ''' Nexus Identity Ssh-Key Generation Scenario Test ''' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def test_nexusidentity_scenario1(self): + + # Testcase: scenario1 + call_scenario1(self) \ No newline at end of file diff --git a/src/nexusidentity/setup.cfg b/src/nexusidentity/setup.cfg new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/nexusidentity/setup.py b/src/nexusidentity/setup.py new file mode 100644 index 00000000000..fb8fcfe2e8d --- /dev/null +++ b/src/nexusidentity/setup.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from codecs import open +from setuptools import setup, find_packages +try: + from azure_bdist_wheel import cmdclass +except ImportError: + from distutils import log as logger + logger.warn("Wheel is not available, disabling bdist_wheel hook") + +# TODO: Confirm this is the right version number you want and it matches your +# HISTORY.rst entry. +VERSION = '1.0.0b1' + +# The full list of classifiers is available at +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'License :: OSI Approved :: MIT License', +] + +# TODO: Add any additional SDK dependencies here +DEPENDENCIES = ['azure-identity', 'msgraph-sdk'] + +with open('README.rst', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name='nexusidentity', + version=VERSION, + description='Microsoft Azure Command-Line Tools Nexusidentity Extension', + # TODO: Update author and email, if applicable + author='Microsoft Corporation', + author_email='azpycli@microsoft.com', + # TODO: change to your extension source code repo if the code will not be + # put in azure-cli-extensions repo + url='https://github.com/Azure/azure-cli-extensions/tree/master/src/nexusidentity', + long_description=README + '\n\n' + HISTORY, + license='MIT', + classifiers=CLASSIFIERS, + packages=find_packages(exclude=["tests"]), + install_requires=DEPENDENCIES, + package_data={'azext_nexusidentity': ['azext_metadata.json']}, +) diff --git a/src/service_name.json b/src/service_name.json index f5e850d3187..a7f363efeba 100644 --- a/src/service_name.json +++ b/src/service_name.json @@ -874,6 +874,11 @@ "AzureServiceName": "Azure Compute Gallery", "URL": "https://learn.microsoft.com/en-us/azure/virtual-machines/azure-compute-gallery" }, + { + "Command": "az nexusidentity", + "AzureServiceName": "Nexus Identity", + "URL": "" + }, { "Command": "az storage-actions", "AzureServiceName": "Azure Storage Actions",