Skip to content

Commit

Permalink
Add support to OpenID Connect Authentication flow
Browse files Browse the repository at this point in the history
This pull request adds support for the OpenID Connect authentication
flow in Keystone and enables both ID and access token authentication
flows. The ID token configuration is designed to allow users to
authenticate via Horizon using an identity federation; whereas the
Access token is used to allow users to authenticate in the OpenStack CLI
using a federated user.

Without this PR, if one wants to configure OpenStack to use identity
federation, he/she needs to do a lot of configurations in the keystone,
Horizon, and register quite a good number of different parameters using
the CLI such as mappings, identity providers, federated protocols, and
so on. Therefore, with this PR, we propose a method for operators to
introduce/present the IdP's metadata to Kolla-ansible, and based on the
presented metadata, Kolla-ansible takes care of all of the
configurations to prepare OpenStack to work in a federated environment.

Implements: blueprint add-openid-support
Co-Authored-By: Jason Anderson <[email protected]>
Change-Id: I0203a3470d7f8f2a54d5e126d947f540d93b8210
(cherry picked from commit f3fbe83)
  • Loading branch information
pedro-martins authored and jovial committed Jul 14, 2021
1 parent 436da12 commit 114a209
Show file tree
Hide file tree
Showing 17 changed files with 951 additions and 13 deletions.
44 changes: 44 additions & 0 deletions ansible/group_vars/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ enable_glance: "{{ enable_openstack_core | bool }}"
enable_haproxy: "yes"
enable_keepalived: "{{ enable_haproxy | bool }}"
enable_keystone: "{{ enable_openstack_core | bool }}"
enable_keystone_federation: "{{ (keystone_identity_providers | length > 0) and (keystone_identity_mappings | length > 0) }}"
enable_mariadb: "yes"
enable_memcached: "yes"
enable_neutron: "{{ enable_openstack_core | bool }}"
Expand Down Expand Up @@ -1041,6 +1042,7 @@ enable_neutron_horizon_policy_file: "{{ enable_neutron }}"
enable_nova_horizon_policy_file: "{{ enable_nova }}"

horizon_internal_endpoint: "{{ internal_protocol }}://{{ kolla_internal_fqdn | put_address_in_context('url') }}:{{ horizon_tls_port if kolla_enable_tls_internal | bool else horizon_port }}"
horizon_public_endpoint: "{{ public_protocol }}://{{ kolla_external_fqdn | put_address_in_context('url') }}:{{ horizon_tls_port if kolla_enable_tls_external | bool else horizon_port }}"

#################
# Qinling options
Expand Down Expand Up @@ -1197,3 +1199,45 @@ swift_public_endpoint: "{{ public_protocol }}://{{ swift_external_fqdn | put_add
octavia_admin_endpoint: "{{ admin_protocol }}://{{ octavia_internal_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}"
octavia_internal_endpoint: "{{ internal_protocol }}://{{ octavia_internal_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}"
octavia_public_endpoint: "{{ public_protocol }}://{{ octavia_external_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}"

