mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-01-23 23:13:38 -06:00
Fixed permission denied error when deploying PostgreSQL in Azure using Docker. Fixes #7506
This commit is contained in:
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 #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 #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 #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')
|
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
|
# OAuth2 Configuration
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -14,13 +14,15 @@ from azure.mgmt.rdbms.postgresql_flexibleservers import \
|
|||||||
from azure.mgmt.rdbms.postgresql_flexibleservers.models import Sku, SkuTier, \
|
from azure.mgmt.rdbms.postgresql_flexibleservers.models import Sku, SkuTier, \
|
||||||
CreateMode, Storage, Server, FirewallRule, HighAvailability
|
CreateMode, Storage, Server, FirewallRule, HighAvailability
|
||||||
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
|
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
|
||||||
AuthenticationRecord, TokenCachePersistenceOptions
|
AuthenticationRecord
|
||||||
from azure.mgmt.resource import ResourceManagementClient
|
from azure.mgmt.resource import ResourceManagementClient
|
||||||
from azure.core.exceptions import ResourceNotFoundError
|
from azure.core.exceptions import ResourceNotFoundError
|
||||||
from providers._abstract import AbsProvider
|
from providers._abstract import AbsProvider
|
||||||
import os
|
import os
|
||||||
from utils.io import debug, error, output
|
from utils.io import debug, error, output
|
||||||
from utils.misc import get_my_ip, get_random_id
|
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):
|
class AzureProvider(AbsProvider):
|
||||||
@ -36,6 +38,7 @@ class AzureProvider(AbsProvider):
|
|||||||
self._credentials = None
|
self._credentials = None
|
||||||
self._authentication_record_json = None
|
self._authentication_record_json = None
|
||||||
self._cli_credentials = None
|
self._cli_credentials = None
|
||||||
|
self.azure_cred_cache_name = None
|
||||||
|
|
||||||
# Get the credentials
|
# Get the credentials
|
||||||
if 'AUTHENTICATION_RECORD_JSON' in os.environ:
|
if 'AUTHENTICATION_RECORD_JSON' in os.environ:
|
||||||
@ -55,6 +58,9 @@ class AzureProvider(AbsProvider):
|
|||||||
if 'AZURE_DATABASE_PASSWORD' in os.environ:
|
if 'AZURE_DATABASE_PASSWORD' in os.environ:
|
||||||
self._database_pass = os.environ['AZURE_DATABASE_PASSWORD']
|
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):
|
def init_args(self, parsers):
|
||||||
""" Create the command line parser for this provider """
|
""" Create the command line parser for this provider """
|
||||||
self.parser = parsers. \
|
self.parser = parsers. \
|
||||||
@ -162,16 +168,19 @@ class AzureProvider(AbsProvider):
|
|||||||
_credential = InteractiveBrowserCredential(
|
_credential = InteractiveBrowserCredential(
|
||||||
tenant_id=self._tenant_id,
|
tenant_id=self._tenant_id,
|
||||||
timeout=180,
|
timeout=180,
|
||||||
cache_persistence_options=TokenCachePersistenceOptions(
|
_cache=load_persistent_cache(
|
||||||
allow_unencrypted_storage=True
|
TokenCachePersistenceOptions(
|
||||||
),
|
name=self.azure_cred_cache_name,
|
||||||
|
allow_unencrypted_storage=True)),
|
||||||
authentication_record=deserialized_auth_record)
|
authentication_record=deserialized_auth_record)
|
||||||
else:
|
else:
|
||||||
_credential = InteractiveBrowserCredential(
|
_credential = InteractiveBrowserCredential(
|
||||||
tenant_id=self._tenant_id,
|
tenant_id=self._tenant_id,
|
||||||
timeout=180,
|
timeout=180,
|
||||||
cache_persistence_options=TokenCachePersistenceOptions(
|
_cache=load_persistent_cache(
|
||||||
allow_unencrypted_storage=True)
|
TokenCachePersistenceOptions(
|
||||||
|
name=self.azure_cred_cache_name,
|
||||||
|
allow_unencrypted_storage=True))
|
||||||
)
|
)
|
||||||
return _credential
|
return _credential
|
||||||
|
|
||||||
@ -185,14 +194,10 @@ class AzureProvider(AbsProvider):
|
|||||||
if type in self._clients:
|
if type in self._clients:
|
||||||
return self._clients[type]
|
return self._clients[type]
|
||||||
|
|
||||||
if type == 'postgresql':
|
self._clients['postgresql'] = PostgreSQLManagementClient(
|
||||||
client = PostgreSQLManagementClient(self._credentials,
|
self._credentials, self._subscription_id)
|
||||||
self._subscription_id)
|
self._clients['resource'] = ResourceManagementClient(
|
||||||
elif type == 'resource':
|
self._credentials, self._subscription_id)
|
||||||
client = ResourceManagementClient(self._credentials,
|
|
||||||
self._subscription_id)
|
|
||||||
|
|
||||||
self._clients[type] = client
|
|
||||||
|
|
||||||
return self._clients[type]
|
return self._clients[type]
|
||||||
|
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
# ##########################################################################
|
# ##########################################################################
|
||||||
|
|
||||||
# Azure implementation
|
# Azure implementation
|
||||||
|
import random
|
||||||
|
|
||||||
|
import config
|
||||||
from pgadmin.misc.cloud.utils import _create_server, CloudProcessDesc
|
from pgadmin.misc.cloud.utils import _create_server, CloudProcessDesc
|
||||||
from pgadmin.misc.bgprocess.processes import BatchProcess
|
from pgadmin.misc.bgprocess.processes import BatchProcess
|
||||||
from pgadmin import make_json_response
|
from pgadmin import make_json_response
|
||||||
@ -15,12 +18,16 @@ from pgadmin.utils import PgAdminModule
|
|||||||
from flask_security import login_required
|
from flask_security import login_required
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
from flask import session, current_app, request
|
from flask import session, current_app, request
|
||||||
|
from flask_login import current_user
|
||||||
from config import root
|
from config import root
|
||||||
|
from .azure_cache import load_persistent_cache, TokenCachePersistenceOptions
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
from azure.mgmt.rdbms.postgresql_flexibleservers import \
|
from azure.mgmt.rdbms.postgresql_flexibleservers import \
|
||||||
PostgreSQLManagementClient
|
PostgreSQLManagementClient
|
||||||
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
|
from azure.identity import AzureCliCredential, InteractiveBrowserCredential,\
|
||||||
TokenCachePersistenceOptions, AuthenticationRecord
|
AuthenticationRecord
|
||||||
from azure.mgmt.resource import ResourceManagementClient
|
from azure.mgmt.resource import ResourceManagementClient
|
||||||
from azure.mgmt.subscription import SubscriptionClient
|
from azure.mgmt.subscription import SubscriptionClient
|
||||||
from azure.mgmt.rdbms.postgresql_flexibleservers.models import \
|
from azure.mgmt.rdbms.postgresql_flexibleservers.models import \
|
||||||
@ -239,6 +246,8 @@ class Azure:
|
|||||||
self.subscription_id = None
|
self.subscription_id = None
|
||||||
self._availability_zone = None
|
self._availability_zone = None
|
||||||
self._available_capabilities_list = []
|
self._available_capabilities_list = []
|
||||||
|
self.cache_name = None
|
||||||
|
self.cache_name = current_user.username + "_msal.cache"
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# Azure Helper functions
|
# Azure Helper functions
|
||||||
@ -250,6 +259,7 @@ class Azure:
|
|||||||
:return: True if valid credentials else false
|
:return: True if valid credentials else false
|
||||||
"""
|
"""
|
||||||
status, identity = self._get_azure_credentials()
|
status, identity = self._get_azure_credentials()
|
||||||
|
session['azure']['azure_cache_file_name'] = self.cache_name
|
||||||
error = ''
|
error = ''
|
||||||
if not status:
|
if not status:
|
||||||
error = identity
|
error = identity
|
||||||
@ -288,16 +298,18 @@ class Azure:
|
|||||||
_credential = InteractiveBrowserCredential(
|
_credential = InteractiveBrowserCredential(
|
||||||
tenant_id=self._tenant_id,
|
tenant_id=self._tenant_id,
|
||||||
timeout=180,
|
timeout=180,
|
||||||
cache_persistence_options=TokenCachePersistenceOptions(
|
_cache=load_persistent_cache(
|
||||||
allow_unencrypted_storage=True
|
TokenCachePersistenceOptions(
|
||||||
),
|
name=self.cache_name,
|
||||||
|
allow_unencrypted_storage=True)),
|
||||||
authentication_record=deserialized_auth_record)
|
authentication_record=deserialized_auth_record)
|
||||||
else:
|
else:
|
||||||
_credential = InteractiveBrowserCredential(
|
_credential = InteractiveBrowserCredential(
|
||||||
tenant_id=self._tenant_id,
|
tenant_id=self._tenant_id,
|
||||||
timeout=180,
|
timeout=180,
|
||||||
cache_persistence_options=TokenCachePersistenceOptions(
|
_cache=load_persistent_cache(TokenCachePersistenceOptions(
|
||||||
allow_unencrypted_storage=True)
|
name=self.cache_name,
|
||||||
|
allow_unencrypted_storage=True))
|
||||||
)
|
)
|
||||||
return _credential
|
return _credential
|
||||||
|
|
||||||
@ -672,6 +684,7 @@ def deploy_on_azure(data):
|
|||||||
azure = session['azure']['azure_obj']
|
azure = session['azure']['azure_obj']
|
||||||
env['AZURE_SUBSCRIPTION_ID'] = azure.subscription_id
|
env['AZURE_SUBSCRIPTION_ID'] = azure.subscription_id
|
||||||
env['AUTH_TYPE'] = data['secret']['auth_type']
|
env['AUTH_TYPE'] = data['secret']['auth_type']
|
||||||
|
env['AZURE_CRED_CACHE_NAME'] = azure.cache_name
|
||||||
if azure.authentication_record_json is not None:
|
if azure.authentication_record_json is not None:
|
||||||
env['AUTHENTICATION_RECORD_JSON'] = \
|
env['AUTHENTICATION_RECORD_JSON'] = \
|
||||||
azure.authentication_record_json
|
azure.authentication_record_json
|
||||||
@ -684,14 +697,20 @@ def deploy_on_azure(data):
|
|||||||
p.set_env_variables(None, env=env)
|
p.set_env_variables(None, env=env)
|
||||||
p.update_server_id(p.id, sid)
|
p.update_server_id(p.id, sid)
|
||||||
p.start()
|
p.start()
|
||||||
del session['azure']['azure_obj']
|
|
||||||
return True, {'label': _label, 'sid': sid}
|
return True, {'label': _label, 'sid': sid}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
finally:
|
||||||
|
del session['azure']['azure_obj']
|
||||||
|
|
||||||
|
|
||||||
def clear_azure_session():
|
def clear_azure_session():
|
||||||
"""Clear session data."""
|
"""Clear session data."""
|
||||||
if 'azure' in session:
|
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')
|
session.pop('azure')
|
||||||
|
136
web/pgadmin/misc/cloud/azure/azure_cache.py
Normal file
136
web/pgadmin/misc/cloud/azure/azure_cache.py
Normal 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.")
|
@ -125,7 +125,7 @@ define('pgadmin.misc.cloud', [
|
|||||||
hooks: {
|
hooks: {
|
||||||
// Triggered when the dialog is closed
|
// Triggered when the dialog is closed
|
||||||
onclose: function () {
|
onclose: function () {
|
||||||
if(event.target instanceof Object){
|
if(event.target instanceof Object && event.target.className == 'ajs-close'){
|
||||||
const axiosApi = getApiInstance();
|
const axiosApi = getApiInstance();
|
||||||
let _url = url_for('cloud.clear_cloud_session');
|
let _url = url_for('cloud.clear_cloud_session');
|
||||||
axiosApi.post(_url)
|
axiosApi.post(_url)
|
||||||
|
@ -112,6 +112,22 @@ def create_app_data_directory(config):
|
|||||||
config.APP_VERSION))
|
config.APP_VERSION))
|
||||||
exit(1)
|
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).
|
# Create Kerberos Credential Cache directory (if not present).
|
||||||
if config.SERVER_MODE and KERBEROS in config.AUTHENTICATION_SOURCES:
|
if config.SERVER_MODE and KERBEROS in config.AUTHENTICATION_SOURCES:
|
||||||
try:
|
try:
|
||||||
|
Loading…
Reference in New Issue
Block a user