From 680ebfbdc80789dc944c9aa5a96eadec6883e453 Mon Sep 17 00:00:00 2001 From: Siddarth R Date: Tue, 24 Jul 2018 02:14:17 +0530 Subject: [PATCH 1/3] Added Necessary Fields for SAML Setup to Organisation and Updated View --- Gemfile | 4 ++- Gemfile.lock | 25 ++++++++++++++++++ app/controllers/organisations_controller.rb | 9 ++++--- app/models/organisation.rb | 17 ++++++++---- app/validators/email_validator.rb | 7 +++++ app/views/organisations/_form.html.slim | 26 ++++++++++++++++--- app/views/organisations/index.html.slim | 4 +-- config/application.rb | 1 + ...723175600_update_organisations_for_saml.rb | 15 +++++++++++ db/schema.rb | 21 ++++++++++----- spec/factories/organisations.rb | 11 ++++++-- spec/features/organisations/create_spec.rb | 9 ++++--- spec/features/organisations/list_spec.rb | 4 +-- spec/features/organisations/update_spec.rb | 7 ++--- spec/models/organisation_spec.rb | 16 ++++++------ 15 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 app/validators/email_validator.rb create mode 100644 db/migrate/20180723175600_update_organisations_for_saml.rb diff --git a/Gemfile b/Gemfile index 141615a1..e473b897 100755 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source 'https://rubygems.org' gem 'bootstrap', '~> 4.0.0' gem 'coffee-rails' +gem 'countries', require: 'countries/global' gem 'devise' gem 'figaro' gem 'font-awesome-rails' @@ -17,13 +18,14 @@ gem 'puma' gem 'rails', '4.2.8' gem 'redis' gem 'rotp' +gem 'ruby-saml', '1.8.0' +gem 'saml_idp' gem 'sass-rails' gem 'sdoc', group: :doc gem 'slim-rails' gem 'turbolinks' gem 'uglifier' gem 'whenever', require: false - # platform :jruby do # gem 'activerecord', '4.2.8' # gem 'jdbc-mysql' diff --git a/Gemfile.lock b/Gemfile.lock index 48515065..28d72a58 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,11 @@ GEM execjs coffee-script-source (1.12.2) concurrent-ruby (1.0.5) + countries (2.1.4) + i18n_data (~> 0.8.0) + money (~> 6.9) + sixarm_ruby_unaccent (~> 1.1) + unicode_utils (~> 1.4) coveralls (0.7.2) multi_json (~> 1.3) rest-client (= 1.6.7) @@ -107,6 +112,7 @@ GEM hirb (0.7.3) i18n (0.9.5) concurrent-ruby (~> 1.0) + i18n_data (0.8.0) jaro_winkler (1.5.1) jbuilder (2.7.0) activesupport (>= 4.2.0) @@ -120,6 +126,8 @@ GEM loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) + macaddr (1.7.1) + systemu (~> 2.6.2) mail (2.7.0) mini_mime (>= 0.1.1) method_source (0.9.0) @@ -130,6 +138,8 @@ GEM mini_portile2 (2.3.0) minitest (5.11.3) mock_redis (0.18.0) + money (6.12.0) + i18n (>= 0.6.4, < 1.1) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -241,7 +251,14 @@ GEM rubocop-rspec (1.27.0) rubocop (>= 0.56.0) ruby-progressbar (1.9.0) + ruby-saml (1.8.0) + nokogiri (>= 1.5.10) safe_yaml (1.0.4) + saml_idp (0.7.2) + activesupport (>= 3.2) + builder (~> 3.0) + nokogiri (>= 1.6.2) + uuid (~> 2.3) sass (3.5.6) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -266,6 +283,7 @@ GEM hirb simplecov simplecov-html (0.10.2) + sixarm_ruby_unaccent (1.2.0) slim (3.0.9) temple (>= 0.7.6, < 0.9) tilt (>= 1.3.3, < 2.1) @@ -280,6 +298,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + systemu (2.6.5) temple (0.8.0) term-ansicolor (1.2.2) tins (~> 0.8) @@ -296,6 +315,9 @@ GEM uglifier (4.1.11) execjs (>= 0.3.0, < 3) unicode-display_width (1.4.0) + unicode_utils (1.4.0) + uuid (2.3.9) + macaddr (~> 1.0) warden (1.2.7) rack (>= 1.0) web-console (3.3.0) @@ -318,6 +340,7 @@ DEPENDENCIES bootstrap (~> 4.0.0) capybara coffee-rails + countries coveralls database_cleaner devise @@ -342,6 +365,8 @@ DEPENDENCIES rspec-rails rubocop rubocop-rspec + ruby-saml (= 1.8.0) + saml_idp sass-rails sdoc shoulda-matchers diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 1f9d041e..9a15f79b 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -14,7 +14,7 @@ def create redirect_to organisations_path else flash[:errors] = org.errors.full_messages - redirect_to new_organisation_path + render :new, locals: { org: org } end end @@ -26,7 +26,7 @@ def update redirect_to organisations_path else flash[:errors] = org.errors.full_messages - redirect_to organisation_path(org) + render :show, locals: { org: org } end end @@ -43,6 +43,9 @@ def load_org end def organisation_params - params.require(:organisation).permit(:name, :url, :email_domain) + params.require(:organisation).permit( + :name, :website, :domain, :country, :state, :address, :admin_email_address, + :slug, :unit_name + ) end end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 4f20f142..ca6565df 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -1,17 +1,24 @@ class Organisation < ActiveRecord::Base - validates :name, :url, :email_domain, presence: true + validates :name, :website, :domain, :country, :state, :address, + :admin_email_address, :slug, presence: true + validates :admin_email_address, email: true + validates :slug, uniqueness: true + + UPDATE_KEYS = %w( + name website domain country state address admin_email_address slug unit_name + ).freeze def self.setup(attrs = {}) - attrs.stringify_keys! - attrs = attrs.select { |k, _v| %w(name url email_domain).include?(k) } + attrs = attrs.stringify_keys + attrs = attrs.select { |k, _v| UPDATE_KEYS.include?(k) } org = Organisation.new(attrs) org.save if org.valid? org end def update_profile(attrs = {}) - attrs.stringify_keys! - attrs = attrs.select { |k, _v| %w(name url email_domain).include?(k) } + attrs = attrs.stringify_keys + attrs = attrs.select { |k, _v| UPDATE_KEYS.include?(k) } assign_attributes(attrs) save if valid? end diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb new file mode 100644 index 00000000..d87b68e0 --- /dev/null +++ b/app/validators/email_validator.rb @@ -0,0 +1,7 @@ +class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.to_s.match?(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i) + record.errors[attribute] << (options[:message] || 'is not an email') + end + end +end diff --git a/app/views/organisations/_form.html.slim b/app/views/organisations/_form.html.slim index 2d4f0c03..954fd957 100644 --- a/app/views/organisations/_form.html.slim +++ b/app/views/organisations/_form.html.slim @@ -9,8 +9,26 @@ = f.label :name = f.text_field :name, class: 'form-control' .form-group - = f.label :url - = f.text_field :url, class: 'form-control' + = f.label :website + = f.text_field :website, class: 'form-control' .form-group - = f.label :email_domain - = f.text_field :email_domain, class: 'form-control' + = f.label :domain + = f.text_field :domain, class: 'form-control' +.form-group + = f.label :country + = f.collection_select :country, Country.all.sort_by(&:name), :gec, :name, {}, class: 'form-control' +.form-group + = f.label :state + = f.text_field :state, class: 'form-control' +.form-group + = f.label :address + = f.text_field :address, class: 'form-control' +.form-group + = f.label :admin_email_address + = f.text_field :admin_email_address, class: 'form-control' +.form-group + = f.label :slug + = f.text_field :slug, class: 'form-control' +.form-group + = f.label :unit_name + = f.text_field :unit_name, class: 'form-control' diff --git a/app/views/organisations/index.html.slim b/app/views/organisations/index.html.slim index 0d6832a7..1927d9aa 100644 --- a/app/views/organisations/index.html.slim +++ b/app/views/organisations/index.html.slim @@ -19,8 +19,8 @@ - org_list.each do |org| tr td = link_to org.name, organisation_path(org) - td = link_to org.url, org.url - td = org.email_domain + td = link_to org.website, org.website + td = org.domain - else td.text-center colspan='3' p There are no organisations yet, why don't you create an organisation diff --git a/config/application.rb b/config/application.rb index 6fdaffb0..1ef50d8c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -21,6 +21,7 @@ class Application < Rails::Application # config.i18n.default_locale = :de # Do not swallow errors in after_commit/after_rollback callbacks. + config.autoload_paths += %W["#{config.root}/app/validators/"] config.active_record.raise_in_transactional_callbacks = true Mime::Type.register "application/x-apple-aspen-config", :mobileconfig end diff --git a/db/migrate/20180723175600_update_organisations_for_saml.rb b/db/migrate/20180723175600_update_organisations_for_saml.rb new file mode 100644 index 00000000..ec012449 --- /dev/null +++ b/db/migrate/20180723175600_update_organisations_for_saml.rb @@ -0,0 +1,15 @@ +class UpdateOrganisationsForSaml < ActiveRecord::Migration + def change + rename_column :organisations, :url, :website + rename_column :organisations, :email_domain, :domain + add_column :organisations, :country, :string + add_column :organisations, :state, :string + add_column :organisations, :address, :string + add_column :organisations, :unit_name, :string + add_column :organisations, :admin_email_address, :string + add_column :organisations, :slug, :string + add_column :organisations, :cert_fingerprint, :string + add_column :organisations, :cert_key, :text + add_column :organisations, :cert_private_key, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 33dbc5bc..751793ad 100755 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180613165050) do +ActiveRecord::Schema.define(version: 20180723175600) do create_table "access_tokens", force: :cascade do |t| t.string "hashed_token", limit: 255 @@ -114,11 +114,20 @@ add_index "ip_addresses", ["mac_address"], name: "index_ip_addresses_on_mac_address", using: :btree create_table "organisations", force: :cascade do |t| - t.string "name", limit: 255 - t.string "url", limit: 255 - t.string "email_domain", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "name", limit: 255 + t.string "website", limit: 255 + t.string "domain", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "country", limit: 255 + t.string "state", limit: 255 + t.string "address", limit: 255 + t.string "unit_name", limit: 255 + t.string "admin_email_address", limit: 255 + t.string "slug", limit: 255 + t.string "cert_fingerprint", limit: 255 + t.text "cert_key", limit: 65535 + t.text "cert_private_key", limit: 65535 end create_table "users", force: :cascade do |t| diff --git a/spec/factories/organisations.rb b/spec/factories/organisations.rb index 8610cd98..27199978 100644 --- a/spec/factories/organisations.rb +++ b/spec/factories/organisations.rb @@ -1,7 +1,14 @@ FactoryBot.define do + country = Country.find_country_by_name(Country.all.map(&:name).sort.sample) factory :organisation do + sequence(:slug) { |n| "#{Faker::Internet.slug}_#{n}" } name Faker::Company.name - url Faker::Internet.url - email_domain Faker::Internet.email.split('@').last + website Faker::Internet.url + domain Faker::Internet.email.split('@').last + country country.gec + state Faker::Address.state + address Faker::Address.street_address + unit_name 'IT' + admin_email_address Faker::Internet.email end end diff --git a/spec/features/organisations/create_spec.rb b/spec/features/organisations/create_spec.rb index 8bebd648..ba877b0d 100644 --- a/spec/features/organisations/create_spec.rb +++ b/spec/features/organisations/create_spec.rb @@ -5,9 +5,10 @@ scenario 'Create an organisation successfully' do sign_in(user) visit new_organisation_path - fill_in 'organisation_name', with: org_data[:name] - fill_in 'organisation_url', with: org_data[:url] - fill_in 'organisation_email_domain', with: org_data[:email_domain] + select Country.all.sample.name, from: 'organisation_country' + %w(name website domain state address admin_email_address slug unit_name).each do |key| + fill_in "organisation_#{key}", with: org_data[key.to_sym] + end click_button('Create Organisation') expect(current_path).to eq(organisations_path) expect(page).to have_xpath( @@ -19,7 +20,7 @@ visit new_organisation_path fill_in 'organisation_name', with: org_data[:name] click_button('Create Organisation') - expect(current_path).to eq(new_organisation_path) + expect(current_path).to eq(organisations_path) expect(page).to have_xpath( "//div[@id='organisation_form_errors']" ) diff --git a/spec/features/organisations/list_spec.rb b/spec/features/organisations/list_spec.rb index ed0aa8b7..5b1bd019 100644 --- a/spec/features/organisations/list_spec.rb +++ b/spec/features/organisations/list_spec.rb @@ -16,10 +16,10 @@ "#{table_xpath}//td/a[@href='#{organisation_path(org)}' and .='#{org.name}']" ) expect(page).to have_xpath( - "#{table_xpath}//td/a[@href='#{org.url}' and .='#{org.url}']" + "#{table_xpath}//td/a[@href='#{org.website}' and .='#{org.website}']" ) expect(page).to have_xpath( - "#{table_xpath}//td[.='#{org.email_domain}']" + "#{table_xpath}//td[.='#{org.domain}']" ) end end diff --git a/spec/features/organisations/update_spec.rb b/spec/features/organisations/update_spec.rb index dac5cc7d..1c59f404 100644 --- a/spec/features/organisations/update_spec.rb +++ b/spec/features/organisations/update_spec.rb @@ -6,9 +6,10 @@ scenario 'Create an organisation successfully' do sign_in(user) visit organisation_path(org) - fill_in 'organisation_name', with: org_data[:name] - fill_in 'organisation_url', with: org_data[:url] - fill_in 'organisation_email_domain', with: org[:email_domain] + select Country.all.sample.name, from: 'organisation_country' + %w(name website domain state address admin_email_address slug unit_name).each do |key| + fill_in "organisation_#{key}", with: org_data[key.to_sym] + end click_button('Update Organisation') expect(current_path).to eq(organisations_path) expect(page).to have_xpath( diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index 495eeabc..a3d84239 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -5,19 +5,19 @@ let(:org_data) { attributes_for(:organisation) } it 'should create organisation' do org = Organisation.setup(org_data) + org_data.each do |key, value| + expect(org.send(key.to_sym)).to eq(value) + end expect(org.persisted?).to eq(true) expect(org.valid?).to eq(true) - expect(org.name).to eq(org_data['name']) - expect(org.url).to eq(org_data['url']) - expect(org.email_domain).to eq(org_data['email_domain']) end it 'should not create organisation if validations fail' do org = Organisation.setup(name: org_data[:name]) expect(org.persisted?).to eq(false) expect(org.valid?).to eq(false) - expect(org.errors.messages.key?(:url)).to eq(true) - expect(org.errors.messages.key?(:email_domain)).to eq(true) + expect(org.errors.messages.key?(:website)).to eq(true) + expect(org.errors.messages.key?(:domain)).to eq(true) end end @@ -26,10 +26,10 @@ let(:org_data) { attributes_for(:organisation) } it 'should update organisation profile' do org.update_profile(org_data) + org_data.each do |key, value| + expect(org.send(key.to_sym)).to eq(value) + end expect(org.valid?).to eq(true) - expect(org.name).to eq(org_data['name']) - expect(org.url).to eq(org_data['url']) - expect(org.email_domain).to eq(org_data['email_domain']) end it 'shouldn not update organisation profile if validations fail' do From ca400c8a4bfb4bb40458ecb98e1482b56a033dd2 Mon Sep 17 00:00:00 2001 From: Siddarth R Date: Tue, 24 Jul 2018 03:42:41 +0530 Subject: [PATCH 2/3] Build the ability to setup SAML Certificates from the Organisations UI --- app/controllers/organisations_controller.rb | 11 +++++ app/models/organisation.rb | 42 +++++++++++++++++++ app/views/organisations/index.html.slim | 7 ++++ config/routes.rb | 4 +- spec/factories/organisations.rb | 4 +- spec/features/organisations/list_spec.rb | 3 ++ .../features/organisations/setup_saml_spec.rb | 14 +++++++ spec/models/organisation_spec.rb | 38 +++++++++++++++++ 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 spec/features/organisations/setup_saml_spec.rb diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 9a15f79b..a2293bfa 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -34,6 +34,17 @@ def show render :show, locals: { org: load_org } end + def setup_saml + org = load_org + if org.saml_setup? + flash[:errors] = 'SAML Certificates Already Setup' + else + load_org.setup_saml_certs + flash[:success] = 'Successfully setup SAML Certificates' + end + redirect_to organisations_path + end + private def load_org diff --git a/app/models/organisation.rb b/app/models/organisation.rb index ca6565df..df9ac2f7 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -1,9 +1,15 @@ class Organisation < ActiveRecord::Base validates :name, :website, :domain, :country, :state, :address, :admin_email_address, :slug, presence: true + validates :address, format: { + with: /\A[a-zA-Z0-9\s]+\z/, + message: 'Invalid - Only Alphabets, Space and Numbers Allowed', + } validates :admin_email_address, email: true validates :slug, uniqueness: true + attr_accessor :cert, :rsa_key + UPDATE_KEYS = %w( name website domain country state address admin_email_address slug unit_name ).freeze @@ -22,4 +28,40 @@ def update_profile(attrs = {}) assign_attributes(attrs) save if valid? end + + def saml_setup? + cert_fingerprint.present? && cert_key.present? && cert_private_key.present? + end + + def setup_saml_certs + return false unless persisted? + require 'openssl' + self.rsa_key = OpenSSL::PKey::RSA.new(2048) + private_key = rsa_key.to_pem + public_key = rsa_key.public_key + subject = "/C=#{country}/ST=#{state}/L=#{address}/O=#{name}/OU=#{unit_name}/CN=#{domain}" + self.cert = OpenSSL::X509::Certificate.new + cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject) + cert.not_before = Time.now + cert.not_after = Time.now + 365 * 24 * 60 * 60 + cert.public_key = public_key + cert.serial = SecureRandom.random_number(10) + cert.version = 2 + ef = OpenSSL::X509::ExtensionFactory.new + ef.subject_certificate = cert + ef.issuer_certificate = cert + cert.extensions = [ + ef.create_extension('basicConstraints', 'CA:TRUE', true), + ef.create_extension('subjectKeyIdentifier', 'hash'), + ] + cert.add_extension ef.create_extension( + 'authorityKeyIdentifier', 'keyid:always,issuer:always' + ) + cert.sign rsa_key, OpenSSL::Digest::SHA1.new + update_attributes( + cert_fingerprint: OpenSSL::Digest::SHA256.hexdigest(cert.to_der).scan(/../).join(':'), + cert_key: cert.to_pem, + cert_private_key: private_key + ) + end end diff --git a/app/views/organisations/index.html.slim b/app/views/organisations/index.html.slim index 1927d9aa..7cbd5fac 100644 --- a/app/views/organisations/index.html.slim +++ b/app/views/organisations/index.html.slim @@ -7,6 +7,9 @@ - if flash.key?(:success) .alert.alert-success#organisation_form_success = flash[:success] + - if flash.key?(:errors) + .alert.alert-danger#organisation_form_errors + = flash[:errors] #organisation_list.table-responsive table.table.table-striped thead @@ -14,6 +17,7 @@ th Name th URL th Email Domain + th Actions tbody - if org_list.present? - org_list.each do |org| @@ -21,6 +25,9 @@ td = link_to org.name, organisation_path(org) td = link_to org.website, org.website td = org.domain + td + - unless org.saml_setup? + = link_to "Setup SAML", organisation_setup_saml_path(org) - else td.text-center colspan='3' p There are no organisations yet, why don't you create an organisation diff --git a/config/routes.rb b/config/routes.rb index 4c1369d9..c168ab5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,7 +3,9 @@ devise_scope :user do authenticated :user do - resources :organisations, except: %i(destroy) + resources :organisations, except: %i(destroy) do + get 'setup_saml', to: :setup_saml + end end delete "/users/sign_out" => "devise/sessions#destroy" diff --git a/spec/factories/organisations.rb b/spec/factories/organisations.rb index 27199978..2ed010ce 100644 --- a/spec/factories/organisations.rb +++ b/spec/factories/organisations.rb @@ -2,12 +2,12 @@ country = Country.find_country_by_name(Country.all.map(&:name).sort.sample) factory :organisation do sequence(:slug) { |n| "#{Faker::Internet.slug}_#{n}" } - name Faker::Company.name + name Faker::Lorem.word website Faker::Internet.url domain Faker::Internet.email.split('@').last country country.gec state Faker::Address.state - address Faker::Address.street_address + address Faker::Lorem.words(3).join(' ') unit_name 'IT' admin_email_address Faker::Internet.email end diff --git a/spec/features/organisations/list_spec.rb b/spec/features/organisations/list_spec.rb index 5b1bd019..ec4e0426 100644 --- a/spec/features/organisations/list_spec.rb +++ b/spec/features/organisations/list_spec.rb @@ -21,6 +21,9 @@ expect(page).to have_xpath( "#{table_xpath}//td[.='#{org.domain}']" ) + expect(page).to have_xpath( + "#{table_xpath}//td/a[@href='#{organisation_setup_saml_path(org)}' and .='Setup SAML']" + ) end end scenario 'Ability to see organsiation details' do diff --git a/spec/features/organisations/setup_saml_spec.rb b/spec/features/organisations/setup_saml_spec.rb new file mode 100644 index 00000000..24444127 --- /dev/null +++ b/spec/features/organisations/setup_saml_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' +RSpec.feature 'Setup SAML', type: :feature do + let!(:org) { create(:organisation) } + let(:user) { create(:user) } + scenario 'Should show a success message if SAML is setup' do + sign_in(user) + allow_any_instance_of(Organisation).to receive(:setup_saml_certs).and_return(true) + visit organisation_setup_saml_path(org) + expect(current_path).to eq(organisations_path) + expect(page).to have_xpath( + "//div[@id='organisation_form_success' and .='Successfully setup SAML Certificates']" + ) + end +end diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index a3d84239..9f8370b7 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -38,4 +38,42 @@ expect(org.errors.messages.key?(:name)).to eq(true) end end + + describe '.setup_saml_certs' do + let(:org) { create(:organisation) } + it 'should set the subject based on organisation profile' do + org.setup_saml_certs + subject = Hash[org.cert.subject.to_a.map { |i| [i[0].to_sym, i[1]] }] + expected_subject = { + C: org.country, ST: org.state, L: org.address, O: org.name, OU: org.unit_name, + CN: org.domain + } + expect(subject).to eq(expected_subject) + end + + it 'should set the expiry of the certificate for 1 year' do + org.setup_saml_certs + Timecop.freeze(Time.now - 10.minutes) + expect(org.cert.not_before > Time.now).to eq(true) + expect(org.cert.not_after > Time.now + 365 * 24 * 60 * 60).to eq(true) + end + + it 'should update the certificate for the organisation' do + org.setup_saml_certs + fingerprint = OpenSSL::Digest::SHA256.hexdigest(org.cert.to_der).scan(/../).join(':') + private_key = org.rsa_key.to_pem + cert = org.cert.to_pem + expect(org.cert_fingerprint).to eq(fingerprint) + expect(org.cert_private_key).to eq(private_key) + expect(org.cert_key).to eq(cert) + end + end + + describe '.saml_setup?' do + let(:org) { create(:organisation) } + it 'should return true if saml is setup' do + org.setup_saml_certs + expect(org.saml_setup?).to eq(true) + end + end end From f60b2870a6899261f019fa0865965c513dd97075 Mon Sep 17 00:00:00 2001 From: Siddarth R Date: Wed, 25 Jul 2018 01:29:49 +0530 Subject: [PATCH 3/3] Added View for SAML Login and Configuration --- app/controllers/saml_idp_controller.rb | 36 ++++++++++++++++ app/models/organisation.rb | 4 ++ app/views/saml_idp/idp/new.html.erb | 58 ++++++++++++++++++++++++++ config/application.yml.sample | 1 + config/initializers/saml_idp.rb | 19 +++++++++ config/routes.rb | 9 +++- spec/models/organisation_spec.rb | 7 ++++ 7 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 app/controllers/saml_idp_controller.rb create mode 100644 app/views/saml_idp/idp/new.html.erb create mode 100644 config/initializers/saml_idp.rb diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb new file mode 100644 index 00000000..e5a4fd9c --- /dev/null +++ b/app/controllers/saml_idp_controller.rb @@ -0,0 +1,36 @@ +class SamlIdpController < SamlIdp::IdpController + layout false + before_action :setup_saml_configuration + + private + + def idp_authenticate(email, password) + User.find_and_check_user(email, password) ? User.find_active_user_by_email(email) : nil + end + + def idp_make_saml_response(found_user) + encode_response found_user + end + + def idp_logout + # user = User.by_email(saml_request.name_id) + # user.logout + end + + def setup_saml_configuration + slug = params[:slug] + org = Organisation.find_by_slug(slug) + saml_url = "#{Figaro.env.gate_url}/#{slug}/saml" + SamlIdp.configure do |config| + config.x509_certificate = org.cert_key + config.secret_key = org.cert_private_key + config.organization_name = org.name + config.organization_url = org.website + config.base_saml_location = saml_url + config.attribute_service_location = "#{saml_url}/attributes" + config.single_service_post_location = "#{saml_url}/auth" + config.single_logout_service_post_location = "#{saml_url}/logout" + config.single_logout_service_redirect_location = "#{saml_url}/logout" + end + end +end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index df9ac2f7..6d0a400e 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -14,6 +14,10 @@ class Organisation < ActiveRecord::Base name website domain country state address admin_email_address slug unit_name ).freeze + def self.find_by_slug(slug) + Organisation.where(slug: slug).first + end + def self.setup(attrs = {}) attrs = attrs.stringify_keys attrs = attrs.select { |k, _v| UPDATE_KEYS.include?(k) } diff --git a/app/views/saml_idp/idp/new.html.erb b/app/views/saml_idp/idp/new.html.erb new file mode 100644 index 00000000..281636f2 --- /dev/null +++ b/app/views/saml_idp/idp/new.html.erb @@ -0,0 +1,58 @@ + + + + + + GoJek – Single Sign on + + + + + +
+
+
+ <%= image_tag("logo.png") %> +