###################################
# Identity federation configuration
###################################
# Here we configure all of the IdPs meta informations that will be required to implement identity federation with OpenStack Keystone.
# We require the administrator to enter the following metadata:
# * name (internal name of the IdP in Keystone);
# * openstack_domain (the domain in Keystone that the IdP belongs to)
# * protocol (the federated protocol used by the IdP; e.g. openid or saml);
# * identifier (the IdP identifier; e.g. https://accounts.google.com);
# * public_name (the public name that will be shown for users in Horizon);
# * attribute_mapping (the attribute mapping to be used for this IdP. This mapping is configured in the "keystone_identity_mappings" configuration);
# * metadata_folder (folder containing all the identity provider metadata as jsons named as the identifier without the protocol
# and with '/' escaped as %2F followed with '.provider' or '.client' or '.conf'; e.g. accounts.google.com.provider; PS, all .conf,
# .provider and .client jsons must be in the folder, even if you dont override any conf in the .conf json, you must leave it as an empty json '{}');
# * certificate_file (the path to the Identity Provider certificate file, the file must be named as 'certificate-key-id.pem';
# e.g. LRVweuT51StjMdsna59jKfB3xw0r8Iz1d1J1HeAbmlw.pem; You can find the key-id in the Identity provider '.well-known/openid-configuration' jwks_uri as kid);
#
# The IdPs meta information are to be presented to Kolla-Ansible as the following example:
# keystone_identity_providers:
# - name: "myidp1"
# openstack_domain: "my-domain"
# protocol: "openid"
# identifier: "https://accounts.google.com"
# public_name: "Authenticate via myidp1"
# attribute_mapping: "mappingId1"
# metadata_folder: "path/to/metadata/folder"
# certificate_file: "path/to/certificate/file.pem"
#
# We also need to configure the attribute mapping that is used by IdPs.
# The configuration of attribute mappings is a list of objects, where each
# object must have a 'name' (that mapps to the 'attribute_mapping' to the IdP
# object in the IdPs set), and the 'file' with a full qualified path to a mapping file.
# keystone_identity_mappings:
# - name: "mappingId1"
# file: "/full/qualified/path/to/mapping/json/file/to/mappingId1"
# - name: "mappingId2"
# file: "/full/qualified/path/to/mapping/json/file/to/mappingId2"
# - name: "mappingId3"
# file: "/full/qualified/path/to/mapping/json/file/to/mappingId3"
keystone_identity_providers: []
keystone_identity_mappings: []
2 changes: 2 additions & 0 deletions ansible/roles/haproxy-config/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ haproxy_backend_tcp_extra: []

haproxy_health_check: "check inter 2000 rise 2 fall 5"
haproxy_health_check_ssl: "check check-ssl inter 2000 rise 2 fall 5"

haproxy_enable_federation_openid: "{{ keystone_identity_providers | selectattr('protocol','equalto','openid') | list | count > 0 }}"
8 changes: 7 additions & 1 deletion ansible/roles/horizon/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ horizon_extra_volumes: "{{ default_extra_volumes }}"
# OpenStack
####################
horizon_logging_debug: "{{ openstack_logging_debug }}"
horizon_keystone_url: "{{ keystone_internal_url }}/v3"
horizon_keystone_url: "{{ keystone_public_url if horizon_use_keystone_public_url | bool else keystone_internal_url }}/v3"


####################
Expand All @@ -152,3 +152,9 @@ horizon_murano_source_version: "{{ kolla_source_version }}"
# TLS
####################
horizon_enable_tls_backend: "{{ kolla_enable_tls_backend }}"

# This variable was created for administrators to define which one of the Keystone's URLs should be configured in Horizon.
# In some cases, such as when using OIDC, horizon will need to be configured with Keystone's public URL.
# Therefore, instead of overriding the whole "horizon_keystone_url", this change allows an easier integration because
# the Keystone public URL is already defined with variable "keystone_public_url".
horizon_use_keystone_public_url: False
27 changes: 15 additions & 12 deletions ansible/roles/horizon/templates/local_settings.j2
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,9 @@ OPENSTACK_HOST = "{{ kolla_internal_fqdn }}"
OPENSTACK_KEYSTONE_URL = "{{ horizon_keystone_url }}"
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}"

{% if enable_keystone_federation | bool %}
# Enables keystone web single-sign-on if set to True.
#WEBSSO_ENABLED = False
WEBSSO_ENABLED = True

# Determines which authentication choice to show as default.
#WEBSSO_INITIAL_CHOICE = "credentials"
Expand All @@ -223,13 +224,13 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}"
# Do not remove the mandatory credentials mechanism.
# Note: The last two tuples are sample mapping keys to a identity provider
# and federation protocol combination (WEBSSO_IDP_MAPPING).
#WEBSSO_CHOICES = (
# ("credentials", _("Keystone Credentials")),
# ("oidc", _("OpenID Connect")),
# ("saml2", _("Security Assertion Markup Language")),
# ("acme_oidc", "ACME - OpenID Connect"),
# ("acme_saml2", "ACME - SAML2"),
#)
WEBSSO_KEYSTONE_URL = "{{ keystone_public_url }}/v3"
WEBSSO_CHOICES = (
("credentials", _("Keystone Credentials")),
{% for idp in keystone_identity_providers %}
("{{ idp.name }}_{{ idp.protocol }}", "{{ idp.public_name }}"),
{% endfor %}
)

