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.