Skip to content

Commit 0d080dc

Browse files
authored
Merge pull request #2188 from guangyee/oidc_websso
support OpenID Connect WebSSO (SOC-9616)
2 parents 1e127bf + 541bf39 commit 0d080dc

File tree

12 files changed

+321
-7
lines changed

12 files changed

+321
-7
lines changed

chef/cookbooks/crowbar-openstack/providers/wsgi.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ def whyrun_supported?
6767
ssl_keyfile: current_resource.ssl_keyfile,
6868
ssl_cacert: current_resource.ssl_cacert,
6969
timeout: current_resource.timeout,
70+
openidc_enabled: current_resource.openidc_enabled,
71+
openidc_provider: current_resource.openidc_provider,
72+
openidc_response_type: current_resource.openidc_response_type,
73+
openidc_scope: current_resource.openidc_scope,
74+
openidc_metadata_url: current_resource.openidc_metadata_url,
75+
openidc_client_id: current_resource.openidc_client_id,
76+
openidc_client_secret: current_resource.openidc_client_secret,
77+
openidc_passphrase: current_resource.openidc_passphrase,
78+
openidc_redirect_uri: current_resource.openidc_redirect_uri,
7079
access_log: current_resource.access_log,
7180
error_log: current_resource.error_log,
7281
apache_log_dir: node[:apache][:log_dir],
@@ -116,6 +125,16 @@ def load_current_resource
116125

117126
@current_resource.timeout(@new_resource.timeout)
118127

128+
@current_resource.openidc_enabled(@new_resource.openidc_enabled)
129+
@current_resource.openidc_provider(@new_resource.openidc_provider)
130+
@current_resource.openidc_response_type(@new_resource.openidc_response_type)
131+
@current_resource.openidc_scope(@new_resource.openidc_scope)
132+
@current_resource.openidc_metadata_url(@new_resource.openidc_metadata_url)
133+
@current_resource.openidc_client_id(@new_resource.openidc_client_id)
134+
@current_resource.openidc_client_secret(@new_resource.openidc_client_secret)
135+
@current_resource.openidc_passphrase(@new_resource.openidc_passphrase)
136+
@current_resource.openidc_redirect_uri(@new_resource.openidc_redirect_uri)
137+
119138
@current_resource.access_log(_get_access_log)
120139
@current_resource.error_log(_get_error_log)
121140

chef/cookbooks/crowbar-openstack/resources/wsgi.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@
2222

2323
attribute :timeout, kind_of: Integer, default: nil
2424

25+
attribute :openidc_enabled, kind_of: [TrueClass, FalseClass], default: false
26+
attribute :openidc_provider, kind_of: String, default: nil
27+
attribute :openidc_response_type, kind_of: String, default: nil
28+
attribute :openidc_scope, kind_of: String, default: nil
29+
attribute :openidc_metadata_url, kind_of: String, default: nil
30+
attribute :openidc_client_id, kind_of: String, default: nil
31+
attribute :openidc_client_secret, kind_of: String, default: nil
32+
attribute :openidc_passphrase, kind_of: String, default: nil
33+
attribute :openidc_redirect_uri, kind_of: String, default: nil
34+
2535
attribute :access_log, kind_of: String, default: nil
2636
attribute :error_log, kind_of: String, default: nil
2737

chef/cookbooks/crowbar-openstack/templates/default/vhost-wsgi.conf.erb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,34 @@ Listen <%= @bind_host %>:<%= @bind_port %>
2626
<% end -%>
2727
<% end %>
2828