# A dictionary of specific identity provider and federation protocol
# combinations. From the selected authentication mechanism, the value
Expand All @@ -238,10 +239,12 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}"
# specific WebSSO endpoint in keystone, otherwise it will use the value
# as the protocol_id when redirecting to the WebSSO by protocol endpoint.
# NOTE: The value is expected to be a tuple formatted as: (<idp_id>, <protocol_id>).
#WEBSSO_IDP_MAPPING = {
# "acme_oidc": ("acme", "oidc"),
# "acme_saml2": ("acme", "saml2"),
#}
WEBSSO_IDP_MAPPING = {
{% for idp in keystone_identity_providers %}
"{{ idp.name }}_{{ idp.protocol }}": ("{{ idp.name }}", "{{ idp.protocol }}"),
{% endfor %}
}
{% endif %}

# Disable SSL certificate checks (useful for self-signed certificates):
#OPENSTACK_SSL_NO_VERIFY = True
Expand Down
22 changes: 22 additions & 0 deletions ansible/roles/keystone/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ keystone_services:
tls_backend: "{{ keystone_enable_tls_backend }}"
port: "{{ keystone_public_port }}"
listen_port: "{{ keystone_public_listen_port }}"
backend_http_extra: "{{ ['balance source'] if enable_keystone_federation | bool else [] }}"
keystone_external:
enabled: "{{ enable_keystone }}"
mode: "http"
external: true
tls_backend: "{{ keystone_enable_tls_backend }}"
port: "{{ keystone_public_port }}"
listen_port: "{{ keystone_public_listen_port }}"
backend_http_extra: "{{ ['balance source'] if enable_keystone_federation | bool else [] }}"
keystone_admin:
enabled: "{{ enable_keystone }}"
mode: "http"
Expand Down Expand Up @@ -179,3 +181,23 @@ keystone_ks_services:
# TLS
####################
keystone_enable_tls_backend: "{{ kolla_enable_tls_backend }}"

###############################
# OpenStack identity federation
###############################
# Default OpenID Connect remote attribute key
keystone_remote_id_attribute_oidc: "HTTP_OIDC_ISS"
keystone_container_federation_oidc_metadata_folder: "{{ '/etc/apache2/metadata' if kolla_base_distro in ['debian', 'ubuntu'] else '/etc/httpd/metadata' }}"
keystone_container_federation_oidc_idp_certificate_folder: "{{ '/etc/apache2/cert' if kolla_base_distro in ['debian', 'ubuntu'] else '/etc/httpd/cert' }}"
keystone_container_federation_oidc_attribute_mappings_folder: "{{ container_config_directory }}/federation/oidc/attribute_maps"
keystone_host_federation_oidc_metadata_folder: "{{ node_config_directory }}/keystone/federation/oidc/metadata"
keystone_host_federation_oidc_idp_certificate_folder: "{{ node_config_directory }}/keystone/federation/oidc/cert"
keystone_host_federation_oidc_attribute_mappings_folder: "{{ node_config_directory }}/keystone/federation/oidc/attribute_maps"

