You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by am...@apache.org on 2018/03/19 13:05:23 UTC
[ambari] branch trunk updated: AMBARI-23213. New CLI options for
setup-sso tool
This is an automated email from the ASF dual-hosted git repository.
amagyar pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git
The following commit(s) were added to refs/heads/trunk by this push:
new e1b9530 AMBARI-23213. New CLI options for setup-sso tool
e1b9530 is described below
commit e1b9530070c7fd1e40e9f933fb3b7e57818441e2
Author: Sandor Molnar <sm...@hortonworks.com>
AuthorDate: Wed Mar 14 23:26:19 2018 +0100
AMBARI-23213. New CLI options for setup-sso tool
---
ambari-server/src/main/python/ambari-server.py | 10 +-
.../src/main/python/ambari_server/setupSso.py | 152 ++++++-----
ambari-server/src/test/python/TestSetupSso.py | 283 +++++++++++++++++++++
3 files changed, 382 insertions(+), 63 deletions(-)
diff --git a/ambari-server/src/main/python/ambari-server.py b/ambari-server/src/main/python/ambari-server.py
index fe11bf4..1715387 100755
--- a/ambari-server/src/main/python/ambari-server.py
+++ b/ambari-server/src/main/python/ambari-server.py
@@ -572,6 +572,14 @@ def init_ldap_setup_parser_options(parser):
parser.add_option('--ambari-admin-password', default=None, help="Ambari Admin password for LDAP setup", dest="ambari_admin_password")
@OsFamilyFuncImpl(OsFamilyImpl.DEFAULT)
+def init_setup_sso_options(parser):
+ parser.add_option('--sso-enabled', default=None, help="Indicates whether to enable/disable SSO", dest="sso_enabled")
+ parser.add_option('--sso-provider-url', default=None, help="The URL of SSO provider; this must be provided when --sso-enabled is set to 'true'", dest="sso_provider_url")
+ parser.add_option('--sso-public-cert-file', default=None, help="The path where the public certificate PEM is located; this must be provided when --sso-enabled is set to 'true'", dest="sso_public_cert_file")
+ parser.add_option('--sso-jwt-cookie-name', default=None, help="The name of the JWT cookie", dest="sso_jwt_cookie_name")
+ parser.add_option('--sso-jwt-audience-list', default=None, help="A comma separated list of JWT audience(s)", dest="sso_jwt_audience_list")
+
+@OsFamilyFuncImpl(OsFamilyImpl.DEFAULT)
def init_pam_setup_parser_options(parser):
parser.add_option('--pam-config-file', default=None, help="Path to the PAM configuration file", dest="pam_config_file")
parser.add_option('--pam-auto-create-groups', default=None, help="Automatically create groups for authenticated users [true/false]", dest="pam_auto_create_groups")
@@ -846,7 +854,7 @@ def init_action_parser(action, parser):
UPDATE_HOST_NAMES_ACTION: init_empty_parser_options,
CHECK_DATABASE_ACTION: init_empty_parser_options,
ENABLE_STACK_ACTION: init_enable_stack_parser_options,
- SETUP_SSO_ACTION: init_empty_parser_options,
+ SETUP_SSO_ACTION: init_setup_sso_options,
DB_PURGE_ACTION: init_db_purge_parser_options,
INSTALL_MPACK_ACTION: init_install_mpack_parser_options,
UNINSTALL_MPACK_ACTION: init_uninstall_mpack_parser_options,
diff --git a/ambari-server/src/main/python/ambari_server/setupSso.py b/ambari-server/src/main/python/ambari_server/setupSso.py
index fade208..9827761 100644
--- a/ambari-server/src/main/python/ambari_server/setupSso.py
+++ b/ambari-server/src/main/python/ambari_server/setupSso.py
@@ -18,12 +18,13 @@ See the License for the specific language governing permissions and
limitations under the License.
'''
import logging
+import re
from ambari_commons.os_utils import is_root, run_os_command, copy_file, set_file_permissions, remove_file
from ambari_commons.exceptions import FatalException, NonFatalException
from ambari_commons.logging_utils import get_silent, print_warning_msg, print_error_msg
from ambari_server.userInput import get_validated_string_input, get_YN_input, get_multi_line_input
-
+from ambari_server.setupSecurity import REGEX_HOSTNAME_PORT, REGEX_TRUE_FALSE
from ambari_server.serverConfiguration import get_ambari_properties, get_value_from_properties, update_properties, \
store_password_file
@@ -47,77 +48,104 @@ JWT_PUBLIC_KEY_HEADER = "-----BEGIN CERTIFICATE-----\n"
JWT_PUBLIC_KEY_FOOTER = "\n-----END CERTIFICATE-----\n"
+def validateOptions(options):
+ errors = []
+ if options.sso_enabled and not re.search(REGEX_TRUE_FALSE, options.sso_enabled):
+ errors.append("--sso-enabled should be to either 'true' or 'false'")
+
+ if options.sso_enabled == 'true':
+ if not options.sso_provider_url:
+ errors.append("Missing option: --sso-provider-url")
+ if not options.sso_public_cert_file:
+ errors.append("Missing option: --sso-public-cert-file")
+ if options.sso_provider_url and not re.search(REGEX_HOSTNAME_PORT, options.sso_provider_url):
+ errors.append("Invalid --sso-provider-url")
+
+ if len(errors) > 0:
+ error_msg = "The following errors occurred while processing your request: {0}"
+ raise FatalException(1, error_msg.format(str(errors)))
+
+
+def populateSsoProviderUrl(options, properties):
+ if not options.sso_provider_url:
+ provider_url = get_value_from_properties(properties, JWT_AUTH_PROVIDER_URL, JWT_AUTH_PROVIDER_URL_DEFAULT)
+ provider_url = get_validated_string_input("Provider URL [URL] ({0}):".format(provider_url), provider_url, REGEX_HOSTNAME_PORT,
+ "Invalid provider URL", False)
+ else:
+ provider_url = options.sso_provider_url
+
+ properties.process_pair(JWT_AUTH_PROVIDER_URL, provider_url)
+
+
+def populateSsoPublicCert(options, properties):
+ if not options.sso_public_cert_file:
+ cert_path = properties.get_property(JWT_PUBLIC_KEY)
+ cert_string = get_multi_line_input("Public Certificate pem ({0})".format('stored' if cert_path else 'empty'))
+ store_new_cert = False
+ if cert_string is not None:
+ store_new_cert = True
+ if store_new_cert:
+ full_cert = JWT_PUBLIC_KEY_HEADER + cert_string + JWT_PUBLIC_KEY_FOOTER
+ cert_path = store_password_file(full_cert, JWT_PUBLIC_KEY_FILENAME)
+ else:
+ cert_path = options.sso_public_cert_file
+
+ properties.process_pair(JWT_PUBLIC_KEY, cert_path)
+
+
+def populateJwtCookieName(options, properties):
+ if not options.sso_jwt_cookie_name:
+ cookie_name = get_value_from_properties(properties, JWT_COOKIE_NAME, JWT_COOKIE_NAME_DEFAULT)
+ cookie_name = get_validated_string_input("JWT Cookie name ({0}):".format(cookie_name), cookie_name, REGEX_ANYTHING,
+ "Invalid cookie name", False)
+ else:
+ cookie_name = options.sso_jwt_cookie_name
-def setup_sso(args):
+ properties.process_pair(JWT_COOKIE_NAME, cookie_name)
+
+
+def populateJwtAudiences(options, properties):
+ if not options.sso_jwt_audience_list:
+ audiences = properties.get_property(JWT_AUDIENCES)
+ audiences = get_validated_string_input("JWT audiences list (comma-separated), empty for any ({0}):".format(audiences), audiences,
+ REGEX_ANYTHING, "Invalid value", False)
+ else:
+ audiences = options.sso_jwt_audience_list
+
+ properties.process_pair(JWT_AUDIENCES, audiences)
+
+
+def setup_sso(options):
logger.info("Setup SSO.")
if not is_root():
- err = 'ambari-server setup-sso should be run with ' \
- 'root-level privileges'
- raise FatalException(4, err)
+ raise FatalException(4, 'ambari-server setup-sso should be run with root-level privileges')
+
if not get_silent():
+ validateOptions(options)
+
properties = get_ambari_properties()
must_setup_params = False
- store_new_cert = False
-
- sso_enabled = properties.get_property(JWT_AUTH_ENBABLED).lower() in ['true']
-
- if sso_enabled:
- if get_YN_input("Do you want to disable SSO authentication [y/n] (n)?", False):
- properties.process_pair(JWT_AUTH_ENBABLED, "false")
- else:
- if get_YN_input("Do you want to configure SSO authentication [y/n] (y)?", True):
- properties.process_pair(JWT_AUTH_ENBABLED, "true")
- must_setup_params = True
+ if not options.sso_enabled:
+ sso_enabled = properties.get_property(JWT_AUTH_ENBABLED).lower() in ['true']
+ if sso_enabled:
+ if get_YN_input("Do you want to disable SSO authentication [y/n] (n)?", False):
+ properties.process_pair(JWT_AUTH_ENBABLED, "false")
else:
- return False
+ if get_YN_input("Do you want to configure SSO authentication [y/n] (y)?", True):
+ properties.process_pair(JWT_AUTH_ENBABLED, "true")
+ must_setup_params = True
+ else:
+ return False
+ else:
+ properties.process_pair(JWT_AUTH_ENBABLED, options.sso_enabled)
+ must_setup_params = options.sso_enabled == 'true'
if must_setup_params:
-
- provider_url = get_value_from_properties(properties, JWT_AUTH_PROVIDER_URL, JWT_AUTH_PROVIDER_URL_DEFAULT)
- provider_url = get_validated_string_input("Provider URL [URL] ({0}):".format(provider_url),
- provider_url,
- REGEX_ANYTHING,
- "Invalid provider URL",
- False)
- properties.process_pair(JWT_AUTH_PROVIDER_URL, provider_url)
-
- cert_path = properties.get_property(JWT_PUBLIC_KEY)
- cert_string = get_multi_line_input("Public Certificate pem ({0})".format('stored' if cert_path else 'empty'))
- if cert_string is not None:
- store_new_cert = True
-
- if get_YN_input("Do you want to configure advanced properties [y/n] (n) ?", False):
- cookie_name = get_value_from_properties(properties, JWT_COOKIE_NAME, JWT_COOKIE_NAME_DEFAULT)
- cookie_name = get_validated_string_input("JWT Cookie name ({0}):".format(cookie_name),
- cookie_name,
- REGEX_ANYTHING,
- "Invalid cookie name",
- False)
- properties.process_pair(JWT_COOKIE_NAME, cookie_name)
-
- audiences = properties.get_property(JWT_AUDIENCES)
- audiences = get_validated_string_input("JWT audiences list (comma-separated), empty for any ({0}):".format(audiences),
- audiences,
- REGEX_ANYTHING,
- "Invalid value",
- False)
- properties.process_pair(JWT_AUDIENCES, audiences)
-
- # TODO not required for now as we support Knox only
- # orig_query_param = get_value_from_properties(JWT_ORIGINAL_URL_QUERY_PARAM, JWT_ORIGINAL_URL_QUERY_PARAM_DEFAULT)
- # orig_query_param = get_validated_string_input("Original URL query parameter name ({}):".format(orig_query_param),
- # orig_query_param,
- # REGEX_ANYTHING,
- # "Invalid value",
- # False)
- # properties.process_pair(JWT_ORIGINAL_URL_QUERY_PARAM, orig_query_param)
-
- if store_new_cert:
- full_cert = JWT_PUBLIC_KEY_HEADER + cert_string + JWT_PUBLIC_KEY_FOOTER
- cert_path = store_password_file(full_cert, JWT_PUBLIC_KEY_FILENAME)
-
- properties.process_pair(JWT_PUBLIC_KEY, cert_path)
+ populateSsoProviderUrl(options, properties)
+ populateSsoPublicCert(options, properties)
+ populateJwtCookieName(options, properties)
+ populateJwtAudiences(options, properties)
update_properties(properties)
diff --git a/ambari-server/src/test/python/TestSetupSso.py b/ambari-server/src/test/python/TestSetupSso.py
new file mode 100644
index 0000000..af1ac6f
--- /dev/null
+++ b/ambari-server/src/test/python/TestSetupSso.py
@@ -0,0 +1,283 @@
+'''
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+'''
+import os
+import platform
+import sys
+import unittest
+import StringIO
+
+from mock.mock import patch, MagicMock
+
+from only_for_platform import os_distro_value
+from ambari_commons import os_utils
+
+import shutil
+project_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),os.path.normpath("../../../../"))
+shutil.copyfile(project_dir+"/ambari-server/conf/unix/ambari.properties", "/tmp/ambari.properties")
+
+# We have to use this import HACK because the filename contains a dash
+_search_file = os_utils.search_file
+
+def search_file_proxy(filename, searchpatch, pathsep=os.pathsep):
+ global _search_file
+ if "ambari.properties" in filename:
+ return "/tmp/ambari.properties"
+ return _search_file(filename, searchpatch, pathsep)
+
+os_utils.search_file = search_file_proxy
+
+with patch.object(platform, "linux_distribution", return_value = MagicMock(return_value=('Redhat', '6.4', 'Final'))):
+ with patch("os.path.isdir", return_value = MagicMock(return_value=True)):
+ with patch("os.access", return_value = MagicMock(return_value=True)):
+ with patch.object(os_utils, "parse_log4j_file", return_value={'ambari.log.dir': '/var/log/ambari-server'}):
+ with patch("platform.linux_distribution", return_value = os_distro_value):
+ with patch("os.symlink"):
+ with patch("glob.glob", return_value = ['/etc/init.d/postgresql-9.3']):
+ _ambari_server_ = __import__('ambari-server')
+ with patch("__builtin__.open"):
+ from ambari_commons.exceptions import FatalException, NonFatalException
+ from ambari_server.properties import Properties
+ from ambari_server.setupSso import setup_sso, JWT_AUTH_ENBABLED, JWT_AUTH_PROVIDER_URL, JWT_PUBLIC_KEY, JWT_COOKIE_NAME, JWT_AUDIENCES
+
+class TestSetupSso(unittest.TestCase):
+
+ @patch("ambari_server.setupSso.is_root")
+ def test_non_root_user_should_not_be_able_to_setup_sso(self, is_root_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = False
+ options = self._create_empty_options_mock()
+
+ try:
+ setup_sso(options)
+ self.fail("Should fail with non-fatal exception")
+ except FatalException as e:
+ self.assertTrue("ambari-server setup-sso should be run with root-level privileges" in e.reason)
+ pass
+
+ sys.stdout = sys.__stdout__
+ pass
+
+
+
+ @patch("ambari_server.setupSso.get_silent")
+ @patch("ambari_server.setupSso.is_root")
+ def test_silent_mode_is_not_allowed(self, is_root_mock, get_silent_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = True
+ get_silent_mock.return_value = True
+ options = self._create_empty_options_mock()
+
+ try:
+ setup_sso(options)
+ self.fail("Should fail with fatal exception")
+ except NonFatalException as e:
+ self.assertTrue("setup-sso is not enabled in silent mode." in e.reason)
+ pass
+
+ sys.stdout = sys.__stdout__
+ pass
+
+
+
+ @patch("ambari_server.setupSso.get_silent")
+ @patch("ambari_server.setupSso.is_root")
+ def test_invalid_sso_enabled_cli_option_should_result_in_error(self, is_root_mock, get_silent_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = True
+ get_silent_mock.return_value = False
+ options = self._create_empty_options_mock()
+ options.sso_enabled = 'not_true_or_false'
+
+ try:
+ setup_sso(options)
+ self.fail("Should fail with fatal exception")
+ except FatalException as e:
+ self.assertTrue("--sso-enabled should be to either 'true' or 'false'" in e.reason)
+ pass
+
+ sys.stdout = sys.__stdout__
+ pass
+
+
+
+ @patch("ambari_server.setupSso.get_silent")
+ @patch("ambari_server.setupSso.is_root")
+ def test_missing_sso_provider_url_cli_option_when_enabling_sso_should_result_in_error(self, is_root_mock, get_silent_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = True
+ get_silent_mock.return_value = False
+ options = self._create_empty_options_mock()
+ options.sso_enabled = 'true'
+ options.sso_provider_url = ''
+
+ try:
+ setup_sso(options)
+ self.fail("Should fail with fatal exception")
+ except FatalException as e:
+ self.assertTrue("Missing option: --sso-provider-url" in e.reason)
+ pass
+
+ sys.stdout = sys.__stdout__
+ pass
+
+
+
+ @patch("ambari_server.setupSso.get_silent")
+ @patch("ambari_server.setupSso.is_root")
+ def test_missing_sso_public_cert_file_cli_option_when_enabling_sso_should_result_in_error(self, is_root_mock, get_silent_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = True
+ get_silent_mock.return_value = False
+ options = self._create_empty_options_mock()
+ options.sso_enabled = 'true'
+ options.sso_public_cert_file = ''
+
+ try:
+ setup_sso(options)
+ self.fail("Should fail with fatal exception")
+ except FatalException as e:
+ self.assertTrue("Missing option: --sso-public-cert-file" in e.reason)
+ pass
+
+ sys.stdout = sys.__stdout__
+ pass
+
+
+
+ @patch("ambari_server.setupSso.get_silent")
+ @patch("ambari_server.setupSso.is_root")
+ def test_invalid_sso_provider_url_cli_option_when_enabling_sso_should_result_in_error(self, is_root_mock, get_silent_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = True
+ get_silent_mock.return_value = False
+ options = self._create_empty_options_mock()
+ options.sso_enabled = 'true'
+ options.sso_provider_url = '!invalidHost:invalidPort'
+
+ try:
+ setup_sso(options)
+ self.fail("Should fail with fatal exception")
+ except FatalException as e:
+ self.assertTrue("Invalid --sso-provider-url" in e.reason)
+ pass
+
+ sys.stdout = sys.__stdout__
+ pass
+
+
+
+ @patch("ambari_server.setupSso.update_properties")
+ @patch("ambari_server.setupSso.get_ambari_properties")
+ @patch("ambari_server.setupSso.get_silent")
+ @patch("ambari_server.setupSso.is_root")
+ def test_all_cli_options_are_collected_when_enabling_sso(self, is_root_mock, get_silent_mock, get_ambari_properties_mock, update_properties_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = True
+ get_silent_mock.return_value = False
+
+ properties = Properties();
+ get_ambari_properties_mock.return_value = properties
+
+ sso_enabled = 'true'
+ sso_provider_url = 'http://testHost:8080'
+ sso_public_cert_file = '/test/file/path'
+ sso_jwt_cookie_name = 'test_cookie'
+ sso_jwt_audience_list = 'test, audience, list'
+ options = self._create_empty_options_mock()
+ options.sso_enabled = sso_enabled
+ options.sso_provider_url = sso_provider_url
+ options.sso_public_cert_file = sso_public_cert_file
+ options.sso_jwt_cookie_name = sso_jwt_cookie_name
+ options.sso_jwt_audience_list = sso_jwt_audience_list
+
+ setup_sso(options)
+
+ self.assertTrue(update_properties_mock.called)
+ self.assertEqual(properties.get_property(JWT_AUTH_ENBABLED), sso_enabled)
+ self.assertEqual(properties.get_property(JWT_AUTH_PROVIDER_URL), sso_provider_url)
+ self.assertEqual(properties.get_property(JWT_PUBLIC_KEY), sso_public_cert_file)
+ self.assertEqual(properties.get_property(JWT_COOKIE_NAME), sso_jwt_cookie_name)
+ self.assertEqual(properties.get_property(JWT_AUDIENCES), sso_jwt_audience_list)
+
+ sys.stdout = sys.__stdout__
+ pass
+
+
+
+ @patch("ambari_server.setupSso.update_properties")
+ @patch("ambari_server.setupSso.get_ambari_properties")
+ @patch("ambari_server.setupSso.get_silent")
+ @patch("ambari_server.setupSso.is_root")
+ def test_only_sso_enabled_cli_option_is_collected_when_disabling_sso(self, is_root_mock, get_silent_mock, get_ambari_properties_mock, update_properties_mock):
+ out = StringIO.StringIO()
+ sys.stdout = out
+
+ is_root_mock.return_value = True
+ get_silent_mock.return_value = False
+
+ properties = Properties();
+ get_ambari_properties_mock.return_value = properties
+
+ sso_enabled = 'false'
+ sso_provider_url = 'http://testHost:8080'
+ sso_public_cert_file = '/test/file/path'
+ sso_jwt_cookie_name = 'test_cookie'
+ sso_jwt_audience_list = 'test, audience, list'
+ options = self._create_empty_options_mock()
+ options.sso_enabled = sso_enabled
+ options.sso_provider_url = sso_provider_url
+ options.sso_public_cert_file = sso_public_cert_file
+ options.sso_jwt_cookie_name = sso_jwt_cookie_name
+ options.sso_jwt_audience_list = sso_jwt_audience_list
+
+ setup_sso(options)
+
+ self.assertTrue(update_properties_mock.called)
+ self.assertEqual(properties.get_property(JWT_AUTH_ENBABLED), sso_enabled)
+ self.assertTrue(JWT_AUTH_PROVIDER_URL not in properties.propertyNames())
+ self.assertTrue(JWT_PUBLIC_KEY not in properties.propertyNames())
+ self.assertTrue(JWT_COOKIE_NAME not in properties.propertyNames())
+ self.assertTrue(JWT_AUDIENCES not in properties.propertyNames())
+
+
+ sys.stdout = sys.__stdout__
+ pass
+
+ def _create_empty_options_mock(self):
+ options = MagicMock()
+ options.sso_enabled = None
+ options.sso_provider_url = None
+ options.sso_public_cert_file = None
+ options.sso_jwt_cookie_name = None
+ options.sso__jwt_audience_list = None
+ return options
+
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
amagyar@apache.org.