29+
<% if @openidc_enabled %>
30+
# See
31+
# https://github.com/zmartzone/mod_auth_openidc/blob/master/auth_openidc.conf
32+
OIDCClaimPrefix "OIDC-"
33+
OIDCResponseType "<%= @openidc_response_type %>"
34+
OIDCScope "<%= @openidc_scope %>"
35+
OIDCProviderMetadataURL "<%= @openidc_metadata_url %>"
36+
OIDCClientID "<%= @openidc_client_id %>"
37+
OIDCClientSecret "<%= @openidc_client_secret %>"
38+
OIDCCryptoPassphrase "<%= @openidc_passphrase %>"
39+
OIDCRedirectURI "<%= @openidc_redirect_uri %>"
40+
41+
<Location /v3/OS-FEDERATION/identity_providers/<%= @openidc_provider %>/protocols/openid/auth>
42+
Require valid-user
43+
AuthType openid-connect
44+
</Location>
45+
46+
<Location /v3/auth/OS-FEDERATION/websso/openid>
47+
Require valid-user
48+
AuthType openid-connect
49+
</Location>
50+
51+
<Location /v3/auth/OS-FEDERATION/identity_providers/<%= @openidc_provider %>/protocols/openid/websso>
52+
Require valid-user
53+
AuthType openid-connect
54+
</Location>
55+
<% end %>
56+
2957
ErrorLogFormat "%{cu}t %M"
3058
ErrorLog <%= @apache_log_dir %>/<%= @error_log %>
3159
CustomLog <%= @apache_log_dir %>/<%= @access_log %> combined

chef/cookbooks/horizon/recipes/server.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@
447447
multi_domain_support: multi_domain_support,
448448
policy_file_path: node["horizon"]["policy_file_path"],
449449
policy_file: node["horizon"]["policy_file"],
450+
websso_keystone_url: keystone_settings["websso_keystone_url"]
450451
)
451452
action :create
452453
notifies :reload, "service[horizon]"

