Fixed permission denied error when deploying PostgreSQL in Azure using Docker. Fixes #7506

This commit is contained in:
Yogesh Mahajan 2022-06-27 19:36:20 +05:30 committed by Akshay Joshi
parent 35fb3bb38f
commit 659009c1de
7 changed files with 206 additions and 23 deletions

View File

@ -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.

View File

@ -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
##########################################################################

View File

@ -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]

View File

@ -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')

View File

@ -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.")

View File

@ -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)

View File

@ -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: