From 17ac571e7a441152ae78ef8363d5b79f9edbdec1 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 18 Jun 2015 19:06:56 -0700 Subject: [PATCH] Keystone Federation Service Provider Configuration This patch adds the ability to configure Keystone as a Service Provider (SP) for a Federated Identity Provider (IdP). * New variables to configure Keystone as a service provider are now supported under a root `keystone_sp` variable. Example configurations can be seen in Keystone's defaults file. This configuration includes the list of identity providers and trusted dashboards. (At this time only one identity provider is supported). * Identity provider configuration includes the remote-to-local user mapping and the list of remote attributes the SP can obtain from the IdP. * Shibboleth is installed and configured in the Keystone containers when SP configuration is present. * Horizon is configured for SSO login DocImpact UpgradeImpact Implements: blueprint keystone-federation Change-Id: I78b3d740434ea4b3ca0bd9f144e4a07026be23c6 Co-Authored-By: Jesse Pretorius --- defaults/main.yml | 78 ++++++++++- files/sso_callback_template.html | 22 +++ handlers/main.yml | 10 ++ library/keystone_sp | 117 ++++++++++++++++ tasks/keystone_apache.yml | 12 ++ tasks/keystone_federation_sp_idp_setup.yml | 152 +++++++++++++++++++++ tasks/keystone_federation_sp_setup.yml | 101 ++++++++++++++ tasks/keystone_install.yml | 13 ++ tasks/keystone_post_install.yml | 1 + tasks/main.yml | 9 ++ templates/keystone-httpd.conf.j2 | 26 ++++ templates/keystone.conf.j2 | 16 ++- templates/shibboleth-attribute-map.xml.j2 | 63 +++++++++ templates/shibboleth2.xml.j2 | 104 ++++++++++++++ 14 files changed, 722 insertions(+), 2 deletions(-) create mode 100644 files/sso_callback_template.html create mode 100644 library/keystone_sp create mode 100644 tasks/keystone_federation_sp_idp_setup.yml create mode 100644 tasks/keystone_federation_sp_setup.yml create mode 100644 templates/shibboleth-attribute-map.xml.j2 create mode 100644 templates/shibboleth2.xml.j2 diff --git a/defaults/main.yml b/defaults/main.yml index 0bb5a62e..74615039 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -120,7 +120,7 @@ keystone_service_internalurl: "{{ keystone_service_internalurl_v3 }}" keystone_service_adminurl: "{{ keystone_service_adminurl_v3 }}" ## Set this value to override the "public_endpoint" keystone.conf variable -#keystone_public_endpoint: +#keystone_public_endpoint: "{{ keystone_service_publicuri }}" ## Apache setup keystone_apache_log_level: info @@ -190,6 +190,82 @@ keystone_recreate_keys: False # contact_telephone: 555-55-5555 # contact_type: technical +# Enable the following section in order to install and configure +# Keystone as a Resource Service Provider (SP) and to configure +# trusts with specific Identity Providers (IdP). +#keystone_sp: +# cert_duration_years: 5 +# trusted_dashboard_list: +# - "https://{{ external_lb_vip_address }}/auth/websso/" +# trusted_idp_list: +# note that only one of these is supported at any one time for now +# - name: "keystone-idp" +# entity_ids: +# - 'https://keystone-idp:5000/v3/OS-FEDERATION/saml2/idp' +# metadata_uri: 'https://keystone-idp:5000/v3/OS-FEDERATION/saml2/metadata' +# metadata_file: 'metadata-keystone-idp.xml' +# metadata_reload: 1800 +# federated_identities: +# - domain: Default +# project: fedproject +# group: fedgroup +# role: _member_ +# protocols: +# - name: saml2 +# mapping: +# name: keystone-idp-mapping +# rules: +# - remote: +# - type: openstack_user +# local: +# - group: +# name: fedgroup +# domain: +# name: Default +# user: +# name: '{0}' +# attributes: +# - name: openstack_user +# id: openstack_user +# - name: openstack_roles +# id: openstack_roles +# - name: openstack_project +# id: openstack_project +# - name: openstack_user_domain +# id: openstack_user_domain +# - name: openstack_project_domain +# id: openstack_project_domain +# +# - name: 'testshib-idp' +# entity_ids: +# - 'https://idp.testshib.org/idp/shibboleth' +# metadata_uri: 'http://www.testshib.org/metadata/testshib-providers.xml' +# metadata_file: 'metadata-testshib-idp.xml' +# metadata_reload: 1800 +# federated_identities: +# - domain: Default +# project: fedproject +# group: fedgroup +# role: _member_ +# protocols: +# - name: saml2 +# mapping: +# name: testshib-idp-mapping +# rules: +# - remote: +# - type: eppn +# local: +# - group: +# name: fedgroup +# domain: +# name: Default +# - user: +# name: '{0}' + +# Keystone Federation SP Packages +keystone_sp_apt_packages: + - libapache2-mod-shib2 + # Common apt packages keystone_apt_packages: - apache2 diff --git a/files/sso_callback_template.html b/files/sso_callback_template.html new file mode 100644 index 00000000..3364d69e --- /dev/null +++ b/files/sso_callback_template.html @@ -0,0 +1,22 @@ + + + + Keystone WebSSO redirect + + +
+ Please wait... +
+ + +
+ + + diff --git a/handlers/main.yml b/handlers/main.yml index 989637d1..259c53ea 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -22,3 +22,13 @@ until: apache_restart|success retries: 5 delay: 2 + +- name: Restart Shibd + service: + name: "shibd" + state: "restarted" + pattern: "shibd" + register: shibd_restart + until: shibd_restart|success + retries: 5 + delay: 2 diff --git a/library/keystone_sp b/library/keystone_sp new file mode 100644 index 00000000..da6e73df --- /dev/null +++ b/library/keystone_sp @@ -0,0 +1,117 @@ +#!/usr/bin/python +# (c) 2015, Kevin Carter +# +# Copyright 2015, Rackspace US, Inc. +# +# Licensed 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. + +DOCUMENTATION = """ +--- +module: keystone_sp +version_added: "1.9.2" +short_description: + - Creates a fact for keystone_federated_identities and keystone_protocols +description: + - Sets facts called `keystone_federated_identities` and + `keystone_federated_protocols`, which are lists of hashes built from + keystone_sp using the information in the `federated_identities` and + `protocols` keys. +options: + sp_data: + description: + - Hash to build the service provider lists from + required: true +author: Kevin Carter +""" + +EXAMPLES = """ +# Set the keystone_federated_identities and keystone_federated_protocols facts +- keystone_sp: + sp_data: "{{ keystone_sp }}" + when: keystone_sp is defined +""" + +# Keystone service provider data structure example. +""" +keystone_sp: + trusted_idp_list: + - name: "keystone-idp" + federated_identities: + - domain: Default + project: fedproject + group: fedgroup + role: _member_ + protocols: + - name: saml2 + mapping: + ... + - name: 'testshib-idp' + federated_identities: + - domain: Default + project: fedproject2 + group: fedgroup2 + role: _member_ + protocols: + - name: saml2 + mapping: + ... +""" + + +class KeystoneSp(object): + def __init__(self, module): + """Generate an integer from a name.""" + self.module = module + self.identities_return_list = list() + self.protocols_return_list = list() + self.sp_data = self.module.params['sp_data'] + + def populate_sp_data(self): + trusted_idp_list = self.sp_data['trusted_idp_list'] + for trusted_idp in trusted_idp_list: + federated_identities = trusted_idp.get('federated_identities') + if federated_identities: + self.identities_return_list.extend(federated_identities) + protocols = trusted_idp.get('protocols') + if protocols: + for protocol in protocols: + self.protocols_return_list.append( + {'idp': trusted_idp, 'protocol': protocol}) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + sp_data=dict( + required=True + ) + ), + supports_check_mode=False + ) + try: + ksp = KeystoneSp(module=module) + ksp.populate_sp_data() + module.exit_json( + changed=True, + ansible_facts={ + 'keystone_federated_identities': ksp.identities_return_list, + 'keystone_federated_protocols': ksp.protocols_return_list} + ) + except Exception as exp: + resp = {'stderr': exp} + module.fail_json(msg='Failed Process', **resp) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/tasks/keystone_apache.yml b/tasks/keystone_apache.yml index 6fb72969..e97e8e7d 100644 --- a/tasks/keystone_apache.yml +++ b/tasks/keystone_apache.yml @@ -61,5 +61,17 @@ apache2_module: name: ssl state: "{{ (keystone_ssl_enabled | bool) | ternary('present', 'absent') }}" + notify: + - Restart Apache + tags: + - keystone-httpd + +- name: Enable/disable mod_shib2 for apache2 + apache2_module: + name: shib2 + state: "{{ ( keystone_sp is defined ) | ternary('present', 'absent') }}" + ignore_errors: yes + notify: + - Restart Apache tags: - keystone-httpd diff --git a/tasks/keystone_federation_sp_idp_setup.yml b/tasks/keystone_federation_sp_idp_setup.yml new file mode 100644 index 00000000..9d0bb7ee --- /dev/null +++ b/tasks/keystone_federation_sp_idp_setup.yml @@ -0,0 +1,152 @@ +--- +# Copyright 2014, Rackspace US, Inc. +# +# Licensed 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. + +# note that these tasks will run when the id/name parameter is present. +# Providing the id/name without the other required params is a user error. + +# TODO: Revisit this method when Ansible 2 releases +# User with_subelements instead, but in v1.x it's broken +- name: Set keystone_federated_identities fact + keystone_sp: + sp_data: "{{ keystone_sp }}" + tags: + - keystone-federation-sp + +- name: Ensure domain which remote IDP users are mapped onto exists + keystone: + command: ensure_domain + domain_name: "{{ item.domain }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: item.domain is defined + with_items: keystone_federated_identities + tags: + - keystone-federation-sp + +- name: Ensure project which remote IDP users are mapped onto exists + keystone: + command: ensure_project + project_name: "{{ item.project }}" + domain_name: "{{ item.domain | default('Default') }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: item.project is defined + with_items: keystone_federated_identities + tags: + - keystone-federation-sp + +- name: Ensure user which remote IDP users are mapped onto exists + keystone: + command: ensure_user + user_name: "{{ item.user }}" + password: "{{ item.password }}" + project_name: "{{ item.project }}" + domain_name: "{{ item.domain | default('Default') }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: > + item.user is defined and + item.password is defined and + item.project is defined + with_items: keystone_federated_identities + tags: + - keystone-federation-sp + +- name: Ensure Group for external IDP users exists + keystone: + command: ensure_group + group_name: "{{ item.group }}" + domain_name: "{{ item.domain | default('Default') }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: item.group is defined + with_items: keystone_federated_identities + tags: + - keystone-federation-sp + +- name: Ensure Role for external IDP users exists + keystone: + command: "ensure_role" + role_name: "{{ item.role | default('_member_') }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: > + item.group is defined and + item.project is defined + with_items: keystone_federated_identities + tags: + - keystone-federation-sp + +- name: Ensure Group/Project/Role mapping exists + keystone: + command: ensure_group_role + group_name: "{{ item.group }}" + project_name: "{{ item.project }}" + role_name: "{{ item.role | default('_member_') }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: > + item.group is defined and + item.project is defined + with_items: keystone_federated_identities + tags: + - keystone-federation-sp + +- name: Ensure mapping for external IDP attributes exists + keystone: + command: ensure_mapping + mapping_name: "{{ item.protocol.mapping.name }}" + mapping_rules: "{{ item.protocol.mapping.rules }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: item.protocol.mapping.name is defined + with_items: keystone_federated_protocols + tags: + - keystone-federation-sp + +- name: Ensure external IDP + keystone: + command: ensure_identity_provider + idp_name: "{{ item.name }}" + idp_remote_ids: "{{ item.entity_ids }}" + idp_enabled: true + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: item.name is defined + with_items: keystone_sp.trusted_idp_list + tags: + - keystone-federation-sp + +- name: Ensure federation protocol exists + keystone: + command: ensure_protocol + protocol_name: "{{ item.protocol.name }}" + idp_name: "{{ item.idp.name }}" + mapping_name: "{{ item.protocol.mapping.name }}" + token: "{{ keystone_auth_admin_token }}" + endpoint: "{{ keystone_service_adminurl }}" + insecure: "{{ keystone_service_adminuri_insecure }}" + when: item.protocol.name is defined + with_items: keystone_federated_protocols + tags: + - keystone-federation-sp diff --git a/tasks/keystone_federation_sp_setup.yml b/tasks/keystone_federation_sp_setup.yml new file mode 100644 index 00000000..ba21f0d6 --- /dev/null +++ b/tasks/keystone_federation_sp_setup.yml @@ -0,0 +1,101 @@ +--- +# Copyright 2015, Rackspace US, Inc. +# +# Licensed 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. + +- name: Drop Shibboleth Config + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + owner: "{{ keystone_system_user_name }}" + group: "{{ keystone_system_group_name }}" + mode: "{{ item.mode|default('0644') }}" + with_items: + - { src: "shibboleth-attribute-map.xml.j2", dest: "/etc/shibboleth/attribute-map.xml" } + - { src: "shibboleth2.xml.j2", dest: "/etc/shibboleth/shibboleth2.xml" } + notify: + - Restart Shibd + tags: + - keystone-config + - keystone-federation-sp + +- name: Generate the Shibboleth SP key-pair + shell: "shib-keygen -h {{ external_lb_vip_address }} -y {{ keystone_sp.cert_duration_years }}" + args: + creates: "/etc/shibboleth/sp-cert.pem" + when: inventory_hostname == groups['keystone_all'][0] + notify: + - Restart Apache + - Restart Shibd + tags: + - keystone-config + - keystone-federation-sp + +- name: Store Shibboleth SP key-pair + memcached: + name: "{{ item.name }}" + file_path: "{{ item.src }}" + state: "present" + server: "{{ memcached_servers }}" + encrypt_string: "{{ memcached_encryption_key }}" + with_items: + - { src: "/etc/shibboleth/sp-cert.pem", name: "keystone_sp_cert" } + - { src: "/etc/shibboleth/sp-key.pem", name: "keystone_sp_key" } + register: memcache_keys + until: memcache_keys|success + retries: 5 + delay: 2 + when: inventory_hostname == groups['keystone_all'][0] + tags: + - keystone-config + - keystone-federation-sp + +- name: Distribute the Shibboleth SP key-pair + memcached: + name: "{{ item.name }}" + file_path: "{{ item.src }}" + state: "retrieve" + file_mode: "{{ item.file_mode }}" + dir_mode: "{{ item.dir_mode }}" + server: "{{ memcached_servers }}" + encrypt_string: "{{ memcached_encryption_key }}" + with_items: + - { src: "/etc/shibboleth/sp-cert.pem", name: "keystone_sp_cert", file_mode: "0640", dir_mode: "0750" } + - { src: "/etc/shibboleth/sp-key.pem", name: "keystone_sp_key", file_mode: "0600", dir_mode: "0750" } + register: memcache_keys + until: memcache_keys|success + retries: 5 + delay: 2 + when: inventory_hostname != groups['keystone_all'][0] + notify: + - Restart Apache + - Restart Shibd + tags: + - keystone-config + - keystone-federation-sp + +- name: Set appropriate file ownership on the Shibboleth SP key-pair + file: + path: "{{ item }}" + owner: "_shibd" + group: "_shibd" + with_items: + - "/etc/shibboleth/sp-cert.pem" + - "/etc/shibboleth/sp-key.pem" + when: inventory_hostname != groups['keystone_all'][0] + notify: + - Restart Apache + - Restart Shibd + tags: + - keystone-config + - keystone-federation-sp diff --git a/tasks/keystone_install.yml b/tasks/keystone_install.yml index 216f6cea..21ff7a0e 100644 --- a/tasks/keystone_install.yml +++ b/tasks/keystone_install.yml @@ -49,6 +49,19 @@ tags: - keystone-apt-packages +- name: Install SP apt packages + apt: + pkg: "{{ item }}" + state: latest + register: install_packages + until: install_packages|success + retries: 5 + delay: 2 + with_items: keystone_sp_apt_packages + when: keystone_sp is defined + tags: + - keystone-apt-packages + - name: Install pip packages pip: name: "{{ item }}" diff --git a/tasks/keystone_post_install.yml b/tasks/keystone_post_install.yml index 29119547..6e92e9ae 100644 --- a/tasks/keystone_post_install.yml +++ b/tasks/keystone_post_install.yml @@ -36,6 +36,7 @@ mode: "{{ item.mode|default('0644') }}" with_items: - { src: "keystone-paste.ini", dest: "/etc/keystone/keystone-paste.ini" } + - { src: "sso_callback_template.html", dest: "/etc/keystone/sso_callback_template.html" } - { src: "keystone-wsgi.py", dest: "/var/www/cgi-bin/keystone/admin", mode: "0755" } - { src: "keystone-wsgi.py", dest: "/var/www/cgi-bin/keystone/main", mode: "0755" } notify: diff --git a/tasks/main.yml b/tasks/main.yml index fc8c8346..b2b9a66c 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -29,6 +29,10 @@ - include: keystone_post_install.yml +- include: keystone_federation_sp_setup.yml + when: > + keystone_sp is defined + - include: keystone_db_setup.yml when: > inventory_hostname == groups['keystone_all'][0] @@ -40,6 +44,11 @@ when: > inventory_hostname == groups['keystone_all'][0] +- include: keystone_federation_sp_idp_setup.yml + when: > + keystone_sp is defined and + inventory_hostname == groups['keystone_all'][0] + - name: Flush handlers meta: flush_handlers diff --git a/templates/keystone-httpd.conf.j2 b/templates/keystone-httpd.conf.j2 index 3c86f325..e3f85096 100644 --- a/templates/keystone-httpd.conf.j2 +++ b/templates/keystone-httpd.conf.j2 @@ -25,6 +25,32 @@ WSGIDaemonProcess keystone user={{ keystone_system_user_name }} group=nogroup pr SSLOptions +StdEnvVars +ExportCertData {% endif %} + {% if keystone_sp is defined -%} + ShibURLScheme {{ keystone_service_publicuri_proto }} + + + SetHandler shib + + + + AuthType shibboleth + ShibRequestSetting requireSession 1 + ShibRequestSetting exportAssertion 1 + ShibRequireSession On + ShibExportAssertion On + Require valid-user + + + + ShibRequestSetting requireSession 1 + AuthType shibboleth + ShibExportAssertion Off + Require valid-user + + + WSGIScriptAliasMatch ^(/v3/OS-FEDERATION/identity_providers/.*?/protocols/.*?/auth)$ /var/www/cgi-bin/keystone/main/$1 + {%- endif %} + WSGIScriptAlias / /var/www/cgi-bin/keystone/main WSGIProcessGroup keystone diff --git a/templates/keystone.conf.j2 b/templates/keystone.conf.j2 index b2c61a4a..4eb82f44 100644 --- a/templates/keystone.conf.j2 +++ b/templates/keystone.conf.j2 @@ -43,8 +43,12 @@ cache_time = {{ keystone_revocation_cache_time }} [auth] +{% if keystone_sp is defined %} +methods = {{ keystone_auth_methods }},saml2 +saml2 = keystone.auth.plugins.mapped.Mapped +{% else %} methods = {{ keystone_auth_methods }} - +{% endif %} [database] connection = mysql://{{ keystone_galera_user }}:{{ keystone_container_mysql_password }}@{{ keystone_galera_address }}/{{ keystone_galera_database }}?charset=utf8 @@ -132,3 +136,13 @@ public_port = {{ keystone_service_port }} rabbit_hosts = {{ rabbitmq_servers }} rabbit_userid = {{ rabbitmq_userid }} rabbit_password = {{ rabbitmq_password }} + +{% if keystone_sp is defined %} +[federation] +remote_id_attribute = Shib-Identity-Provider +{% if keystone_sp.trusted_dashboard_list is defined %} +{% for item in keystone_sp.trusted_dashboard_list %} +trusted_dashboard = {{ item }} +{% endfor %} +{% endif %} +{% endif %} diff --git a/templates/shibboleth-attribute-map.xml.j2 b/templates/shibboleth-attribute-map.xml.j2 new file mode 100644 index 00000000..7e5271d0 --- /dev/null +++ b/templates/shibboleth-attribute-map.xml.j2 @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for idp in keystone_sp.trusted_idp_list %} + {% if idp.protocols is defined %} + {% for protocol in idp.protocols %} + {% if protocol.name == "saml2" and protocol.attributes is defined %} + {% for attr in protocol.attributes %} + + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + + diff --git a/templates/shibboleth2.xml.j2 b/templates/shibboleth2.xml.j2 new file mode 100644 index 00000000..1aff057a --- /dev/null +++ b/templates/shibboleth2.xml.j2 @@ -0,0 +1,104 @@ + + + + + + + + + + + + + SAML2 SAML1 + + + + + SAML2 Local + + + + + + + + + + + + + + + + + + + + +{% if keystone_sp.trusted_idp_list is defined -%} + {% for item in keystone_sp.trusted_idp_list %} + + {% endfor %} +{% endif %} + + + + + + + + + + + + + + + + + + + + + +