Skip to content
Draft
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: 3 additions & 0 deletions tools/paconn-cli/paconn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@
_UPDATE = 'update'
_VALIDATE = 'validate'
_CONVERT = 'convert'
_LIST = 'list'
_SHARE = 'share'
_DELETE = 'delete'
45 changes: 45 additions & 0 deletions tools/paconn-cli/paconn/apimanager/powerappsrp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

8 changes: 6 additions & 2 deletions tools/paconn-cli/paconn/authentication/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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)

Expand Down
65 changes: 65 additions & 0 deletions tools/paconn-cli/paconn/authentication/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -53,3 +55,66 @@ def authenticate_device_code(self):
client_id=self.client_id)

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,
client_secret=client_secret)

credentials = AADTokenCredentials(
token=mgmt_token,
client_id=self.client_id)

# Add the correct oid (service principal object ID)
token = credentials.token
token['oid'] = sp_object_id

return token
11 changes: 10 additions & 1 deletion tools/paconn-cli/paconn/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
76 changes: 76 additions & 0 deletions tools/paconn-cli/paconn/commands/delete.py
Original file line number Diff line number Diff line change
@@ -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
81 changes: 81 additions & 0 deletions tools/paconn-cli/paconn/commands/list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -----------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# -----------------------------------------------------------------------------
"""
List command.
"""

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):
"""
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:
# 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 []
5 changes: 3 additions & 2 deletions tools/paconn-cli/paconn/commands/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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.')
Loading