Skip to content

Commit

Permalink
new extension for nexus identity feature (#7641)
Browse files Browse the repository at this point in the history
* new extension for nexus identity feature
  • Loading branch information
jtamma committed Jun 6, 2024
1 parent 285962b commit acc3e20
Show file tree
Hide file tree
Showing 17 changed files with 399 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/nexusidentity/HISTORY.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. :changelog:
Release History
===============

1.0.0b1
++++++
* Initial release.
5 changes: 5 additions & 0 deletions src/nexusidentity/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Microsoft Azure CLI 'nexusidentity' Extension
==========================================

This package is for the 'nexusidentity' extension.
i.e. 'az nexusidentity'
32 changes: 32 additions & 0 deletions src/nexusidentity/azext_nexusidentity/__init__.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions src/nexusidentity/azext_nexusidentity/_client_factory.py
Original file line number Diff line number Diff line change
@@ -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.
# --------------------------------------------------------------------------------------------
18 changes: 18 additions & 0 deletions src/nexusidentity/azext_nexusidentity/_help.py
Original file line number Diff line number Diff line change
@@ -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.
"""
12 changes: 12 additions & 0 deletions src/nexusidentity/azext_nexusidentity/_params.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions src/nexusidentity/azext_nexusidentity/_validators.py
Original file line number Diff line number Diff line change
@@ -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.
# --------------------------------------------------------------------------------------------
4 changes: 4 additions & 0 deletions src/nexusidentity/azext_nexusidentity/azext_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"azext.isPreview": true,
"azext.minCliCoreVersion": "2.59.0"
}
13 changes: 13 additions & 0 deletions src/nexusidentity/azext_nexusidentity/commands.py
Original file line number Diff line number Diff line change
@@ -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')
105 changes: 105 additions & 0 deletions src/nexusidentity/azext_nexusidentity/custom.py
Original file line number Diff line number Diff line change
@@ -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")
5 changes: 5 additions & 0 deletions src/nexusidentity/azext_nexusidentity/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
# -----------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -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.
# -----------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -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":"[email protected]
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": "[email protected] 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
Original file line number Diff line number Diff line change
@@ -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)
Empty file added src/nexusidentity/setup.cfg
Empty file.
Loading

0 comments on commit acc3e20

Please sign in to comment.