# These variables are used to define multiple trusted Horizon dashboards.
# keystone_trusted_dashboards: ['<https://dashboardServerOne/auth/websso/>', '<https://dashboardServerTwo/auth/websso/>', '<https://dashboardServerN/auth/websso/>']
keystone_trusted_dashboards: "{{ ['%s://%s/auth/websso/' % (public_protocol, kolla_external_fqdn), '%s/auth/websso/' % (horizon_public_endpoint)] if enable_horizon | bool else [] }}"
keystone_enable_federation_openid: "{{ enable_keystone_federation | bool and keystone_identity_providers | selectattr('protocol','equalto','openid') | list | count > 0 }}"
keystone_should_remove_attribute_mappings: False
keystone_should_remove_identity_providers: False
keystone_federation_oidc_scopes: "openid email profile"
86 changes: 86 additions & 0 deletions ansible/roles/keystone/tasks/config-federation-oidc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
- name: Remove OpenID certificate and metadata files
become: true
vars:
keystone: "{{ keystone_services['keystone'] }}"
file:
state: absent
path: "{{ item }}"
when:
- inventory_hostname in groups[keystone.group]
with_items:
- "{{ keystone_host_federation_oidc_metadata_folder }}"
- "{{ keystone_host_federation_oidc_idp_certificate_folder }}"
- "{{ keystone_host_federation_oidc_attribute_mappings_folder }}"

- name: Create OpenID configuration directories
vars:
keystone: "{{ keystone_services['keystone'] }}"
file:
dest: "{{ item }}"
state: "directory"
mode: "0770"
become: true
with_items:
- "{{ keystone_host_federation_oidc_metadata_folder }}"
- "{{ keystone_host_federation_oidc_idp_certificate_folder }}"
- "{{ keystone_host_federation_oidc_attribute_mappings_folder }}"
when:
- inventory_hostname in groups[keystone.group]

- name: Copying OpenID Identity Providers metadata
vars:
keystone: "{{ keystone_services['keystone'] }}"
become: true
copy:
src: "{{ item.metadata_folder }}/"
dest: "{{ keystone_host_federation_oidc_metadata_folder }}"
mode: "0660"
with_items: "{{ keystone_identity_providers }}"
when:
- item.protocol == 'openid'
- inventory_hostname in groups[keystone.group]

- name: Copying OpenID Identity Providers certificate
vars:
keystone: "{{ keystone_services['keystone'] }}"
become: true
copy:
src: "{{ item.certificate_file }}"
dest: "{{ keystone_host_federation_oidc_idp_certificate_folder }}"
mode: "0660"
with_items: "{{ keystone_identity_providers }}"
when:
- item.protocol == 'openid'
- inventory_hostname in groups[keystone.group]

- name: Copying OpenStack Identity Providers attribute mappings
vars:
keystone: "{{ keystone_services['keystone'] }}"
become: true
copy:
src: "{{ item.file }}"
dest: "{{ keystone_host_federation_oidc_attribute_mappings_folder }}/{{ item.file | basename }}"
mode: "0660"
with_items: "{{ keystone_identity_mappings }}"
when:
- inventory_hostname in groups[keystone.group]

- name: Setting the certificates files variable
become: true
vars:
keystone: "{{ keystone_services['keystone'] }}"
find:
path: "{{ keystone_host_federation_oidc_idp_certificate_folder }}"
pattern: "*.pem"
register: certificates_path
when:
- inventory_hostname in groups[keystone.group]

- name: Setting the certificates variable
vars:
keystone: "{{ keystone_services['keystone'] }}"
set_fact:
keystone_federation_openid_certificate_key_ids: "{{ certificates_path.files | map(attribute='path') | map('regex_replace', '^.*/(.*)\\.pem$', '\\1#' + keystone_container_federation_oidc_idp_certificate_folder + '/\\1.pem') | list }}" # noqa 204
when:
- inventory_hostname in groups[keystone.group]
4 changes: 4 additions & 0 deletions ansible/roles/keystone/tasks/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@
notify:
- Restart {{ item.key }} container

- include_tasks: config-federation-oidc.yml
when:
- keystone_enable_federation_openid | bool

- name: Copying over wsgi-keystone.conf
vars:
keystone: "{{ keystone_services.keystone }}"
Expand Down
4 changes: 4 additions & 0 deletions ansible/roles/keystone/tasks/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@
- import_tasks: register.yml

- import_tasks: check.yml

- include_tasks: register_identity_providers.yml
when:
- enable_keystone_federation | bool
Loading

0 comments on commit 114a209

Please sign in to comment.