From 8272e515d2860c646648dfa765cea8903c4faa05 Mon Sep 17 00:00:00 2001 From: David Benson Date: Thu, 4 Sep 2025 08:23:08 -0400 Subject: [PATCH 1/5] adding service principal authentication --- tools/paconn-cli/paconn/authentication/auth.py | 8 ++++++-- .../paconn-cli/paconn/authentication/profile.py | 17 +++++++++++++++++ tools/paconn-cli/paconn/commands/login.py | 5 +++-- tools/paconn-cli/paconn/commands/params.py | 6 ++++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tools/paconn-cli/paconn/authentication/auth.py b/tools/paconn-cli/paconn/authentication/auth.py index 7b4254fc16..eaf9e5c647 100644 --- a/tools/paconn-cli/paconn/authentication/auth.py +++ b/tools/paconn-cli/paconn/authentication/auth.py @@ -14,7 +14,7 @@ from paconn.authentication.tokenmanager import TokenManager -def get_authentication(settings, force_authenticate): +def get_authentication(settings, force_authenticate, client_secret=None): """ Logs the user in and saves the token in a file. """ @@ -31,7 +31,11 @@ def get_authentication(settings, force_authenticate): resource=settings.resource, authority_url=settings.authority_url) - credentials = profile.authenticate_device_code() + # Use service principal authentication if client_secret is provided + if client_secret: + credentials = profile.authenticate_service_principal(client_secret) + else: + credentials = profile.authenticate_device_code() tokenmanager.write(credentials) diff --git a/tools/paconn-cli/paconn/authentication/profile.py b/tools/paconn-cli/paconn/authentication/profile.py index 44f2faa2be..5542976169 100644 --- a/tools/paconn-cli/paconn/authentication/profile.py +++ b/tools/paconn-cli/paconn/authentication/profile.py @@ -53,3 +53,20 @@ def authenticate_device_code(self): client_id=self.client_id) return credentials.token + + def authenticate_service_principal(self, client_secret): + """ + Authenticate using service principal credentials. + """ + context = self._get_authentication_context() + + mgmt_token = context.acquire_token_with_client_credentials( + resource=self.resource, + client_id=self.client_id, + client_secret=client_secret) + + credentials = AADTokenCredentials( + token=mgmt_token, + client_id=self.client_id) + + return credentials.token diff --git a/tools/paconn-cli/paconn/commands/login.py b/tools/paconn-cli/paconn/commands/login.py index bfc3940b68..15197e2ead 100644 --- a/tools/paconn-cli/paconn/commands/login.py +++ b/tools/paconn-cli/paconn/commands/login.py @@ -12,7 +12,7 @@ from paconn.settings.settingsbuilder import SettingsBuilder -def login(client_id, tenant, authority_url, resource, settings_file, force): +def login(client_id, tenant, authority_url, resource, settings_file, force, client_secret=None): """ Login command. """ @@ -26,5 +26,6 @@ def login(client_id, tenant, authority_url, resource, settings_file, force): get_authentication( settings=settings, - force_authenticate=force) + force_authenticate=force, + client_secret=client_secret) display('Login successful.') diff --git a/tools/paconn-cli/paconn/commands/params.py b/tools/paconn-cli/paconn/commands/params.py index e93df34d46..f3da7097ba 100644 --- a/tools/paconn-cli/paconn/commands/params.py +++ b/tools/paconn-cli/paconn/commands/params.py @@ -90,6 +90,12 @@ def load_arguments(self, command): type=str, required=False, help='Resource URL for login.') + arg_context.argument( + 'client_secret', + options_list=['--secret', '-k'], + type=str, + required=False, + help='Client secret for service principal authentication.') arg_context.argument( SETTINGS, options_list=SETTINGS_OPTIONS, From 6bd5ade5f3f2162f9478178fa3e46a3c8c874f08 Mon Sep 17 00:00:00 2001 From: David Benson Date: Thu, 4 Sep 2025 17:07:27 -0400 Subject: [PATCH 2/5] fixing login for service principals --- .../paconn/authentication/profile.py | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tools/paconn-cli/paconn/authentication/profile.py b/tools/paconn-cli/paconn/authentication/profile.py index 5542976169..8540766d80 100644 --- a/tools/paconn-cli/paconn/authentication/profile.py +++ b/tools/paconn-cli/paconn/authentication/profile.py @@ -11,6 +11,8 @@ from urllib.parse import urljoin # AADTokenCredentials for multi-factor authentication from msrestazure.azure_active_directory import AADTokenCredentials +import requests +import json class Profile: @@ -54,12 +56,54 @@ def authenticate_device_code(self): return credentials.token + def _get_service_principal_object_id(self, graph_token, client_id): + """ + Get the object ID of the service principal using Microsoft Graph API. + """ + try: + # Extract access token from the ADAL token response + access_token = graph_token.get('accessToken') or graph_token.get('access_token') + if not access_token: + return client_id + + # Microsoft Graph API endpoint to get service principal by appId + url = f"https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appId eq '{client_id}'" + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + + response = requests.get(url, headers=headers) + response.raise_for_status() + + data = response.json() + if data.get('value') and len(data['value']) > 0: + return data['value'][0]['id'] # This is the object ID + else: + # Fallback to client_id if we can't find the service principal + return client_id + except Exception: + # Fallback to client_id if Graph API call fails + return client_id + def authenticate_service_principal(self, client_secret): """ Authenticate using service principal credentials. """ context = self._get_authentication_context() + # First, get a token for Microsoft Graph to look up the service principal object ID + graph_token = context.acquire_token_with_client_credentials( + resource="https://graph.microsoft.com/", + client_id=self.client_id, + client_secret=client_secret) + + # Get the service principal's actual object ID + sp_object_id = self._get_service_principal_object_id( + graph_token, + self.client_id) + + # Now get the token for the actual resource mgmt_token = context.acquire_token_with_client_credentials( resource=self.resource, client_id=self.client_id, @@ -69,4 +113,8 @@ def authenticate_service_principal(self, client_secret): token=mgmt_token, client_id=self.client_id) - return credentials.token + # Add the correct oid (service principal object ID) + token = credentials.token + token['oid'] = sp_object_id + + return token From 567cee65d72b234c944fb22fcc7d54ec231bfb5d Mon Sep 17 00:00:00 2001 From: David Benson Date: Wed, 3 Sep 2025 10:46:52 -0400 Subject: [PATCH 3/5] Add list, delete, and share commands with raw JSON option for list - Add list command with --raw/-j flag to output raw JSON instead of friendly format by default - Add delete command with confirmation prompt and --force option - Add share command for modifying connector permissions - Extend PowerAppsRP API with delete_connector and modify_permissions methods - Register new commands in command table and add parameter definitions --- tools/paconn-cli/paconn/__init__.py | 3 + .../paconn/apimanager/powerappsrp.py | 45 +++++++ tools/paconn-cli/paconn/commands/commands.py | 11 +- tools/paconn-cli/paconn/commands/delete.py | 76 +++++++++++ tools/paconn-cli/paconn/commands/list.py | 89 +++++++++++++ tools/paconn-cli/paconn/commands/params.py | 118 +++++++++++++++++- tools/paconn-cli/paconn/commands/share.py | 74 +++++++++++ 7 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 tools/paconn-cli/paconn/commands/delete.py create mode 100644 tools/paconn-cli/paconn/commands/list.py create mode 100644 tools/paconn-cli/paconn/commands/share.py diff --git a/tools/paconn-cli/paconn/__init__.py b/tools/paconn-cli/paconn/__init__.py index d40051dda6..e535368a73 100644 --- a/tools/paconn-cli/paconn/__init__.py +++ b/tools/paconn-cli/paconn/__init__.py @@ -20,3 +20,6 @@ _UPDATE = 'update' _VALIDATE = 'validate' _CONVERT = 'convert' +_LIST = 'list' +_SHARE = 'share' +_DELETE = 'delete' diff --git a/tools/paconn-cli/paconn/apimanager/powerappsrp.py b/tools/paconn-cli/paconn/apimanager/powerappsrp.py index f7da39a218..864afde160 100644 --- a/tools/paconn-cli/paconn/apimanager/powerappsrp.py +++ b/tools/paconn-cli/paconn/apimanager/powerappsrp.py @@ -129,3 +129,48 @@ def generate_resource_storage(self, environment): payload=payload) return json.loads(response.text) + + def modify_permissions(self, environment, connector_id, permissions_data): + """ + Modifies permissions for a custom connector. + """ + api = urljoin('apis/', connector_id + '/modifyPermissions') + + endpoint = self.api_manager.construct_url( + path=api, + query=PowerAppsRP._get_filter_query(environment)) + + # Build the payload in the expected format + payload = { + "put": [ + { + "properties": permissions_data + } + ] + } + + response = self.api_manager.request( + verb='POST', + endpoint=endpoint, + payload=payload, + headers=self.rp_headers) + + return response.text + + def delete_connector(self, environment, connector_id): + """ + Deletes a custom connector. + """ + api = urljoin('apis/', connector_id) + + endpoint = self.api_manager.construct_url( + path=api, + query=PowerAppsRP._get_filter_query(environment)) + + response = self.api_manager.request( + verb='DELETE', + endpoint=endpoint, + headers=self.rp_headers) + + return response.text + diff --git a/tools/paconn-cli/paconn/commands/commands.py b/tools/paconn-cli/paconn/commands/commands.py index 00d9c23066..b2487ad833 100644 --- a/tools/paconn-cli/paconn/commands/commands.py +++ b/tools/paconn-cli/paconn/commands/commands.py @@ -11,7 +11,7 @@ from knack.commands import CommandGroup from paconn import __CLI_NAME__ -from paconn import _COMMAND_GROUP, _LOGIN, _LOGOUT, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE, _CONVERT +from paconn import _COMMAND_GROUP, _LOGIN, _LOGOUT, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE, _CONVERT, _LIST, _SHARE, _DELETE # pylint: disable=unused-argument @@ -42,3 +42,12 @@ def operation_group(name): with CommandGroup(self, _COMMAND_GROUP, operation_group(_CONVERT)) as command_group: command_group.command(_CONVERT, _CONVERT) + + with CommandGroup(self, _COMMAND_GROUP, operation_group(_LIST)) as command_group: + command_group.command(_LIST, _LIST) + + with CommandGroup(self, _COMMAND_GROUP, operation_group(_SHARE)) as command_group: + command_group.command(_SHARE, _SHARE) + + with CommandGroup(self, _COMMAND_GROUP, operation_group(_DELETE)) as command_group: + command_group.command(_DELETE, _DELETE) diff --git a/tools/paconn-cli/paconn/commands/delete.py b/tools/paconn-cli/paconn/commands/delete.py new file mode 100644 index 0000000000..3aef64f381 --- /dev/null +++ b/tools/paconn-cli/paconn/commands/delete.py @@ -0,0 +1,76 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- +""" +Delete command. +""" + +from paconn import _DELETE +from paconn.common.util import display +from paconn.settings.util import load_powerapps_and_flow_rp +from paconn.settings.settingsbuilder import SettingsBuilder +from knack.prompting import prompt_y_n + + +def delete( + environment, + connector_id, + force, + powerapps_url, + powerapps_version, + settings_file): + """ + Delete command to remove a custom connector. + """ + # Get settings + settings = SettingsBuilder.get_settings( + environment=environment, + settings_file=settings_file, + connector_id=connector_id, + powerapps_url=powerapps_url, + powerapps_version=powerapps_version, + api_properties=None, + api_definition=None, + icon=None, + script=None) + + powerapps_rp, _ = load_powerapps_and_flow_rp( + settings=settings, + command_context=_DELETE) + + # Get connector info for display purposes + try: + connector_info = powerapps_rp.get_connector( + environment=settings.environment, + connector_id=settings.connector_id) + + connector_name = connector_info.get('properties', {}).get('displayName', settings.connector_id) + display(f'Target connector: {connector_name} ({settings.connector_id})') + except Exception: + # If we can't get connector info, just proceed with the ID + connector_name = settings.connector_id + display(f'Target connector: {settings.connector_id}') + + # Confirm deletion unless force is specified + if not force: + confirm = prompt_y_n(f'Are you sure you want to delete connector "{connector_name}"? ' + 'This action cannot be undone.') + if not confirm: + display('Delete operation cancelled.') + return + + # Perform the deletion + try: + response_text = powerapps_rp.delete_connector( + environment=settings.environment, + connector_id=settings.connector_id) + + display(f'Connector "{connector_name}" deleted successfully.') + if response_text and response_text.strip(): + display(f'Response: {response_text}') + + except Exception as e: + display(f'Error deleting connector: {str(e)}') + raise diff --git a/tools/paconn-cli/paconn/commands/list.py b/tools/paconn-cli/paconn/commands/list.py new file mode 100644 index 0000000000..045f86b0aa --- /dev/null +++ b/tools/paconn-cli/paconn/commands/list.py @@ -0,0 +1,89 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- +""" +List command. +""" + +import json + +from paconn import _LIST + +from paconn.common.util import display +from paconn.settings.util import load_powerapps_and_flow_rp +from paconn.settings.settingsbuilder import SettingsBuilder + +# Constants for response parsing +_PROPERTIES = 'properties' +_IS_CUSTOM_API = 'isCustomApi' + + +def list( + environment, + powerapps_url, + powerapps_version, + settings_file, + raw_json=False): + """ + List command. + """ + # Get settings + settings = SettingsBuilder.get_settings( + environment=environment, + settings_file=settings_file, + api_properties=None, + api_definition=None, + icon=None, + script=None, + connector_id=None, + powerapps_url=powerapps_url, + powerapps_version=powerapps_version) + + powerapps_rp, _ = load_powerapps_and_flow_rp( + settings=settings, + command_context=_LIST) + + connectors = powerapps_rp.get_all_connectors( + environment=settings.environment) + + if 'value' in connectors and connectors['value']: + # Filter to only custom connectors + all_connectors = connectors['value'] + custom_connectors = [ + conn for conn in all_connectors + if conn.get(_PROPERTIES, {}).get(_IS_CUSTOM_API, False) + ] + + if custom_connectors: + if raw_json: + display(json.dumps(custom_connectors, indent=2)) + else: + display('Found {} custom connector(s) in environment {}:'.format( + len(custom_connectors), + settings.environment)) + + for connector in custom_connectors: + display(' - {}'.format( + connector.get('name', 'Unknown'))) + + # Show additional details if available + properties = connector.get('properties', {}) + display_name = properties.get('displayName', '') + description = properties.get('description', '') + created_by = properties.get('createdBy', {}) + creator_name = created_by.get('displayName', '') if created_by else '' + + if display_name: + display(' Display Name: {}'.format(display_name)) + if description: + display(' Description: {}'.format(description[:100] + ('...' if len(description) > 100 else ''))) + if creator_name: + display(' Created By: {}'.format(creator_name)) + + display('') # Empty line for readability + else: + display('No custom connectors found in environment {}.'.format(settings.environment)) + else: + display('No custom connectors found in environment {}.'.format(settings.environment)) diff --git a/tools/paconn-cli/paconn/commands/params.py b/tools/paconn-cli/paconn/commands/params.py index f3da7097ba..e79837dc9e 100644 --- a/tools/paconn-cli/paconn/commands/params.py +++ b/tools/paconn-cli/paconn/commands/params.py @@ -9,7 +9,7 @@ """ from knack.arguments import ArgumentsContext -from paconn import _LOGIN, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE, _CONVERT +from paconn import _LOGIN, _DOWNLOAD, _CREATE, _UPDATE, _VALIDATE, _CONVERT, _LIST, _SHARE, _DELETE CLIENT_SECRET = 'client_secret' CLIENT_SECRET_OPTIONS = ['--secret', '-r'] @@ -59,6 +59,10 @@ DESTINATION_OPTIONS = ['--dest', '-dst'] DESTINATION_HELP = 'Destination directory for the converted connector files.' +PERMISSIONS_FILE = 'permissions_file' +PERMISSIONS_FILE_OPTIONS = ['--permissions', '-perm'] +PERMISSIONS_FILE_HELP = 'Location of the permissions JSON file containing roleName and principal information.' + # pylint: disable=unused-argument def load_arguments(self, command): @@ -331,3 +335,115 @@ def load_arguments(self, command): type=str, required=False, help=SETTINGS_HELP) + + with ArgumentsContext(self, _LIST) as arg_context: + arg_context.argument( + ENVIRONMENT, + options_list=ENVIRONMENT_OPTIONS, + type=str, + required=False, + help=ENVIRONMENT_HELP) + arg_context.argument( + POWERAPPS_URL, + options_list=POWERAPPS_URL_OPTIONS, + type=str, + required=False, + help=POWERAPPS_URL_HELP) + arg_context.argument( + POWERAPPS_VERSION, + options_list=POWERAPPS_VERSION_OPTIONS, + type=str, + required=False, + help=POWERAPPS_VERSION_HELP) + arg_context.argument( + SETTINGS, + options_list=SETTINGS_OPTIONS, + type=str, + required=False, + help=SETTINGS_HELP) + arg_context.argument( + 'raw_json', + options_list=['--raw', '-j'], + action='store_true', + help='Output raw JSON for custom connectors instead of friendly format.') + + with ArgumentsContext(self, _SHARE) as arg_context: + arg_context.argument( + ENVIRONMENT, + options_list=ENVIRONMENT_OPTIONS, + type=str, + required=False, + help=ENVIRONMENT_HELP) + arg_context.argument( + CONNECTOR_ID, + options_list=CONNECTOR_ID_OPTIONS, + type=str, + required=False, + help=CONNECTOR_ID_HELP) + arg_context.argument( + PERMISSIONS_FILE, + options_list=PERMISSIONS_FILE_OPTIONS, + type=str, + required=True, + help=PERMISSIONS_FILE_HELP) + arg_context.argument( + POWERAPPS_URL, + options_list=POWERAPPS_URL_OPTIONS, + type=str, + required=False, + help=POWERAPPS_URL_HELP) + arg_context.argument( + POWERAPPS_VERSION, + options_list=POWERAPPS_VERSION_OPTIONS, + type=str, + required=False, + help=POWERAPPS_VERSION_HELP) + arg_context.argument( + SETTINGS, + options_list=SETTINGS_OPTIONS, + type=str, + required=False, + help=SETTINGS_HELP) + + + with ArgumentsContext(self, _DELETE) as arg_context: + arg_context.argument( + ENVIRONMENT, + options_list=ENVIRONMENT_OPTIONS, + type=str, + required=False, + help=ENVIRONMENT_HELP) + arg_context.argument( + CONNECTOR_ID, + options_list=CONNECTOR_ID_OPTIONS, + type=str, + required=False, + help=CONNECTOR_ID_HELP) + arg_context.argument( + 'force', + options_list=['--force', '-f'], + type=bool, + required=False, + nargs='?', + default=False, + const=True, + help='Force deletion without confirmation prompt.') + arg_context.argument( + POWERAPPS_URL, + options_list=POWERAPPS_URL_OPTIONS, + type=str, + required=False, + help=POWERAPPS_URL_HELP) + arg_context.argument( + POWERAPPS_VERSION, + options_list=POWERAPPS_VERSION_OPTIONS, + type=str, + required=False, + help=POWERAPPS_VERSION_HELP) + arg_context.argument( + SETTINGS, + options_list=SETTINGS_OPTIONS, + type=str, + required=False, + help=SETTINGS_HELP) + diff --git a/tools/paconn-cli/paconn/commands/share.py b/tools/paconn-cli/paconn/commands/share.py new file mode 100644 index 0000000000..2a674a6674 --- /dev/null +++ b/tools/paconn-cli/paconn/commands/share.py @@ -0,0 +1,74 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- +""" +Share command. +""" + +import json +import os + +from paconn import _SHARE +from paconn.common.util import display +from paconn.settings.util import load_powerapps_and_flow_rp +from paconn.settings.settingsbuilder import SettingsBuilder + + +def share( + environment, + connector_id, + permissions_file, + powerapps_url, + powerapps_version, + settings_file): + """ + Share command to modify connector permissions. + """ + # Get settings + settings = SettingsBuilder.get_settings( + environment=environment, + settings_file=settings_file, + connector_id=connector_id, + powerapps_url=powerapps_url, + powerapps_version=powerapps_version, + api_properties=None, + api_definition=None, + icon=None, + script=None) + + powerapps_rp, _ = load_powerapps_and_flow_rp( + settings=settings, + command_context=_SHARE) + + # Load and validate the permissions JSON file + if not os.path.exists(permissions_file): + raise ValueError(f"Permissions file not found: {permissions_file}") + + try: + with open(permissions_file, 'r', encoding='utf-8') as f: + permissions_data = json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in permissions file: {e}") + + # Validate required fields in the permissions data + required_fields = ['roleName', 'principal'] + for field in required_fields: + if field not in permissions_data: + raise ValueError(f"Missing required field in permissions file: {field}") + + principal = permissions_data['principal'] + required_principal_fields = ['email', 'id', 'type'] + for field in required_principal_fields: + if field not in principal: + raise ValueError(f"Missing required field in principal: {field}") + + # Call the API to modify permissions + response_text = powerapps_rp.modify_permissions( + environment=settings.environment, + connector_id=settings.connector_id, + permissions_data=permissions_data) + + display(f'Permissions modified successfully for connector {settings.connector_id}.') + display(f'Response: {response_text}') From ebbf73b6bad193c4d2217fe3949633b6eacbe385 Mon Sep 17 00:00:00 2001 From: David Benson Date: Thu, 4 Sep 2025 08:59:46 -0400 Subject: [PATCH 4/5] clean up list command output --- tools/paconn-cli/paconn/commands/list.py | 52 +++++++++------------- tools/paconn-cli/paconn/commands/params.py | 5 --- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/tools/paconn-cli/paconn/commands/list.py b/tools/paconn-cli/paconn/commands/list.py index 045f86b0aa..8b49c19c67 100644 --- a/tools/paconn-cli/paconn/commands/list.py +++ b/tools/paconn-cli/paconn/commands/list.py @@ -7,8 +7,6 @@ List command. """ -import json - from paconn import _LIST from paconn.common.util import display @@ -24,8 +22,7 @@ def list( environment, powerapps_url, powerapps_version, - settings_file, - raw_json=False): + settings_file): """ List command. """ @@ -57,33 +54,28 @@ def list( ] if custom_connectors: - if raw_json: - display(json.dumps(custom_connectors, indent=2)) - else: - display('Found {} custom connector(s) in environment {}:'.format( - len(custom_connectors), - settings.environment)) - - for connector in custom_connectors: - display(' - {}'.format( - connector.get('name', 'Unknown'))) - - # Show additional details if available - properties = connector.get('properties', {}) - display_name = properties.get('displayName', '') - description = properties.get('description', '') - created_by = properties.get('createdBy', {}) - creator_name = created_by.get('displayName', '') if created_by else '' - - if display_name: - display(' Display Name: {}'.format(display_name)) - if description: - display(' Description: {}'.format(description[:100] + ('...' if len(description) > 100 else ''))) - if creator_name: - display(' Created By: {}'.format(creator_name)) - - display('') # Empty line for readability + # Return structured data with PascalCase keys for knack to format + result_data = [] + for connector in custom_connectors: + properties = connector.get('properties', {}) + created_by = properties.get('createdBy', {}) + + connector_data = { + 'Name': connector.get('name', ''), + 'Id': connector.get('id', ''), + 'Type': connector.get('type', ''), + 'DisplayName': properties.get('displayName', ''), + 'IconUri': properties.get('iconUri', ''), + 'IconBrandColor': properties.get('iconBrandColor', ''), + 'Description': properties.get('description', ''), + 'CreatedBy': created_by.get('displayName', '') if created_by else '' + } + result_data.append(connector_data) + + return result_data else: display('No custom connectors found in environment {}.'.format(settings.environment)) + return [] else: display('No custom connectors found in environment {}.'.format(settings.environment)) + return [] diff --git a/tools/paconn-cli/paconn/commands/params.py b/tools/paconn-cli/paconn/commands/params.py index e79837dc9e..f96457e952 100644 --- a/tools/paconn-cli/paconn/commands/params.py +++ b/tools/paconn-cli/paconn/commands/params.py @@ -361,11 +361,6 @@ def load_arguments(self, command): type=str, required=False, help=SETTINGS_HELP) - arg_context.argument( - 'raw_json', - options_list=['--raw', '-j'], - action='store_true', - help='Output raw JSON for custom connectors instead of friendly format.') with ArgumentsContext(self, _SHARE) as arg_context: arg_context.argument( From 841ee9fffe984ee089fcea844e0ad04bdc8538d5 Mon Sep 17 00:00:00 2001 From: David Benson Date: Thu, 4 Sep 2025 11:24:22 -0400 Subject: [PATCH 5/5] fixing permissions check to be more generic --- tools/paconn-cli/paconn/commands/share.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tools/paconn-cli/paconn/commands/share.py b/tools/paconn-cli/paconn/commands/share.py index 2a674a6674..882ea2897e 100644 --- a/tools/paconn-cli/paconn/commands/share.py +++ b/tools/paconn-cli/paconn/commands/share.py @@ -58,12 +58,6 @@ def share( if field not in permissions_data: raise ValueError(f"Missing required field in permissions file: {field}") - principal = permissions_data['principal'] - required_principal_fields = ['email', 'id', 'type'] - for field in required_principal_fields: - if field not in principal: - raise ValueError(f"Missing required field in principal: {field}") - # Call the API to modify permissions response_text = powerapps_rp.modify_permissions( environment=settings.environment,