Fixed permission denied error when deploying PostgreSQL in Azure using Docker. Fixes #7506
parent
35fb3bb38f
commit
659009c1de
|
@ -36,3 +36,4 @@ Bug fixes
|
|||
| `Issue #7461 <https://redmine.postgresql.org/issues/7461>`_ - Fixed an issue where the connection wasn't being closed when the user switched to a new connection and closed the query tool.
|
||||
| `Issue #7468 <https://redmine.postgresql.org/issues/7468>`_ - Skip the history records if the JSON info can't be parsed instead of showing 'No history'.
|
||||
| `Issue #7502 <https://redmine.postgresql.org/issues/7502>`_ - Fixed an issue where an error message is displayed when creating the new database.
|
||||
| `Issue #7506 <https://redmine.postgresql.org/issues/7506>`_ - Fixed permission denied error when deploying PostgreSQL in Azure using Docker.
|
||||
|
|
|
@ -690,6 +690,12 @@ KRB_AUTO_CREATE_USER = True
|
|||
|
||||
KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache')
|
||||
|
||||
#############################################################################
|
||||
# Create local directory to store azure credential cache
|
||||
#############################################################################
|
||||
|
||||
AZURE_CREDENTIAL_CACHE_DIR = os.path.join(DATA_DIR, 'azurecredentialcache')
|
||||
|
||||
##########################################################################
|
||||
# OAuth2 Configuration
|
||||
##########################################################################
|
||||
|
|
|
@ -14,13 +14,15 @@ from azure.mgmt.rdbms.postgresql_flexibleservers import \
|
|||
from azure.mgmt.rdbms.postgresql_flexibleservers.models import Sku, SkuTier, \
|
||||
CreateMode, Storage, Server, FirewallRule, HighAvailability
|
||||
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
|
||||
AuthenticationRecord, TokenCachePersistenceOptions
|
||||
AuthenticationRecord
|
||||
from azure.mgmt.resource import ResourceManagementClient
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from providers._abstract import AbsProvider
|
||||
import os
|
||||
from utils.io import debug, error, output
|
||||
from utils.misc import get_my_ip, get_random_id
|
||||
from pgadmin.misc.cloud.azure.azure_cache import load_persistent_cache, \
|
||||
TokenCachePersistenceOptions
|
||||
|
||||
|
||||
class AzureProvider(AbsProvider):
|
||||
|
@ -36,6 +38,7 @@ class AzureProvider(AbsProvider):
|
|||
self._credentials = None
|
||||
self._authentication_record_json = None
|
||||
self._cli_credentials = None
|
||||
self.azure_cred_cache_name = None
|
||||
|
||||
# Get the credentials
|
||||
if 'AUTHENTICATION_RECORD_JSON' in os.environ:
|
||||
|
@ -55,6 +58,9 @@ class AzureProvider(AbsProvider):
|
|||
if 'AZURE_DATABASE_PASSWORD' in os.environ:
|
||||
self._database_pass = os.environ['AZURE_DATABASE_PASSWORD']
|
||||
|
||||
if 'AZURE_CRED_CACHE_NAME' in os.environ:
|
||||
self.azure_cred_cache_name = os.environ['AZURE_CRED_CACHE_NAME']
|
||||
|
||||
def init_args(self, parsers):
|
||||
""" Create the command line parser for this provider """
|
||||
self.parser = parsers. \
|
||||
|
@ -162,16 +168,19 @@ class AzureProvider(AbsProvider):
|
|||
_credential = InteractiveBrowserCredential(
|
||||
tenant_id=self._tenant_id,
|
||||
timeout=180,
|
||||
cache_persistence_options=TokenCachePersistenceOptions(
|
||||
allow_unencrypted_storage=True
|
||||
),
|
||||
_cache=load_persistent_cache(
|
||||
TokenCachePersistenceOptions(
|
||||
name=self.azure_cred_cache_name,
|
||||
allow_unencrypted_storage=True)),
|
||||
authentication_record=deserialized_auth_record)
|
||||
else:
|
||||
_credential = InteractiveBrowserCredential(
|
||||
tenant_id=self._tenant_id,
|
||||
timeout=180,
|
||||
cache_persistence_options=TokenCachePersistenceOptions(
|
||||
allow_unencrypted_storage=True)
|
||||
_cache=load_persistent_cache(
|
||||
TokenCachePersistenceOptions(
|
||||
name=self.azure_cred_cache_name,
|
||||
allow_unencrypted_storage=True))
|
||||
)
|
||||
return _credential
|
||||
|
||||
|
@ -185,14 +194,10 @@ class AzureProvider(AbsProvider):
|
|||
if type in self._clients:
|
||||
return self._clients[type]
|
||||
|
||||
if type == 'postgresql':
|
||||
client = PostgreSQLManagementClient(self._credentials,
|
||||
self._subscription_id)
|
||||
elif type == 'resource':
|
||||
client = ResourceManagementClient(self._credentials,
|
||||
self._subscription_id)
|
||||
|
||||
self._clients[type] = client
|
||||
self._clients['postgresql'] = PostgreSQLManagementClient(
|
||||
self._credentials, self._subscription_id)
|
||||
self._clients['resource'] = ResourceManagementClient(
|
||||
self._credentials, self._subscription_id)
|
||||
|
||||
return self._clients[type]
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
# ##########################################################################
|
||||
|
||||
# Azure implementation
|
||||
import random
|
||||
|
||||
import config
|
||||
from pgadmin.misc.cloud.utils import _create_server, CloudProcessDesc
|
||||
from pgadmin.misc.bgprocess.processes import BatchProcess
|
||||
from pgadmin import make_json_response
|
||||
|
@ -15,12 +18,16 @@ from pgadmin.utils import PgAdminModule
|
|||
from flask_security import login_required
|
||||
import simplejson as json
|
||||
from flask import session, current_app, request
|
||||
from flask_login import current_user
|
||||
from config import root
|
||||
from .azure_cache import load_persistent_cache, TokenCachePersistenceOptions
|
||||
import os
|
||||
|
||||
|
||||
from azure.mgmt.rdbms.postgresql_flexibleservers import \
|
||||
PostgreSQLManagementClient
|
||||
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
|
||||
TokenCachePersistenceOptions, AuthenticationRecord
|
||||
from azure.identity import AzureCliCredential, InteractiveBrowserCredential,\
|
||||
AuthenticationRecord
|
||||
from azure.mgmt.resource import ResourceManagementClient
|
||||
from azure.mgmt.subscription import SubscriptionClient
|
||||
from azure.mgmt.rdbms.postgresql_flexibleservers.models import \
|
||||
|
@ -239,6 +246,8 @@ class Azure:
|
|||
self.subscription_id = None
|
||||
self._availability_zone = None
|
||||
self._available_capabilities_list = []
|
||||
self.cache_name = None
|
||||
self.cache_name = current_user.username + "_msal.cache"
|
||||
|
||||
##########################################################################
|
||||
# Azure Helper functions
|
||||
|
@ -250,6 +259,7 @@ class Azure:
|
|||
:return: True if valid credentials else false
|
||||
"""
|
||||
status, identity = self._get_azure_credentials()
|
||||
session['azure']['azure_cache_file_name'] = self.cache_name
|
||||
error = ''
|
||||
if not status:
|
||||
error = identity
|
||||
|
@ -288,16 +298,18 @@ class Azure:
|
|||
_credential = InteractiveBrowserCredential(
|
||||
tenant_id=self._tenant_id,
|
||||
timeout=180,
|
||||
cache_persistence_options=TokenCachePersistenceOptions(
|
||||
allow_unencrypted_storage=True
|
||||
),
|
||||
_cache=load_persistent_cache(
|
||||
TokenCachePersistenceOptions(
|
||||
name=self.cache_name,
|
||||
allow_unencrypted_storage=True)),
|
||||
authentication_record=deserialized_auth_record)
|
||||
else:
|
||||
_credential = InteractiveBrowserCredential(
|
||||
tenant_id=self._tenant_id,
|
||||
timeout=180,
|
||||
cache_persistence_options=TokenCachePersistenceOptions(
|
||||
allow_unencrypted_storage=True)
|
||||
_cache=load_persistent_cache(TokenCachePersistenceOptions(
|
||||
name=self.cache_name,
|
||||
allow_unencrypted_storage=True))
|
||||
)
|
||||
return _credential
|
||||
|
||||
|
@ -672,6 +684,7 @@ def deploy_on_azure(data):
|
|||
azure = session['azure']['azure_obj']
|
||||
env['AZURE_SUBSCRIPTION_ID'] = azure.subscription_id
|
||||
env['AUTH_TYPE'] = data['secret']['auth_type']
|
||||
env['AZURE_CRED_CACHE_NAME'] = azure.cache_name
|
||||
if azure.authentication_record_json is not None:
|
||||
env['AUTHENTICATION_RECORD_JSON'] = \
|
||||
azure.authentication_record_json
|
||||
|
@ -684,14 +697,20 @@ def deploy_on_azure(data):
|
|||
p.set_env_variables(None, env=env)
|
||||
p.update_server_id(p.id, sid)
|
||||
p.start()
|
||||
del session['azure']['azure_obj']
|
||||
return True, {'label': _label, 'sid': sid}
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return False, str(e)
|
||||
finally:
|
||||
del session['azure']['azure_obj']
|
||||
|
||||
|
||||
def clear_azure_session():
|
||||
"""Clear session data."""
|
||||
if 'azure' in session:
|
||||
file_name = session['azure']['azure_cache_file_name']
|
||||
file = config.AZURE_CREDENTIAL_CACHE_DIR + '/' + file_name
|
||||
# Delete cache file if exists
|
||||
if os.path.exists(file):
|
||||
os.remove(file)
|
||||
session.pop('azure')
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
# ------------------------------------
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
# ------------------------------------
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
import config
|
||||
|
||||
|
||||
import six
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import msal_extensions
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TokenCachePersistenceOptions(object):
|
||||
"""Options for persistent token caching.
|
||||
|
||||
Most credentials accept an instance of this class to configure persistent
|
||||
token caching. The default values configure a credential to use a cache
|
||||
shared with Microsoft developer tools and
|
||||
:class:`~azure.identity.SharedTokenCacheCredential`.
|
||||
To isolate a credential's data from other applications,
|
||||
specify a `name` for the cache.
|
||||
|
||||
By default, the cache is encrypted with the current platform's user data
|
||||
protection API, and will raise an error when this is not available.
|
||||
To configure the cache to fall back to an unencrypted file instead
|
||||
of raising an error, specify `allow_unencrypted_storage=True`.
|
||||
|
||||
.. warning:: The cache contains authentication secrets. If the cache is
|
||||
not encrypted, protecting it is the application's responsibility.
|
||||
A breach of its contents will fully compromise accounts.
|
||||
|
||||
.. literalinclude:: ../tests/test_persistent_cache.py
|
||||
:start-after: [START snippet]
|
||||
:end-before: [END snippet]
|
||||
:language: python
|
||||
:caption: Configuring a credential for persistent caching
|
||||
:dedent: 8
|
||||
|
||||
:keyword str name: name of the cache, used to isolate its data from other
|
||||
applications. Defaults to the name of the cache shared by Microsoft
|
||||
dev tools and :class:`~azure.identity.SharedTokenCacheCredential`.
|
||||
:keyword bool allow_unencrypted_storage: whether the cache should fall
|
||||
back to storing its data in plain text when
|
||||
encryption isn't possible. False by default. Setting this to
|
||||
True does not disable encryption. The cache will
|
||||
always try to encrypt its data.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# type: (**Any) -> None
|
||||
self.allow_unencrypted_storage = \
|
||||
kwargs.get("allow_unencrypted_storage", False)
|
||||
self.name = kwargs.get("name", "msal.cache")
|
||||
|
||||
|
||||
def load_persistent_cache(options):
|
||||
# type:
|
||||
# (TokenCachePersistenceOptions) -> msal_extensions.PersistedTokenCache
|
||||
import msal_extensions
|
||||
persistence = _get_persistence(
|
||||
allow_unencrypted=options.allow_unencrypted_storage,
|
||||
account_name="MSALCache",
|
||||
cache_name=options.name
|
||||
)
|
||||
return msal_extensions.PersistedTokenCache(persistence)
|
||||
|
||||
|
||||
def _get_persistence(allow_unencrypted, account_name, cache_name):
|
||||
# type: (bool, str, str) -> msal_extensions.persistence.BasePersistence
|
||||
"""Get an msal_extensions persistence instance for the current platform.
|
||||
|
||||
On Windows the cache is a file protected by the Data Protection API.
|
||||
On Linux and macOS the cache is stored by
|
||||
libsecret and Keychain, respectively. On those platforms the cache uses
|
||||
the modified timestamp of a file on disk to
|
||||
decide whether to reload the cache.
|
||||
|
||||
:param bool allow_unencrypted: when True, the cache will be kept in
|
||||
plaintext should encryption be impossible in the
|
||||
current environment
|
||||
"""
|
||||
import msal_extensions
|
||||
cache_location = \
|
||||
os.path.join(config.AZURE_CREDENTIAL_CACHE_DIR, cache_name)
|
||||
|
||||
if sys.platform.startswith("win") and "LOCALAPPDATA" in os.environ:
|
||||
return \
|
||||
msal_extensions.FilePersistenceWithDataProtection(cache_location)
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
# the cache uses this file's modified timestamp
|
||||
# to decide whether to reload
|
||||
return msal_extensions.KeychainPersistence(
|
||||
cache_location,
|
||||
"Microsoft.Developer.IdentityService",
|
||||
account_name)
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
# The cache uses this file's modified timestamp to decide whether
|
||||
# to reload. Note this path is the same as that of the plaintext
|
||||
# fallback: a new encrypted cache will stomp an unencrypted cache.
|
||||
|
||||
try:
|
||||
return msal_extensions.LibsecretPersistence(
|
||||
cache_location, cache_name,
|
||||
{"MsalClientID": "Microsoft.Developer.IdentityService"},
|
||||
label=account_name
|
||||
)
|
||||
except Exception as ex: # pylint:disable=broad-except
|
||||
_LOGGER.debug(
|
||||
'msal-extensions is unable to encrypt '
|
||||
'a persistent cache: "%s"', ex, exc_info=True)
|
||||
if not allow_unencrypted:
|
||||
error = ValueError(
|
||||
"Cache encryption is impossible because libsecret "
|
||||
"dependencies are not installed or are unusable,"
|
||||
" for example because no display is available "
|
||||
"(as in an SSH session). The chained exception has"
|
||||
' more information. Specify '
|
||||
'"allow_unencrypted_storage=True" to store'
|
||||
' the cache unencrypted'
|
||||
" instead of raising this exception."
|
||||
)
|
||||
six.raise_from(error, ex)
|
||||
return msal_extensions.FilePersistence(cache_location)
|
||||
|
||||
raise NotImplementedError("A persistent cache is not "
|
||||
"available in this environment.")
|
|
@ -125,7 +125,7 @@ define('pgadmin.misc.cloud', [
|
|||
hooks: {
|
||||
// Triggered when the dialog is closed
|
||||
onclose: function () {
|
||||
if(event.target instanceof Object){
|
||||
if(event.target instanceof Object && event.target.className == 'ajs-close'){
|
||||
const axiosApi = getApiInstance();
|
||||
let _url = url_for('cloud.clear_cloud_session');
|
||||
axiosApi.post(_url)
|
||||
|
|
|
@ -112,6 +112,22 @@ def create_app_data_directory(config):
|
|||
config.APP_VERSION))
|
||||
exit(1)
|
||||
|
||||
# Create Azure Credential Cache directory (if not present).
|
||||
try:
|
||||
_create_directory_if_not_exists(config.AZURE_CREDENTIAL_CACHE_DIR)
|
||||
except PermissionError as e:
|
||||
print(FAILED_CREATE_DIR.format(config.AZURE_CREDENTIAL_CACHE_DIR, e))
|
||||
print(
|
||||
"HINT : Create the directory {}, ensure it is writable by\n"
|
||||
"'{}', and try again, or, create a config_local.py file\n"
|
||||
" and override the AZURE_CREDENTIAL_CACHE_DIR setting per\n"
|
||||
" https://www.pgadmin.org/docs/pgadmin4/{}/config_py.html".
|
||||
format(
|
||||
config.AZURE_CREDENTIAL_CACHE_DIR,
|
||||
getpass.getuser(),
|
||||
config.APP_VERSION))
|
||||
exit(1)
|
||||
|
||||
# Create Kerberos Credential Cache directory (if not present).
|
||||
if config.SERVER_MODE and KERBEROS in config.AUTHENTICATION_SOURCES:
|
||||
try:
|
||||
|
|
Loading…
Reference in New Issue