chef/cookbooks/horizon/templates/default/local_settings.py.erb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,18 @@ EXTERNAL_MONITORING = [
250250

251251
SESSION_TIMEOUT = <%= @session_timeout * 60 %>
252252
SECRET_KEY = "<%= @secret_key %>"
253+
254+
# FIXME(gyee): if we ever going to support multiple protocols or multiple
255+
# IDPs, we'll need to refactor this accordingly.
256+
<% if node[:keystone][:federation][:openidc][:enabled] %>
257+
WEBSSO_ENABLED = True
258+
259+
# NOTE(gyee): this need to be versioned public Keystone URL as this is used
260+
# to redirect the browsers to Keystone.
261+
WEBSSO_KEYSTONE_URL = "<%= @websso_keystone_url %>"
262+
263+
WEBSSO_CHOICES = (
264+
("credentials", _("Keystone Credentials")),
265+
("openid", _("OpenID Connect")),
266+
)
267+
<% end %>

chef/cookbooks/keystone/libraries/helpers.rb

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,101 @@ def self.unversioned_internal_auth_url(node, admin_host)
3232
service_URL(node[:keystone][:api][:protocol], admin_host, node[:keystone][:api][:service_port])
3333
end
3434

35+
# NOTE(gyee): trusted_dashboard in Keystone can be multiple URLs.
36+
# For example, in a typical production deployment, there can be multiple
37+
# Horizon endpoints, depending on where Horizon is being accessed. For
38+
# example, the endpoint for access inside the firewall could be different
39+
# from the one that is outside of the firewall. And in some cases, the
40+
# endpoint could be the corporate HTTP proxy.
41+
# For now, we are prepopulating one that is only understood by Crowbar for
42+
# testing/demo purpopses. In a production environment, it can be amended with
43+
# other external endpoints.
44+
def self.dashboard_public_url(dashboard_node)
45+
ha_enabled = dashboard_node[:horizon][:ha][:enabled]
46+
ssl_enabled = dashboard_node[:horizon][:apache][:ssl]
47+
want_fqdn = true
48+
public_fqdn = CrowbarHelper.get_host_for_public_url(
49+
dashboard_node,
50+
ssl_enabled,
51+
ha_enabled,
52+
want_fqdn
53+
)
54+
55+
protocol = "http"
56+
protocol = "https" if ssl_enabled
57+
58+
"#{protocol}://#{public_fqdn}"
59+
end
60+
61+
def self.websso_enabled(node)
62+
node[:keystone][:federation][:openidc][:enabled]
63+
end
64+
65+
def self.trusted_dashboard_url(node)
66+
# NOTE(gyee): since Horizon is depended on Keystone and will be deployed
67+
# after Keystone, the Horizon node may not be available when Keystone
68+
# is first deployed. However, chef executes the recipes periodically.
69+
# On the next chef run after, after Horizon had deployed, we should be
70+
# able to figure out the trusted_dashboard from the node, assuming Horizon
71+
# is always deployed on the same node as Keystone. Otherwise, we'll need
72+
# to do node search.
73+
74+
horizon_server = CrowbarUtilsSearch.node_search_with_cache(node, "roles:horizon-server").first
75+
76+
unless horizon_server.nil?
77+
horizon_url =
78+
if horizon_server["horizon"].key?("apache")
79+
::File.join(dashboard_public_url(horizon_server), "/auth/websso/")
80+
else
81+
""
82+
end
83+
end
84+
85+
horizon_url
86+
end
87+
88+
def self.trusted_dashboards(node)
89+
# NOTE(gyee): if user does not specify any trusted_dashboards, we'll
90+
# automagically generate one based on the current knowned crowbar
91+
# configuration.
92+
if node[:keystone][:federation][:trusted_dashboards].empty?
93+
node[:keystone][:federation][:trusted_dashboards] << trusted_dashboard_url(node)
94+
end
95+
96+
node[:keystone][:federation][:trusted_dashboards]
97+
end
98+
99+
# NOTE(gyee): for some WebSSO protocols (i.e. saml, openidc, etc), the
100+
# authentication process involves redirecting the browser to the identity
101+
# provider's endpoint for authentication, then the user is redirected back
102+
# to Keystone upon successfully authentication with the identity provider.
103+
# Therefore, the Keystone redirect URL must be external or public as it needs
104+
# to be accessible by the browsers. Also, in some cases, the URL needs to be
105+
# fully qualified. For example, Google OpenID Connection requires the URL to
106+
# be FQDN instead of IP as it must match the authorized domain. Therefore,
107+
# the WEBSSO_KEYSTONE_URL in Horizon should contain the FQDN, depending
108+
# on the identity provider. For production deployments, we offers the user
109+
# the ability to override it because we don't know the user's network
110+
# topology beforehand. For example, the external endpoint could be handled
111+
# by an HTTP proxy which may not be known to crowbar.
112+
def self.websso_keystone_url(node)
113+
ha_enabled = node[:keystone][:ha][:enabled]
114+
ssl_enabled = node["keystone"]["api"]["protocol"] == "https"
115+
want_fqdn = true
116+
public_fqdn = CrowbarHelper.get_host_for_public_url(node, ssl_enabled, ha_enabled, want_fqdn)
117+
118+
websso_keystone_url =
119+
if node[:keystone][:federation][:websso_keystone_url].empty?
120+
# Will use the one automagically generated by crowbar if user doesn't
121+
# explicitly set one. This should be for testing/demo purposes in most
122+
# cases.
123+
public_auth_url(node, public_fqdn)
124+
else
125+
node[:keystone][:federation][:websso_keystone_url]
126+
end
127+
websso_keystone_url
128+
end
129+
35130
def self.keystone_settings(current_node, cookbook_name)
36131
instance = current_node[cookbook_name][:keystone_instance] || "default"
37132

@@ -56,7 +151,6 @@ def self.keystone_settings(current_node, cookbook_name)
56151
ha_enabled = node[:keystone][:ha][:enabled]
57152
use_ssl = node["keystone"]["api"]["protocol"] == "https"
58153
public_host = CrowbarHelper.get_host_for_public_url(node, use_ssl, ha_enabled)
59-
60154
admin_host = CrowbarHelper.get_host_for_admin_url(node, ha_enabled)
61155

62156
has_default_user = node["keystone"]["default"]["create_user"]
@@ -71,6 +165,7 @@ def self.keystone_settings(current_node, cookbook_name)
71165
"api_version_for_middleware" => "v%.1f" % node[:keystone][:api][:version],
72166
"admin_auth_url" => admin_auth_url(node, admin_host),
73167
"public_auth_url" => public_auth_url(node, public_host),
168+
"websso_keystone_url" => websso_keystone_url(node),
74169
"internal_auth_url" => internal_auth_url(node, admin_host),
75170
"unversioned_internal_auth_url" => unversioned_internal_auth_url(node, admin_host),
76171
"use_ssl" => use_ssl,
@@ -95,7 +190,9 @@ def self.keystone_settings(current_node, cookbook_name)
95190
"default_user_domain_id" => has_default_user ? default_domain_id : nil,
96191
"default_password" => has_default_user ? node["keystone"]["default"]["password"] : nil,
97192
"service_project" => node["keystone"]["service"]["project"],
98-
"service_tenant" => node["keystone"]["service"]["project"]
193+
"service_tenant" => node["keystone"]["service"]["project"],
194+
"websso_enabled" => websso_enabled(node),
195+
"trusted_dashboards" => trusted_dashboards(node)
99196
}
100197
end
101198

