Skip to content

Commit cc6ea8b

Browse files
committed
Adding support for configuring External Authentication
- New menu entry "Configure External Authentication (httpd)" - Checks for pre-requisites - Validates the IPA Server is reachable - Uninstalls/Re-Installs and configures the IPA Client - Configures ipa/pam/sssd/httpd/SELinux - Restarts sssd and httpd
1 parent 3022f9b commit cc6ea8b

File tree

5 files changed

+348
-0
lines changed

5 files changed

+348
-0
lines changed

lib/appliance_console.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def agree_with_timeout(*args, &block)
153153
require 'appliance_console/database_configuration'
154154
require 'appliance_console/internal_database_configuration'
155155
require 'appliance_console/external_database_configuration'
156+
require 'appliance_console/external_httpd_authentication'
156157
require 'appliance_console/temp_storage_configuration'
157158
require 'appliance_console/env'
158159
require 'appliance_console/key_configuration'
@@ -359,6 +360,21 @@ module ApplianceConsole
359360
ca.remote(cahost, causer).run
360361
end
361362
press_any_key
363+
364+
when I18n.t("advanced_settings.httpdauth")
365+
say("#{selection}\n\n")
366+
367+
httpdauth = ExternalHttpdAuthentication.new(host)
368+
if httpdauth.activate
369+
httpdauth.post_activation
370+
say("\nExternal Authentication configured successfully.\n")
371+
press_any_key
372+
else
373+
say("\nExternal Authentication configuration failed!\n")
374+
press_any_key
375+
raise MiqSignalError
376+
end
377+
362378
when I18n.t("advanced_settings.evmstop")
363379
say("#{selection}\n\n")
364380
if agree("\nNote: It may take up to a few minutes for all EVM Server processes to exit gracefully. Perform an EVM stop? (Y/N): ")
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
require_relative "external_httpd_configuration"
2+
3+
module ApplianceConsole
4+
class ExternalHttpdAuthentication
5+
include ApplianceConsole::ExternalHttpdConfiguration
6+
7+
def initialize(host = nil)
8+
@ipaserver, @domain, @password = nil
9+
@domain = host.gsub(/^([^.]+\.)/, '') if host && host.include?('.')
10+
@principal = "admin"
11+
@timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
12+
end
13+
14+
def ask_for_parameters
15+
say("\nIPA Server Parameters:\n\n")
16+
@ipaserver = ask_for_hostname("IPA Server Hostname")
17+
@domain = ask_for_domain("IPA Server Domain", @domain)
18+
@principal = just_ask("IPA Server Principal", @principal)
19+
@password = ask_for_password("IPA Server Principal Password")
20+
21+
@ipaserver = "#{@ipaserver}.#{@domain}" unless @ipaserver.include?(".")
22+
end
23+
24+
def show_parameters
25+
say("\nExternal Authentication (httpd) Configuration:\n")
26+
say("IPA Server Details:\n")
27+
say(" Hostname: #{@ipaserver}\n")
28+
say(" Domain: #{@domain}\n")
29+
say(" Realm: #{realm}\n")
30+
say(" Naming Context: #{domain_naming_context}\n")
31+
say(" Principal: #{@principal}\n")
32+
end
33+
34+
def activate
35+
return false unless valid_environment?
36+
ask_for_parameters
37+
show_parameters
38+
return false unless agree("\nProceed? (Y/N): ")
39+
return false unless valid_parameters?(@ipaserver)
40+
41+
begin
42+
configure_ipa
43+
configure_pam
44+
configure_sssd
45+
configure_httpd_external_auth
46+
configure_httpd
47+
configure_selinux
48+
rescue => e
49+
say("Failed to Configure External Authentication - #{e}")
50+
return false
51+
end
52+
true
53+
end
54+
55+
def post_activation
56+
say("\nRestarting sssd and httpd ...")
57+
AwesomeSpawn.run!("#{SERVICE_COMMAND} sssd restart")
58+
AwesomeSpawn.run!("#{SERVICE_COMMAND} httpd restart")
59+
60+
say("Configuring sssd to start upon reboots ...")
61+
AwesomeSpawn.run!("#{CHKCONFIG_COMMAND} sssd on")
62+
end
63+
64+
private
65+
66+
def domain_naming_context
67+
@domain.split(".").collect { |s| "dc=#{s}" }.join(",")
68+
end
69+
70+
def realm
71+
@domain.upcase
72+
end
73+
74+
def configure_ipa
75+
say("\nConfiguring IPA (may take a minute) ...")
76+
ipa_client_unconfigure if ipa_client_configured?
77+
ipa_client_configure(realm, @domain, @ipaserver, @principal, @password)
78+
end
79+
80+
def configure_pam
81+
say("Configuring pam ...")
82+
config_file_write(PAM_CONFIGURATION, PAM_CONFIG, @timestamp)
83+
end
84+
85+
def configure_sssd
86+
say("Configuring sssd ...")
87+
config = config_file_read(SSSD_CONFIG)
88+
configure_sssd_domain(config, @domain)
89+
configure_sssd_service(config)
90+
configure_sssd_ifp(config)
91+
config_file_write(config, SSSD_CONFIG, @timestamp)
92+
end
93+
94+
def configure_httpd_external_auth
95+
say("Configuring httpd External Authentication ...")
96+
config_file_write(httpd_mod_intercept_config, HTTPD_EXTERNAL_AUTH, @timestamp)
97+
end
98+
99+
def configure_httpd
100+
say("Configuring httpd ...")
101+
config = config_file_read(HTTPD_CONFIG)
102+
configure_httpd_application(config)
103+
config_file_write(config, HTTPD_CONFIG, @timestamp)
104+
end
105+
106+
def configure_selinux
107+
say("Configuring SELinux ...")
108+
AwesomeSpawn.run!("#{SETSEBOOL_COMMAND} -P allow_httpd_mod_auth_pam on")
109+
result = AwesomeSpawn.run("#{GETSEBOOL_COMMAND} httpd_dbus_sssd")
110+
AwesomeSpawn.run!("#{SETSEBOOL_COMMAND} -P httpd_dbus_sssd on") if result.exit_status == 0
111+
end
112+
end
113+
end
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
module ApplianceConsole
2+
module ExternalHttpdConfiguration
3+
#
4+
# External Authentication Definitions
5+
#
6+
IPA_INSTALL_COMMAND = "/usr/sbin/ipa-client-install"
7+
8+
PAM_MODULE = "httpd-auth"
9+
PAM_CONFIG = "/etc/pam.d/#{PAM_MODULE}"
10+
PAM_CONFIGURATION = <<EOS
11+
auth required pam_sss.so
12+
account required pam_sss.so
13+
EOS
14+
SSSD_CONFIG = "/etc/sssd/sssd.conf"
15+
16+
EXTERNAL_AUTH_FILE = "conf.d/cfme-external-auth"
17+
HTTPD_EXTERNAL_AUTH = "/etc/httpd/#{EXTERNAL_AUTH_FILE}"
18+
HTTPD_CONFIG = "/etc/httpd/conf.d/cfme-https-application.conf"
19+
20+
RPM_COMMAND = "/bin/rpm"
21+
SERVICE_COMMAND = "/sbin/service"
22+
CHKCONFIG_COMMAND = "/sbin/chkconfig"
23+
24+
GETSEBOOL_COMMAND = "/usr/sbin/getsebool"
25+
SETSEBOOL_COMMAND = "/usr/sbin/setsebool"
26+
27+
INTERCEPT_FORM = "/dashboard/authenticate"
28+
29+
LDAP_ATTRS = {
30+
"mail" => "REMOTE_USER_EMAIL",
31+
"givenname" => "REMOTE_USER_FIRSTNAME",
32+
"sn" => "REMOTE_USER_LASTNAME",
33+
"displayname" => "REMOTE_USER_FULLNAME"
34+
}
35+
36+
#
37+
# IPA Configuration Methods
38+
#
39+
def ipa_client_configured?
40+
File.exist?(SSSD_CONFIG)
41+
end
42+
43+
def ipa_client_configure(realm, domain, server, principal, password)
44+
say("Configuring the IPA Client ...")
45+
AwesomeSpawn.run!(IPA_INSTALL_COMMAND,
46+
:params => {"-N" => nil,
47+
"--force-join" => nil,
48+
"--realm=" => realm,
49+
"--domain=" => domain,
50+
"--server=" => server,
51+
"--principal=" => principal,
52+
"--password=" => password,
53+
"--fixed-primary" => nil,
54+
"--unattended" => nil})
55+
end
56+
57+
def ipa_client_unconfigure
58+
say("Un-Configuring the IPA Client ...")
59+
AwesomeSpawn.run(IPA_INSTALL_COMMAND,
60+
:params => {"--uninstall" => nil,
61+
"--unattended" => nil})
62+
end
63+
64+
#
65+
# HTTPD Configuration Methods
66+
#
67+
def httpd_mod_intercept_config
68+
config = "
69+
LoadModule authnz_pam_module modules/mod_authnz_pam.so
70+
LoadModule intercept_form_submit_module modules/mod_intercept_form_submit.so
71+
LoadModule lookup_identity_module modules/mod_lookup_identity.so
72+
73+
<Location #{INTERCEPT_FORM}>
74+
InterceptFormPAMService #{PAM_MODULE}
75+
InterceptFormLogin user_name
76+
InterceptFormPassword user_password
77+
InterceptFormLoginSkip admin
78+
InterceptFormClearRemoteUserForSkipped on
79+
</Location>
80+
81+
<Location #{INTERCEPT_FORM}>
82+
"
83+
LDAP_ATTRS.each { |ldap, http| config << " LookupUserAttr #{ldap} #{http}\n" }
84+
config << "
85+
LookupUserGroups REMOTE_USER_GROUPS \":\"
86+
LookupDbusTimeout 5000
87+
</Location>
88+
"
89+
end
90+
91+
def httpd_external_auth_config
92+
config = "RequestHeader unset X_REMOTE_USER\n"
93+
attrs = %w(REMOTE_USER EXTERNAL_AUTH_ERROR) + LDAP_ATTRS.values + %w(REMOTE_USER_GROUPS)
94+
attrs.each { |attr| config << "RequestHeader set X_#{attr} %{#{attr}}e env=#{attr}\n" }
95+
config.chomp!
96+
end
97+
98+
def configure_httpd_application(config)
99+
ext_auth_include = "Include #{EXTERNAL_AUTH_FILE}"
100+
unless config.include?(ext_auth_include)
101+
config[/(\n)<VirtualHost/, 1] = "\n#{ext_auth_include}\n\n"
102+
end
103+
104+
if config.include?("set X_REMOTE_USER")
105+
config[/RequestHeader unset X_REMOTE_USER(\n.*)+env=REMOTE_USER_GROUPS/] = httpd_external_auth_config
106+
else
107+
config[/set X_FORWARDED_PROTO 'https'(\n)/, 1] = "\n\n#{httpd_external_auth_config}\n"
108+
end
109+
end
110+
111+
#
112+
# SSSD File Methods
113+
#
114+
def configure_sssd_domain(config, domain)
115+
ldap_user_extra_attrs = LDAP_ATTRS.keys.join(", ")
116+
if config.include?("ldap_user_extra_attrs = ")
117+
pattern = "[domain/#{domain}](\n.*)+ldap_user_extra_attrs = (.*)"
118+
config[/#{pattern}/, 2] = ldap_user_extra_attrs
119+
else
120+
pattern = "[domain/#{domain}].*(\n)"
121+
config[/#{pattern}/, 1] = "\nldap_user_extra_attrs = #{ldap_user_extra_attrs}\n"
122+
end
123+
end
124+
125+
def configure_sssd_service(config)
126+
services = config.match(/\[sssd\](\n.*)+services = (.*)/)[2]
127+
services = "#{services}, ifp" unless services.include?("ifp")
128+
config[/\[sssd\](\n.*)+services = (.*)/, 2] = services
129+
end
130+
131+
def configure_sssd_ifp(config)
132+
user_attributes = LDAP_ATTRS.keys.collect { |k| "+#{k}" }.join(", ")
133+
if config.include?("[ifp]")
134+
config[/\[ifp\](\n.*)+user_attributes = (.*)/, 2] = user_attributes
135+
else
136+
config << "\n[ifp]
137+
allowed_uids = apache, root
138+
user_attributes = #{user_attributes}\n"
139+
end
140+
end
141+
142+
#
143+
# RPM Utilities
144+
#
145+
def rpm_installed?(rpm_package)
146+
result = AwesomeSpawn.run!(RPM_COMMAND, :params => {"-qa" => rpm_package})
147+
if result.output.blank?
148+
say("#{rpm_package} RPM is not installed")
149+
return false
150+
end
151+
true
152+
end
153+
154+
#
155+
# Validation Methods
156+
#
157+
def installation_valid?
158+
rpm_packages = %w(ipa-client sssd-dbus mod_intercept_form_submit mod_authnz_pam mod_lookup_identity)
159+
missing = rpm_packages.count { |package| !rpm_installed?(package) }
160+
if missing > 0
161+
say("\nAppliance Installation is not valid for enabling External Authentication\n")
162+
return false
163+
end
164+
true
165+
end
166+
167+
def valid_environment?
168+
return false unless installation_valid?
169+
if ipa_client_configured?
170+
return false unless agree("\nIPA Client already configured on this Appliance, Un-Configure first? (Y/N): ")
171+
ipa_client_unconfigure
172+
return false unless agree("\nProceed with External Authentication Configuration? (Y/N): ")
173+
end
174+
true
175+
end
176+
177+
def valid_parameters?(ipaserver)
178+
host_reachable?(ipaserver, "IPA Server")
179+
end
180+
181+
#
182+
# Config File I/O Methods
183+
#
184+
def config_file_read(path)
185+
File.open(path, "r") { |f| f.read }
186+
end
187+
188+
def config_file_write(config, path, timestamp)
189+
FileUtils.copy(path, "#{path}.#{timestamp}") if File.exist?(path)
190+
File.open(path, "w") { |f| f.write(config) }
191+
end
192+
193+
#
194+
# Network validation
195+
#
196+
def host_reachable?(host, what = "Server")
197+
require 'net/ping'
198+
say("Checking connectivity to #{host} ... ")
199+
unless Net::Ping::External.new(host).ping
200+
say("Failed.\nCould not connect to #{host},")
201+
say("the #{what} must be reachable by name.")
202+
return false
203+
end
204+
say("Succeeded.")
205+
true
206+
end
207+
end
208+
end

lib/appliance_console/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ en:
88
- hostname
99
- datetime
1010
- dbrestore
11+
- httpdauth
1112
- evmstop
1213
- evmstart
1314
- restart
@@ -20,6 +21,7 @@ en:
2021
hostname: Set Hostname
2122
datetime: Set Timezone, Date, and Time
2223
dbrestore: Restore Database From Backup
24+
httpdauth: Configure External Authentication (httpd)
2325
evmstop: Stop EVM Server Processes
2426
evmstart: Start EVM Server Processes
2527
restart: Restart Appliance

lib/appliance_console/prompts.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module ApplianceConsole
22
module Prompts
33
CLEAR_CODE = `clear`
44
IP_REGEXP = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
5+
DOMAIN_REGEXP = /^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,13}$/
56
DATE_REGEXP = /^(2[0-9]{3})-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01])/
67
TIME_REGEXP = /^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/
78
INT_REGEXP = /^[0-9]+$/
@@ -49,6 +50,10 @@ def are_you_sure?(clarifier = nil)
4950
agree("Are you sure#{clarifier}? (Y/N): ")
5051
end
5152

53+
def ask_for_domain(prompt, default = nil, validate = DOMAIN_REGEXP, error_text = "a valid Domain.", &block)
54+
just_ask(prompt, default, validate, error_text, &block)
55+
end
56+
5257
def ask_for_ip(prompt, default, validate = IP_REGEXP, error_text = "a valid IP Address.", &block)
5358
just_ask(prompt, default, validate, error_text, &block)
5459
end
@@ -58,6 +63,10 @@ def ask_for_ip_or_none(prompt, default = nil)
5863
ask_for_ip(prompt, default, validation).gsub(/^'?NONE'?$/i, "")
5964
end
6065

66+
def ask_for_hostname(prompt, default = nil, validate = HOSTNAME_REGEXP, error_text = "a valid Hostname.", &block)
67+
just_ask(prompt, default, validate, error_text, &block)
68+
end
69+
6170
def ask_for_ip_or_hostname(prompt, default = nil)
6271
validation = ->(h) { (h =~ HOSTNAME_REGEXP || h =~ IP_REGEXP) && h.length > 0 }
6372
ask_for_ip(prompt, default, validation, "a valid Hostname or IP Address.")

0 commit comments

Comments
 (0)