Fix for multiple accounts and caching
This commit is contained in:
Родитель
09bd9e3152
Коммит
604dfc0b32
|
@ -13,7 +13,7 @@
|
|||
<ProjectTypeGuids>{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
|
||||
<LaunchProvider>Standard Python launcher</LaunchProvider>
|
||||
<InterpreterId>MSBuild|venv|$(MSBuildProjectFullPath)</InterpreterId>
|
||||
<CommandLineArguments>validate -d D:\src\PowerPlatformConnectors\tools\paconn-cli\shared_bugbash-5fcustomcodeconnectora-5f9518e3c234f950a8\apiDefinition.swagger.json</CommandLineArguments>
|
||||
<CommandLineArguments>logout</CommandLineArguments>
|
||||
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
|
||||
<Name>paconn</Name>
|
||||
<IsWindowsApplication>False</IsWindowsApplication>
|
||||
|
@ -27,7 +27,6 @@
|
|||
<Compile Include="paconn\apimanager\__init__.py" />
|
||||
<Compile Include="paconn\authentication\auth.py" />
|
||||
<Compile Include="paconn\authentication\msalprofile.py" />
|
||||
<Compile Include="paconn\authentication\tokenmanager.py" />
|
||||
<Compile Include="paconn\authentication\__init__.py" />
|
||||
<Compile Include="paconn\commands\commands.py" />
|
||||
<Compile Include="paconn\commands\logout.py" />
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
Initializer
|
||||
"""
|
||||
|
||||
__VERSION__ = '0.1.1'
|
||||
__VERSION__ = '0.1.2'
|
||||
__CLI_NAME__ = 'paconn'
|
||||
|
||||
# Commands
|
||||
|
|
|
@ -7,22 +7,54 @@
|
|||
"""
|
||||
Authentication methods
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from paconn.authentication.msalprofile import MsalProfile
|
||||
from paconn.authentication.tokenmanager import TokenManager
|
||||
from paconn.settings.authsettings import AuthSettings
|
||||
from paconn.settings.authsettingsserializer import AuthSettingsSerializer
|
||||
from paconn.common.util import get_config_dir
|
||||
from msal_extensions import *
|
||||
|
||||
def get_authentication(settings, auth_type='interactive'):
|
||||
TOKEN_FILE = 'accessTokens.bin'
|
||||
|
||||
def _build_persistence(token_file, fallback_to_plaintext=False):
|
||||
"""Build a suitable persistence instance based your current OS"""
|
||||
if sys.platform.startswith('win'):
|
||||
return FilePersistenceWithDataProtection(token_file)
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
return KeychainPersistence(token_file, "paconn", "paconn")
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
try:
|
||||
return LibsecretPersistence(
|
||||
token_file,
|
||||
schema_name='paconn',
|
||||
attributes={"appName": "paconn"})
|
||||
except:
|
||||
if not fallback_to_plaintext:
|
||||
raise
|
||||
logging.exception("Encryption unavailable. Opting in to plain text.")
|
||||
|
||||
return FilePersistence(token_file)
|
||||
|
||||
def _get_token_cache():
|
||||
token_file = os.path.join(get_config_dir(), TOKEN_FILE)
|
||||
persistence = _build_persistence(token_file)
|
||||
token_cache = PersistedTokenCache(persistence)
|
||||
|
||||
return token_cache
|
||||
|
||||
def get_authentication(settings, auth_type='interactive', force_interactive=False):
|
||||
"""
|
||||
Logs the user in and saves the token in a file.
|
||||
"""
|
||||
|
||||
|
||||
# Read authentication settings
|
||||
auth_settings = AuthSettingsSerializer.read_with_cli_settings(settings)
|
||||
|
||||
# Read last saved token
|
||||
tokenmanager = TokenManager()
|
||||
token_cache = tokenmanager.read()
|
||||
token_cache = _get_token_cache()
|
||||
|
||||
# Get new token
|
||||
profile = MsalProfile(
|
||||
|
@ -30,18 +62,17 @@ def get_authentication(settings, auth_type='interactive'):
|
|||
token_cache=token_cache)
|
||||
|
||||
if auth_type == 'interactive':
|
||||
(result, account) = profile.auth_interactive()
|
||||
(result, account) = profile.auth_interactive(force_interactive=force_interactive)
|
||||
else:
|
||||
(result, account) = profile.authenticate_silent()
|
||||
|
||||
# Save token
|
||||
if token_cache.has_state_changed:
|
||||
tokenmanager.write(token_cache)
|
||||
|
||||
# Save authentication settings
|
||||
if account:
|
||||
if account and 'username' in account:
|
||||
auth_settings.username = account['username']
|
||||
AuthSettingsSerializer.write(auth_settings)
|
||||
else:
|
||||
auth_settings.username = None
|
||||
|
||||
AuthSettingsSerializer.write(auth_settings)
|
||||
|
||||
return (result, account)
|
||||
|
||||
|
@ -51,5 +82,11 @@ def get_silent_authentication():
|
|||
auth_type = 'silent')
|
||||
|
||||
def remove_authentication():
|
||||
tokenmanager = TokenManager()
|
||||
tokenmanager.delete_token_file()
|
||||
# Read authentication settings
|
||||
auth_settings = AuthSettingsSerializer.read()
|
||||
token_cache = _get_token_cache()
|
||||
profile = MsalProfile(
|
||||
auth_settings=auth_settings,
|
||||
token_cache=token_cache)
|
||||
account = profile.logout()
|
||||
return account
|
||||
|
|
|
@ -9,7 +9,6 @@ User profile management class.`
|
|||
"""
|
||||
from msal import PublicClientApplication
|
||||
from knack.prompting import prompt_choice_list
|
||||
from paconn.common.util import format_json
|
||||
from knack.util import CLIError
|
||||
from paconn.settings.authsettings import AuthSettings
|
||||
|
||||
|
@ -26,60 +25,96 @@ class MsalProfile:
|
|||
authority=auth_settings.get_authority_tenant(),
|
||||
token_cache=token_cache)
|
||||
|
||||
def get_account_from_username(self, username, prompt=False):
|
||||
accounts = self.app.get_accounts(username)
|
||||
if accounts:
|
||||
return accounts[0]
|
||||
|
||||
return None
|
||||
|
||||
def authenticate_silent(self):
|
||||
result = None
|
||||
account = self.get_account_from_username(
|
||||
username=self.auth_settings.username)
|
||||
|
||||
result = self.app.acquire_token_silent_with_error(
|
||||
scopes=self.auth_settings.scopes,
|
||||
account=account,
|
||||
force_refresh=True)
|
||||
|
||||
return (result, account)
|
||||
|
||||
def get_account_from_username_prompt(self, username, prompt_option):
|
||||
account = None
|
||||
accounts = None
|
||||
|
||||
accounts = self.app.get_accounts()
|
||||
|
||||
if accounts:
|
||||
acc_id = 0
|
||||
if len(accounts) > 1:
|
||||
usernames = list(account['username'] for account in accounts)
|
||||
|
||||
# Find the last used username
|
||||
acc_id = 1
|
||||
try:
|
||||
acc_id = usernames.index(self.auth_settings.username)
|
||||
except ValueError:
|
||||
acc_id = 1
|
||||
usernames = list(account['username'] for account in accounts)
|
||||
|
||||
acc_id = prompt_choice_list('Please select an account:', usernames, acc_id)
|
||||
account = accounts[acc_id]
|
||||
result = self.app.acquire_token_silent(self.auth_settings.scopes, account=account)
|
||||
|
||||
return (result, account)
|
||||
|
||||
def authenticate_device_flow(self):
|
||||
"""
|
||||
Authenticate the end-user using device auth.
|
||||
"""
|
||||
(result, account) = self.authenticate_silent()
|
||||
|
||||
if not result:
|
||||
flow = self.app.initiate_device_flow(scopes=self.auth_settings.scopes)
|
||||
if "user_code" not in flow:
|
||||
raise CLIError("Fail to create device flow. Err: %s" % format_json(flow))
|
||||
|
||||
print(flow['message'])
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
acc_id = usernames.index(username)
|
||||
except ValueError:
|
||||
acc_id = len(usernames)
|
||||
|
||||
result = self.app.acquire_token_by_device_flow(flow)
|
||||
usernames.append(prompt_option)
|
||||
|
||||
if not account:
|
||||
accounts = self.app.get_accounts()
|
||||
if accounts:
|
||||
account = accounts[0]
|
||||
acc_id = acc_id + 1
|
||||
acc_id = prompt_choice_list(
|
||||
'Please select an account:',
|
||||
usernames,
|
||||
acc_id)
|
||||
|
||||
if acc_id < len(accounts):
|
||||
account = accounts[acc_id]
|
||||
|
||||
return account
|
||||
|
||||
def authenticate_silent_prompt(self):
|
||||
account = self.get_account_from_username_prompt(
|
||||
username=self.auth_settings.username,
|
||||
prompt_option='Add a new account')
|
||||
|
||||
result = self.app.acquire_token_silent_with_error(
|
||||
scopes=self.auth_settings.scopes,
|
||||
account=account,
|
||||
force_refresh=True)
|
||||
|
||||
return (result, account)
|
||||
|
||||
def auth_interactive(self):
|
||||
(result, account) = self.authenticate_silent()
|
||||
def auth_interactive(self, force_interactive=False):
|
||||
(result, account) = (None, None)
|
||||
|
||||
if not result:
|
||||
result = self.app.acquire_token_interactive(scopes=self.auth_settings.scopes)
|
||||
if not force_interactive:
|
||||
(result, account) = self.authenticate_silent_prompt()
|
||||
|
||||
if not account:
|
||||
accounts = self.app.get_accounts()
|
||||
if accounts:
|
||||
account = accounts[0]
|
||||
if not result or 'error' in result:
|
||||
if not account:
|
||||
account = self.get_account_from_username(self.auth_settings.username)
|
||||
|
||||
return (result, account)
|
||||
username = None
|
||||
if account and 'username' in account:
|
||||
username = account['username']
|
||||
|
||||
result = self.app.acquire_token_interactive(
|
||||
scopes=self.auth_settings.scopes,
|
||||
login_hint=username,
|
||||
prompt='select_account')
|
||||
|
||||
if result and not 'error' in result:
|
||||
if 'id_token' in result and 'preferred_username' in result['id_token_claims']:
|
||||
username = result['id_token_claims']['preferred_username']
|
||||
account = self.get_account_from_username(username)
|
||||
|
||||
return (result, account)
|
||||
|
||||
def logout(self):
|
||||
account = self.get_account_from_username_prompt(
|
||||
username=self.auth_settings.username,
|
||||
prompt_option='None')
|
||||
|
||||
if account:
|
||||
self.app.remove_account(account)
|
||||
|
||||
return account
|
|
@ -1,62 +0,0 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
Token file manager.
|
||||
"""
|
||||
|
||||
import os
|
||||
import msal
|
||||
from knack.util import CLIError
|
||||
from paconn.common.util import get_config_dir
|
||||
|
||||
TOKEN_FILE = 'accessTokens.json'
|
||||
|
||||
|
||||
class TokenManager:
|
||||
"""
|
||||
Class to manager login token.
|
||||
"""
|
||||
def __init__(self, token_file=TOKEN_FILE):
|
||||
self.token_file = os.path.join(get_config_dir(), token_file)
|
||||
|
||||
def get_credentials(self):
|
||||
"""
|
||||
Returns credential object from token file.
|
||||
"""
|
||||
credentials = self.read()
|
||||
token_expired = TokenManager.is_expired(credentials)
|
||||
if token_expired:
|
||||
raise CLIError('Access token invalid. Please login again.')
|
||||
|
||||
return credentials
|
||||
|
||||
def read(self):
|
||||
"""
|
||||
Reads a login token file.
|
||||
"""
|
||||
token_cache = msal.SerializableTokenCache()
|
||||
if os.path.isfile(self.token_file):
|
||||
try:
|
||||
with open(self.token_file, 'r') as cred_file:
|
||||
token_cache.deserialize(cred_file.read())
|
||||
except ValueError as exception:
|
||||
raise CLIError("Failed to load access token. (Inner Error: {})".format(exception))
|
||||
return token_cache
|
||||
|
||||
def write(self, token_cache):
|
||||
"""
|
||||
Writes the login credentials to a token file.
|
||||
"""
|
||||
try:
|
||||
with os.fdopen(os.open(self.token_file, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), 'w+') as cred_file:
|
||||
cred_file.write(token_cache.serialize())
|
||||
except Exception as exception:
|
||||
raise CLIError("Failed to save access token. (Inner Error: {})".format(exception))
|
||||
|
||||
def delete_token_file(self):
|
||||
if os.path.isfile(self.token_file):
|
||||
os.remove(self.token_file)
|
|
@ -10,9 +10,9 @@ Login command.
|
|||
from paconn.authentication.auth import get_authentication
|
||||
from paconn.common.util import display
|
||||
from paconn.settings.authsettings import AuthSettings
|
||||
from knack.util import CLIError
|
||||
|
||||
|
||||
def login(client_id, tenant, authority_url, scopes):
|
||||
def login(client_id, tenant, authority_url, scopes, force_interactive):
|
||||
"""
|
||||
Login command.
|
||||
"""
|
||||
|
@ -33,12 +33,16 @@ def login(client_id, tenant, authority_url, scopes):
|
|||
authority_url=authority_url,
|
||||
scopes=scopes)
|
||||
|
||||
(result, account) = get_authentication(settings=settings)
|
||||
(result, account) = get_authentication(
|
||||
settings=settings,
|
||||
force_interactive=force_interactive)
|
||||
|
||||
if result:
|
||||
if account:
|
||||
display('Logged in with account {}.'.format(account['username']))
|
||||
else:
|
||||
display('Login successful.')
|
||||
if result and "error" in result:
|
||||
raise CLIError(result["error_description"])
|
||||
elif not result:
|
||||
raise CLIError('Login failed.')
|
||||
|
||||
if account:
|
||||
display('Logged in with account {}.'.format(account['username']))
|
||||
else:
|
||||
display('Login failed.')
|
||||
display('Login successful.')
|
||||
|
|
|
@ -15,5 +15,6 @@ def logout():
|
|||
"""
|
||||
Logout command.
|
||||
"""
|
||||
remove_authentication()
|
||||
display('Logout successful.')
|
||||
account = remove_authentication()
|
||||
if account:
|
||||
display('Logged out from account {}.'.format(account['username']))
|
||||
|
|
|
@ -82,6 +82,15 @@ def load_arguments(self, command):
|
|||
type=str,
|
||||
required=False,
|
||||
help='Scopes for login.')
|
||||
arg_context.argument(
|
||||
'force_interactive',
|
||||
options_list=['--force_interactive', '-f'],
|
||||
type=bool,
|
||||
required=False,
|
||||
nargs='?',
|
||||
default=False,
|
||||
const=True,
|
||||
help='Force interactive login even if previous login exists.')
|
||||
|
||||
with ArgumentsContext(self, _DOWNLOAD) as arg_context:
|
||||
arg_context.argument(
|
||||
|
|
|
@ -14,6 +14,8 @@ from paconn.apimanager.flowrpbuilder import FlowRPBuilder
|
|||
from paconn.common.prompts import get_environment, get_connector_id
|
||||
from paconn.settings.settingsserializer import SettingsSerializer
|
||||
from paconn.authentication.auth import get_silent_authentication
|
||||
from paconn.common.util import display
|
||||
from knack.util import CLIError
|
||||
|
||||
# Setting file name
|
||||
SETTINGS_FILE = 'settings.json'
|
||||
|
@ -39,6 +41,14 @@ def load_powerapps_and_flow_rp(settings, command_context):
|
|||
# Get credentials
|
||||
(credentials, account) = get_silent_authentication()
|
||||
|
||||
if credentials and "error" in credentials:
|
||||
raise CLIError(credentials['error_description'])
|
||||
elif not credentials:
|
||||
raise CLIError('Couldn\'t obtain login token. Please log in again.')
|
||||
|
||||
if account and 'username' in account:
|
||||
display('Using account {}.'.format(account['username']))
|
||||
|
||||
# Get powerapps rp
|
||||
powerapps_rp = PowerAppsRPBuilder.get_from_settings(
|
||||
credentials=credentials,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import os
|
||||
from setuptools import setup
|
||||
|
||||
__VERSION__ = '0.1.1'
|
||||
__VERSION__ = '0.1.2'
|
||||
|
||||
|
||||
def read(fname):
|
||||
|
@ -57,7 +57,8 @@ setup(
|
|||
'pytest-xdist',
|
||||
'virtualenv',
|
||||
'requests',
|
||||
'adal',
|
||||
'msal',
|
||||
'msal_extensions',
|
||||
'msrestazure',
|
||||
'azure-storage-blob>=2.1,<12.0'
|
||||
],
|
||||
|
|
Загрузка…
Ссылка в новой задаче