chef/cookbooks/keystone/recipes/server.rb

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@
3131
# useful with .openrc
3232
package "python-openstackclient"
3333

34+
# FIXME(gyee): need a more elegant way to update the default auth methods
35+
# if these happened to change from one release to another.
36+
auth_methods = "password,token,oauth1,mapped,application_credential"
37+
3438
ha_enabled = node[:keystone][:ha][:enabled]
3539

40+
keystone_settings = KeystoneHelper.keystone_settings(node, @cookbook_name)
41+
3642
if ha_enabled
3743
log "HA support for keystone is enabled"
3844
admin_address = Chef::Recipe::Barclamp::Inventory.get_network_by_type(node, "admin").address
@@ -48,6 +54,35 @@
4854
bind_service_port = node[:keystone][:api][:service_port]
4955
end
5056

57+
# verify the OpenID Connect Federation parameters
58+
openidc_enabled = node[:keystone][:federation][:openidc][:enabled]
59+
openidc_provider = node[:keystone][:federation][:openidc][:identity_provider]
60+
openidc_response_type = node[:keystone][:federation][:openidc][:response_type]
61+
openidc_scope = node[:keystone][:federation][:openidc][:scope]
62+
openidc_metadata_url = node[:keystone][:federation][:openidc][:metadata_url]
63+
openidc_client_id = node[:keystone][:federation][:openidc][:client_id]
64+
openidc_client_secret = node[:keystone][:federation][:openidc][:client_secret]
65+
openidc_passphrase = node[:keystone][:federation][:openidc][:passphrase]
66+
openidc_redirect_uri = node[:keystone][:federation][:openidc][:redirect_uri]
67+
68+
openidc_attributes = {
69+
"openidc_response_type" => openidc_response_type,
70+
"openidc_scope" => openidc_scope,
71+
"openidc_metadata_url" => openidc_metadata_url,
72+
"openidc_client_id" => openidc_client_id,
73+
"openidc_client_secret" => openidc_client_secret,
74+
"openidc_passphrase" => openidc_passphrase,
75+
"openidc_provider" => openidc_provider
76+
}
77+
78+
if openidc_enabled
79+
auth_methods = "#{auth_methods},openid"
80+
openidc_attributes.each do |openidc_attr_name, openidc_attr_value|
81+
raise "#{openidc_attr_name} is required and cannot be an empty value" if
82+
openidc_attr_value.empty?
83+
end
84+
end
85+
5186
# Ideally this would be called admin_host, but that's already being
5287
# misleadingly used to store a value which actually represents the
5388
# service bind address.
@@ -136,6 +171,17 @@
136171
ignore_failure true
137172
end
138173

