Merge pull request #7680 from betagouv/saml-with-metadata

update saml idp.
Utilise une gem permettant l'intégration saml avec Dolist
This commit is contained in:
krichtof 2022-08-23 15:18:35 +02:00 committed by GitHub
commit 9c27128c8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 143 additions and 35 deletions

View file

@ -72,7 +72,7 @@ gem 'rake-progressbar', require: false
gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325) gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325)
gem 'rgeo-geojson' gem 'rgeo-geojson'
gem 'rqrcode' gem 'rqrcode'
gem 'ruby-saml-idp' gem 'saml_idp'
gem 'sanitize-url' gem 'sanitize-url'
gem 'sassc-rails' # Use SCSS for stylesheets gem 'sassc-rails' # Use SCSS for stylesheets
gem 'sentry-delayed_job' gem 'sentry-delayed_job'

View file

@ -633,13 +633,18 @@ GEM
ruby-graphviz (1.2.5) ruby-graphviz (1.2.5)
rexml rexml
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-saml-idp (0.3.5)
ruby-vips (2.1.4) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
ruby_parser (3.15.1) ruby_parser (3.15.1)
sexp_processor (~> 4.9) sexp_processor (~> 4.9)
rubyzip (2.3.0) rubyzip (2.3.0)
saml_idp (0.14.0)
activesupport (>= 5.2)
builder (>= 3.0)
nokogiri (>= 1.6.2)
rexml
xmlenc (>= 0.7.1)
sanitize (6.0.0) sanitize (6.0.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
@ -780,6 +785,13 @@ GEM
websocket-driver (0.7.5) websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xmlenc (0.8.0)
activemodel (>= 3.0.0)
activesupport (>= 3.0.0)
nokogiri (>= 1.6.0, < 2.0.0)
xmlmapper (>= 0.7.3)
xmlmapper (0.8.1)
nokogiri (~> 1.11)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.0) zeitwerk (2.6.0)
@ -889,7 +901,7 @@ DEPENDENCIES
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
ruby-saml-idp saml_idp
sanitize-url sanitize-url
sassc-rails sassc-rails
scss_lint scss_lint

View file

@ -1,28 +1,38 @@
class SamlIdpController < ActionController::Base class SamlIdpController < ActionController::Base
include SamlIdp::Controller include SamlIdp::Controller
before_action :validate_saml_request
def new def new
if super_admin_signed_in? if validate_saml_request
@saml_response = encode_SAMLResponse(current_super_admin.email, saml_attributes) render template: 'saml_idp/new'
render :template => "saml_idp/idp/saml_post", :layout => false
else else
redirect_to root_path, alert: t("errors.messages.saml_not_authorized") head :forbidden
end end
end end
def metadata def show
render layout: false, content_type: "application/xml", formats: :xml render xml: SamlIdp.metadata.signed
end
def create
if validate_saml_request
if super_admin_signed_in?
@saml_response = idp_make_saml_response(current_super_admin)
render template: 'saml_idp/saml_post', layout: false
else
redirect_to root_path, alert: t("errors.messages.saml_not_authorized")
end
else
head :forbidden
end
end end
private private
def saml_attributes def idp_make_saml_response(super_admin)
admin_attributes = %[<saml:AttributeStatement><saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"><saml:AttributeValue>#{current_super_admin.email}</saml:AttributeValue></saml:Attribute><saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue>ds|#{current_super_admin.id}</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>] encode_response super_admin, encryption: {
{ cert: saml_request.service_provider.cert,
issuer_uri: saml_auth_url, block_encryption: 'aes256-cbc',
attributes_provider: admin_attributes key_transport: 'rsa-oaep-mgf1p'
} }
end end
end end

View file

@ -28,4 +28,7 @@ as defined by the routes in the `admin/` namespace
<% if Rails.application.secrets.sendinblue[:enabled] && ENV["SAML_IDP_ENABLED"] == "enabled" %> <% if Rails.application.secrets.sendinblue[:enabled] && ENV["SAML_IDP_ENABLED"] == "enabled" %>
<%= link_to "Sendinblue", ENV.fetch("SENDINBLUE_LOGIN_URL"), class: "navigation__link", target: '_blank' %> <%= link_to "Sendinblue", ENV.fetch("SENDINBLUE_LOGIN_URL"), class: "navigation__link", target: '_blank' %>
<% end %> <% end %>
<% if ENV.fetch("SAML_IDP_ENABLED") == "enabled" && ENV["DOLIST_LOGIN_URL"].present? %>
<%= link_to "Dolist", ENV.fetch("DOLIST_LOGIN_URL"), class: "navigation__link", target: '_blank' %>
<% end %>
</nav> </nav>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
</head>
<body onload="document.forms[0].submit();" style="visibility:hidden;">
<%= form_tag do %>
<%= hidden_field_tag("SAMLRequest", params[:SAMLRequest]) %>
<%= hidden_field_tag("RelayState", params[:RelayState]) %>
<% end %>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
</head>
<body onload="document.forms[0].submit();" style="visibility:hidden;">
<%= form_tag(saml_acs_url) do %>
<%= hidden_field_tag("SAMLResponse", @saml_response) %>
<%= hidden_field_tag("RelayState", params[:RelayState]) %>
<%= submit_tag "Submit" %>
<% end %>
</body>
</html>