Single Sign-on Multifactor Authentication.

+
+ + <%= form_tag auth_path, class: 'text-left' do %> + <%= hidden_field_tag("SAMLRequest", params[:SAMLRequest]) %> + <%= hidden_field_tag("RelayState", params[:RelayState]) %> +
+ <%= label_tag :email %> + <%= email_field_tag :email, params[:email], :autocapitalize => "off", :autocorrect => "off", :autofocus => "autofocus", :spellcheck => "false", :size => 30, :class => "email_pwd txt form-control" %> +
+
+ <%= label_tag "Google Authenticator Code" %> + <%= password_field_tag :password, params[:password], :autocapitalize => "off", :autocorrect => "off", :spellcheck => "false", :size => 30, :class => "email_pwd txt form-control" %> +
+ <%= submit_tag "Sign in", :class => "button big blueish btn btn-primary" %> + <% end %> +
+
+
+
+ + diff --git a/config/application.yml.sample b/config/application.yml.sample index a672f13d..c6aa7e3a 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -14,3 +14,4 @@ GATE_DB_USER: '' GATE_DB_PASSWORD: '' DEFAULT_HOST_PATTERN: 's*' UID_BUFFER: 5000 +GATE_URL: 'https://gate.gojek.co.id/' diff --git a/config/initializers/saml_idp.rb b/config/initializers/saml_idp.rb new file mode 100644 index 00000000..d5686606 --- /dev/null +++ b/config/initializers/saml_idp.rb @@ -0,0 +1,19 @@ +SamlIdp.configure do |config| + config.session_expiry = 86400 + config.name_id.formats = { + email_address: -> (principal) { principal.email_address }, + transient: -> (principal) {principal.user_login_id}, + persistent: -> (principal) {principal.user_login_id}, + name: -> (principal) {principal.name}, + } + config.attributes = { + 'eduPersonPrincipalName' => { + 'name' => 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', + 'name_format' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', + 'getter' => ->(principal) { "#{principal.email}" } + }, + EmailAddress: { getter: :email_address }, + FirstName: { getter: :name }, + LastName: { getter: :name } + } +end diff --git a/config/routes.rb b/config/routes.rb index c168ab5b..6b84dc2c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,17 @@ Rails.application.routes.draw do devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }, :path_names => { :sign_in => 'login', :sign_out => 'logout' } + scope '/:slug/saml' do + get '/auth' => 'saml_idp#new' + get '/metadata' => 'saml_idp#show' + post '/auth' => 'saml_idp#create' + match '/logout' => 'saml_idp#logout', via: [:get, :post, :delete] + end + devise_scope :user do authenticated :user do resources :organisations, except: %i(destroy) do - get 'setup_saml', to: :setup_saml + get 'setup_saml', action: :setup_saml end end diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index 9f8370b7..e2417725 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -1,6 +1,13 @@ require 'rails_helper' RSpec.describe Organisation, type: :model do + describe '.find_by_slug' do + let(:org) { create(:organisation) } + it 'should return organisation based on slug' do + expect(Organisation.find_by_slug(org.slug)).to eq(org) + end + end + describe '.setup' do let(:org_data) { attributes_for(:organisation) } it 'should create organisation' do