diff --git a/Gemfile b/Gemfile index 06508087..5e5823f6 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,7 @@ gem 'hiera-eyaml' gem 'net-ldap', require: "net/ldap" gem 'breadcrumbs_on_rails' gem 'cancancan' +gem 'ruby-saml' # To use retry middleware with Faraday v2.0+ gem 'faraday-retry' diff --git a/Gemfile.lock b/Gemfile.lock index ba845ee7..d01c29d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -332,6 +332,9 @@ GEM rubocop-rake (0.6.0) rubocop (~> 1.0) ruby-progressbar (1.11.0) + ruby-saml (1.15.0) + nokogiri (>= 1.13.10) + rexml ruby2_keywords (0.0.5) rubyzip (2.3.2) sassc (2.4.0) @@ -435,6 +438,7 @@ DEPENDENCIES rubocop rubocop-rails rubocop-rake + ruby-saml selenium-webdriver simplecov sprockets-rails diff --git a/app/controllers/saml_sessions_controller.rb b/app/controllers/saml_sessions_controller.rb new file mode 100644 index 00000000..fb0f97fb --- /dev/null +++ b/app/controllers/saml_sessions_controller.rb @@ -0,0 +1,41 @@ +class SamlSessionsController < ApplicationController + skip_before_action :authentication_required + skip_forgery_protection only: :create + + def new + saml_request = OneLogin::RubySaml::Authrequest.new + redirect_to saml_request.create(saml_settings), allow_other_host: true + end + + def create + saml_response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) + saml_response.settings = saml_settings + + if saml_response.is_valid? + user = find_or_create_user(saml_response) + session[:user_id] = user.id + if user.admin? + redirect_to users_path, notice: "Logged in!" + else + redirect_to root_url, notice: "Logged in!" + end + else + redirect_to new_session_path, alert: "Could not sign you in via SSO" + end + end + + private + + def find_or_create_user(saml_response) + User.find_or_create_by!(email: saml_response.nameid) do |user| + user.first_name = "SAML" + user.last_name = "User" + end + end + + def saml_settings + settings = Saml.new.settings + settings.assertion_consumer_service_url = saml_session_url + settings + end +end diff --git a/app/models/saml.rb b/app/models/saml.rb new file mode 100644 index 00000000..ac0a7beb --- /dev/null +++ b/app/models/saml.rb @@ -0,0 +1,14 @@ +class Saml + def self.configured? + Rails.configuration.hdm[:saml].present? + end + + def initialize + @hdm_settings = Rails.configuration.hdm[:saml] + @hdm_settings[:name_identifier_format] ||= "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + end + + def settings + OneLogin::RubySaml::Settings.new(@hdm_settings) + end +end diff --git a/app/views/ldap_sessions/new.html.erb b/app/views/ldap_sessions/new.html.erb index 65acb536..4c7df0f1 100644 --- a/app/views/ldap_sessions/new.html.erb +++ b/app/views/ldap_sessions/new.html.erb @@ -1,13 +1,6 @@

Login

- +<%= render "shared/auth_navigation", active: :ldap %> <%= form_tag ldap_session_path do |form| %>
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 445c6723..3b95e3d0 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -1,15 +1,6 @@

Login

-<% if Ldap.configured? %> - -<% end %> +<%= render "shared/auth_navigation", active: :local %> <%= form_tag sessions_path do |form| %>
diff --git a/app/views/shared/_auth_navigation.html.erb b/app/views/shared/_auth_navigation.html.erb new file mode 100644 index 00000000..9e600207 --- /dev/null +++ b/app/views/shared/_auth_navigation.html.erb @@ -0,0 +1,20 @@ +<% if Ldap.configured? || Saml.configured? %> + +<% end %> diff --git a/config/hdm.yml.template b/config/hdm.yml.template index b4e1e370..cc95923c 100644 --- a/config/hdm.yml.template +++ b/config/hdm.yml.template @@ -65,3 +65,17 @@ production: # base_dn: "ou=hdm,dc=nodomain" # bind_dn: "cn=admin,dc=nodomain" # bind_dn_password: "openldap" + +# Example for SAML SSO authentication +# production: +# read_only: false +# allow_encryption: true +# puppet_db: +# server: "https://localhost:8081" +# config_dir: "/etc/puppetlabs/code" +# saml: +# sp_entity_id: "my-id" +# idp_sso_service_url: "https://my_idp/saml_endpoint" +# idp_cert_fingerprint: "aaa" +# idp_cert: "cert" # use either fingerprint _or_ cert but not both + diff --git a/config/routes.rb b/config/routes.rb index ab694772..e22d9fc9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,6 +33,7 @@ resources :users resource :ldap_session, only: [:new, :create] + resource :saml_session, only: [:new, :create] get 'page/index' diff --git a/test/controllers/saml_sessions_controller_test.rb b/test/controllers/saml_sessions_controller_test.rb new file mode 100644 index 00000000..0c7f82cc --- /dev/null +++ b/test/controllers/saml_sessions_controller_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +class SamlSessionsControllerTest < ActionDispatch::IntegrationTest + setup do + Rails.configuration.hdm[:saml] = SAML_TEST_CONFIG.dup + end + + teardown do + Rails.configuration.hdm.delete(:saml) + end + + test "#new redirects to SSO" do + get new_saml_session_path + + assert_redirected_to %r{\Ahttps://testsso} + end + + test "#create with successful SSO redirects to root_path" do + stubbed_saml_response(valid: true) do + post saml_session_path + assert_redirected_to root_path + end + end + + test "#create with failed SSO redirects to login page" do + stubbed_saml_response(valid: false) do + post saml_session_path + assert_redirected_to new_session_path + end + end + + private + + def stubbed_saml_response(valid: true, &block) + saml_response = Minitest::Mock.new + saml_response.expect(:settings=, true, [OneLogin::RubySaml::Settings]) + saml_response.expect(:is_valid?, valid) + saml_response.expect(:nameid, "testuser@example.com") + OneLogin::RubySaml::Response.stub(:new, saml_response, &block) + end +end diff --git a/test/models/saml_test.rb b/test/models/saml_test.rb new file mode 100644 index 00000000..3fc2b6bb --- /dev/null +++ b/test/models/saml_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' + +class SamlTest < ActiveSupport::TestCase + test "::configured? checks if configuration exists" do + Rails.configuration.hdm[:saml] = SAML_TEST_CONFIG.dup + assert Saml.configured? + Rails.configuration.hdm.delete(:saml) + assert_not Saml.configured? + end + + test "#settings correctly configures ruby-saml" do + Rails.configuration.hdm[:saml] = SAML_TEST_CONFIG.dup + settings = Saml.new.settings + assert_equal "hdm-test", settings.sp_entity_id + assert_equal "https://testsso", settings.idp_sso_service_url + assert_equal "test", settings.idp_cert_fingerprint + assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", settings.name_identifier_format + Rails.configuration.hdm.delete(:saml) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index eb7547ec..fc4ab6bf 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,6 +17,12 @@ end server_thread.join(1) +SAML_TEST_CONFIG = { + sp_entity_id: "hdm-test", + idp_sso_service_url: "https://testsso", + idp_cert_fingerprint: "test" +}.freeze + class ActiveSupport::TestCase # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) unless ENV["COVERAGE"]