View file

@ -44,10 +44,8 @@ FOG_OPENSTACK_URL=""
FOG_OPENSTACK_REGION="" FOG_OPENSTACK_REGION=""
DS_PROXY_URL="" DS_PROXY_URL=""
# SAML Identity provider # SAML
SAML_IDP_ENABLED="disabled" SAML_IDP_ENABLED="disabled"
SAML_IDP_CERTIFICATE=""
SAML_IDP_SECRET_KEY="-----BEGIN RSA PRIVATE KEY-----\nblabla+blabla\n-----END RSA PRIVATE KEY-----\n"
# External service: authentication through France Connect # External service: authentication through France Connect
FC_PARTICULIER_ID="" FC_PARTICULIER_ID=""

View file

@ -148,3 +148,10 @@ DATAGOUV_API_KEY="thisisasecret"
DATAGOUV_API_URL="https://www.data.gouv.fr/api/1" DATAGOUV_API_URL="https://www.data.gouv.fr/api/1"
DATAGOUV_DESCRIPTIF_DEMARCHES_DATASET="datasetid" DATAGOUV_DESCRIPTIF_DEMARCHES_DATASET="datasetid"
DATAGOUV_DESCRIPTIF_DEMARCHES_RESOURCE="resourceid" DATAGOUV_DESCRIPTIF_DEMARCHES_RESOURCE="resourceid"
# SAML
SAML_IDP_CERTIFICATE="idpcertificate"
SAML_IDP_SECRET_KEY="-----BEGIN RSA PRIVATE KEY-----\nblabla+blabla\n-----END RSA PRIVATE KEY-----\n"
SAML_DOLIST_CERTIFICATE="spcertificate"
SAML_DOLIST_HOST="dolisthoname"
DOLIST_LOGIN_URL="https://clientpreprod.dolist.net"

View file

@ -2,6 +2,30 @@
# So we fetch env var directly here # So we fetch env var directly here
if ENV['SAML_IDP_ENABLED'] == 'enabled' if ENV['SAML_IDP_ENABLED'] == 'enabled'
SamlIdp.config.x509_certificate = ENV.fetch("SAML_IDP_CERTIFICATE") SamlIdp.configure do |config|
SamlIdp.config.secret_key = ENV.fetch("SAML_IDP_SECRET_KEY") config.base_saml_location = "https://#{ENV['APP_HOST']}/saml/metadata"
config.x509_certificate = ENV.fetch("SAML_IDP_CERTIFICATE")
config.secret_key = ENV.fetch("SAML_IDP_SECRET_KEY")
config.name_id.formats = {
"1.1" => {
email_address: -> (principal) { principal.email }
},
"2.0" => {
transient: -> (principal) { principal.email },
persistent: -> (p) { p.id }
}
}
service_providers = {
"https://#{ENV.fetch('SAML_DOLIST_HOST')}" => {
response_hosts: [ENV.fetch('SAML_DOLIST_HOST')],
cert: ENV.fetch("SAML_DOLIST_CERTIFICATE")
}
}
config.service_provider.finder = -> (entity_id) do
service_providers[entity_id]
end
end
end end

View file

@ -3,7 +3,7 @@ Rails.application.routes.draw do
get '/saml/auth' => 'saml_idp#new' get '/saml/auth' => 'saml_idp#new'
post '/saml/auth' => 'saml_idp#create' post '/saml/auth' => 'saml_idp#create'
get '/saml/metadata' => 'saml_idp#metadata' get '/saml/metadata' => 'saml_idp#show'
# #
# Manager # Manager

View file

@ -1,23 +1,50 @@
describe SamlIdpController do describe SamlIdpController do
before do
allow_any_instance_of(SamlIdpController).to receive(:validate_saml_request).and_return(valid_saml_request)
end
describe '#new' do describe '#new' do
let(:action) { get :new } let(:action) { get :new }
context 'without superadmin connected' do context 'with invalid saml request' do
it { expect(action).to redirect_to root_path } let(:valid_saml_request) { false }
it { expect(action).to have_http_status(:forbidden) }
it "display alert" do
action
expect(flash[:alert]).to eq("Vous nêtes pas autorisé à accéder à ce service.")
end
end end
context 'with superadmin connected' do context 'with valid saml request' do
let(:superadmin) { create(:super_admin) } let(:valid_saml_request) { true }
before { sign_in superadmin }
it 'encode saml response' do it { expect(action).to have_http_status(:ok) }
expect(subject).to receive(:encode_SAMLResponse).with(superadmin.email, anything()) end
action end
describe '#create' do
let(:action) { post :create }
context 'with invalid saml request' do
let(:valid_saml_request) { false }
it { expect(action).to have_http_status(:forbidden) }
end
context 'with valid saml request' do
let(:valid_saml_request) { true }
context 'without superadmin connected' do
it { expect(action).to redirect_to root_path }
it "display alert" do
action
expect(flash[:alert]).to eq("Vous nêtes pas autorisé à accéder à ce service.")
end
end
context 'with superadmin connected' do
let(:superadmin) { create(:super_admin) }
before { sign_in superadmin }
it 'encode saml response' do
expect(subject).to receive(:idp_make_saml_response).with(superadmin)
action
end
end end
end end
end end