174+
# automagically populate the redirect_uri if user does not specify one
175+
if openidc_redirect_uri.empty?
176+
openidc_redirect_uri = ::File.join(
177+
keystone_settings["websso_keystone_url"],
178+
"/OS-FEDERATION/identity_providers/#{openidc_provider}/protocols/openid/auth"
179+
)
180+
end
181+
182+
package "apache2-mod_auth_openidc" if openidc_enabled
183+
apache_module "auth_openidc" if openidc_enabled
184+
139185
crowbar_openstack_wsgi "WSGI entry for keystone-public" do
140186
bind_host bind_service_host
141187
bind_port bind_service_port
@@ -153,6 +199,16 @@
153199
ssl_cacert node[:keystone][:ssl][:ca_certs] unless node[:keystone][:ssl][:insecure]
154200
# LDAP backend can be slow..
155201
timeout 600
202+
# auth_openidc configuration
203+
openidc_enabled openidc_enabled
204+
openidc_provider openidc_provider
205+
openidc_response_type openidc_response_type
206+
openidc_scope openidc_scope
207+
openidc_metadata_url openidc_metadata_url
208+
openidc_client_id openidc_client_id
209+
openidc_client_secret openidc_client_secret
210+
openidc_passphrase openidc_passphrase
211+
openidc_redirect_uri openidc_redirect_uri
156212
end
157213

158214
apache_site "keystone-public.conf" do
@@ -176,6 +232,16 @@
176232
ssl_cacert node[:keystone][:ssl][:ca_certs] unless node[:keystone][:ssl][:insecure]
177233
# LDAP backend can be slow..
178234
timeout 600
235+
# auth_openidc configuration
236+
openidc_enabled openidc_enabled
237+
openidc_provider openidc_provider
238+
openidc_response_type openidc_response_type
239+
openidc_scope openidc_scope
240+
openidc_metadata_url openidc_metadata_url
241+
openidc_client_id openidc_client_id
242+
openidc_client_secret openidc_client_secret
243+
openidc_passphrase openidc_passphrase
244+
openidc_redirect_uri openidc_redirect_uri
179245
end
180246

181247
apache_site "keystone-admin.conf" do
@@ -265,7 +331,11 @@
265331
protocol: node[:keystone][:api][:protocol],
266332
frontend: node[:keystone][:frontend],
267333
rabbit_settings: fetch_rabbitmq_settings,
268-
profiler_settings: profiler_settings
334+
profiler_settings: profiler_settings,
335+
websso_enabled: keystone_settings["websso_enabled"],
336+
trusted_dashboards: keystone_settings["trusted_dashboards"],
337+
auth_methods: auth_methods,
338+
openidc_enabled: openidc_enabled
269339
)
270340
if node[:keystone][:frontend] == "apache"
271341
notifies :create, resources(ruby_block: "set origin for apache2 restart"), :immediately
@@ -579,8 +649,6 @@
579649

580650
include_recipe "keystone::update_endpoint"
581651

582-
keystone_settings = KeystoneHelper.keystone_settings(node, @cookbook_name)
583-
584652
template "/root/.openrc" do
585653
source "openrc.erb"
586654
owner "root"

chef/cookbooks/keystone/templates/default/keystone.conf.erb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,19 @@ enabled = true
114114
trace_sqlalchemy = <%= @profiler_settings[:trace_sqlalchemy] ? "true" : "false" %>
115115
hmac_keys = <%= @profiler_settings[:hmac_keys].join(",") %>
116116
connection_string = <%= @profiler_settings[:connection_string] %>
117-
<% end -%>
117+
<% end -%>
118+
119+
<% if @websso_enabled %>
120+
[federation]
121+
<% @trusted_dashboards.each do |trusted_dashboard_url| -%>
122+
trusted_dashboard = <%= trusted_dashboard_url %>
123+
<% end -%>
124+
125+
<% if @openidc_enabled -%>
126+
[openid]
127+
remote_id_attribute = HTTP_OIDC_ISS
128+
<% end -%>
129+
<% end -%>
130+
131+
[auth]
132+
methods = <%= @auth_methods %>

0 commit comments

Comments
 (0)