Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem "rubocop-rails-omakase", require: false, group: [ :development ]

gem 'simplecov', require: false, group: :test
gem "ruby-saml", "~> 1.15"
gem "devise_saml_authenticatable", "~> 1.9"
gem "redis", "~> 5.0" # Redis client for Ruby
gem "redis-actionpack", "~> 5.3" # Redis session store for ActionPack
32 changes: 32 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ GEM
devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
devise_saml_authenticatable (1.9.1)
devise (> 2.0.0)
ruby-saml (~> 1.7)
docile (1.4.1)
doorkeeper (5.9.2)
railties (>= 5)
Expand Down Expand Up @@ -243,12 +246,26 @@ GEM
erb
psych (>= 4.0.0)
tsort
redis (5.4.1)
redis-client (>= 0.22.0)
redis-actionpack (5.5.0)
actionpack (>= 5)
redis-rack (>= 2.1.0, < 4)
redis-store (>= 1.1.0, < 2)
redis-client (0.26.2)
connection_pool
redis-rack (3.0.0)
rack-session (>= 0.2.0)
redis-store (>= 1.2, < 2)
redis-store (1.11.0)
redis (>= 4, < 6)
regexp_parser (2.12.0)
reline (0.6.3)
io-console (~> 0.5)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
rexml (3.4.4)
rubocop (1.87.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
Expand Down Expand Up @@ -278,6 +295,9 @@ GEM
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-progressbar (1.13.0)
ruby-saml (1.18.1)
nokogiri (>= 1.13.10)
rexml
securerandom (0.4.1)
simplecov (0.22.0)
docile (~> 1.1)
Expand Down Expand Up @@ -321,6 +341,7 @@ DEPENDENCIES
devise-i18n
devise-security
devise_ldap_authenticatable
devise_saml_authenticatable (~> 1.9)
doorkeeper
doorkeeper-i18n
dotenv-rails
Expand All @@ -334,9 +355,12 @@ DEPENDENCIES
rack-cors
rails (~> 8.0.2)
rails-i18n
redis (~> 5.0)
redis-actionpack (~> 5.3)
rubocop
rubocop-rails
rubocop-rails-omakase
ruby-saml (~> 1.15)
simplecov
tzinfo-data

Expand Down Expand Up @@ -371,6 +395,7 @@ CHECKSUMS
devise-i18n (1.16.0) sha256=a33306f90f317cfe92753a000064e3ba9dec3cab2d5d01ef40d30f4b6e24e753
devise-security (0.18.0) sha256=fc06be1624b5151044ff9c5d8e61abdfa7d56eb16bfdaec16a11235d54708513
devise_ldap_authenticatable (0.8.7) sha256=8af6f839661e24ca9afc5a1508a7ec7e1327e93af4516f2baabacdf511ee5a2e
devise_saml_authenticatable (1.9.1) sha256=347c42e282f1d75b8aacb1537934f92024f79ddf2b50f199911ad8472e8cfa61
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
doorkeeper (5.9.2) sha256=cd4a43e0085f12afe36a8f0fc59d7f7d9ed2cb9d5c3d8a00cb9d5d845622d3f7
doorkeeper-i18n (5.2.9) sha256=dea73afae44ffbf3bf7f2921b24ca801ee199d931591f15e24cb07af9c45b4a4
Expand Down Expand Up @@ -431,15 +456,22 @@ CHECKSUMS
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
redis-actionpack (5.5.0) sha256=dc0570b78c14ec62b35c17b97fab778ee5986bc55e695bfb6826488088693311
redis-client (0.26.2) sha256=1336fb5a7202d398b719531853c184b7c9cbdcace1f00f8356062b9dfba6779b
redis-rack (3.0.0) sha256=abb50b82ae10ad4d11ca2e4901bfc2b98256cdafbbd95f80c86fc9e001478380
redis-store (1.11.0) sha256=edc4f3e239dcd1fdd9905584e6b1e623a84618e14436e6e8a07c70891008eda4
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
responders (3.2.0) sha256=89c2d6ac0ae16f6458a11524cae4a8efdceba1a3baea164d28ee9046bd3df55a
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rubocop (1.87.0) sha256=b9d9ddf55116a513f8ef2c7ae660662d8b49301f118d3f0df61865b33a5c188d
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
rubocop-rails (2.35.4) sha256=3aeaa325439c89950e8327565682ea794065d08e2ecbbfe95032bfa295a35df5
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
ruby-saml (1.18.1) sha256=1b0e7a44aef150b4197955f5e015d593672e242cfdc5d06aa7554ec2350b9107
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
Expand Down
8 changes: 6 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ class ApplicationController < ActionController::API
before_action :active_storage_url_options

def info
client_app = Doorkeeper::Application.find_by(uid: params["client_id"], secret: params["client_secret"])
render json: { valid: client_app.present?, auth: ENV['ENABLE_AUTHENTICATION'].present? }
client_app = Doorkeeper::Application.find_by(uid: params['client_id'], secret: params['client_secret'])
render json: {
valid: client_app.present?,
auth: ENV['ENABLE_AUTHENTICATION'].present?,
sso_enabled: ENV['ENABLE_SSO'].present? ? ActiveModel::Type::Boolean.new.cast(ENV['ENABLE_SSO']) : false
}
end

protected
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/pias_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def index
res = []
# check if user is technical else his pias
pias = if ENV['ENABLE_AUTHENTICATION'].blank? || current_user.is_technical_admin
Pia.all
Pia.eager_load(:user_pias)
.all
else
policy_scope(Pia)
end
Expand Down
104 changes: 104 additions & 0 deletions app/controllers/saml_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
class SamlController < Doorkeeper::TokensController
# skip_before_action :doorkeeper_authorize!

def metadata
meta = OneLogin::RubySaml::Metadata.new
render xml: meta.generate(settings, true)
end

def sso
request = OneLogin::RubySaml::Authrequest.new
redirect_to(request.create(settings), allow_other_host: true)
end

def consume
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings:)

if response.is_valid?
email = response.name_id
session[:nameid] = response.name_id
user = User.find_by("LOWER(email) = ?", email.strip.downcase)
if user
user.unlock_access!
else
password = [*'0'..'9', *'a'..'z', *'A'..'Z', *'!'..'?'].sample(16).join
user = User.create!(email:, password:, password_confirmation: password)
Comment thread Fixed
Comment thread Fixed
Comment thread Fixed
Comment thread Fixed
Comment thread Dismissed
Comment thread Dismissed
user.is_user = true
user.unlock_access!
user.save
end
sign_in(:user, user)

doorkeeper_app = Doorkeeper::Application.first
access_token = Doorkeeper::AccessToken.find_or_create_for(
application: doorkeeper_app,
resource_owner: user.id,
scopes: Doorkeeper::OAuth::Scopes.from_array(%w[public])
)

# redirect_to frontrnd
redirect_to "#{ENV['SSO_FRONTEND_REDIRECTION']}/#/?sso_token=#{access_token.token}", allow_other_host: true
else
logger.info "Response Invalid. Errors: #{response.errors}"
@errors = response.errors
redirect_to ENV['SSO_FRONTEND_REDIRECTION'], allow_other_host: true
end
end

def logout
logout_request = OneLogin::RubySaml::Logoutrequest.new
session[:transaction_id] = logout_request.uuid

logger.info "New SP SLO for User ID: '#{session[:nameid]}', Transaction ID: '#{session[:transaction_id]}'"

settings.name_identifier_value = session[:nameid] if settings.name_identifier_value.nil?

redirect_to(logout_request.create(settings), allow_other_host: true)
end

# Handle the SLO response from the IdP
# GET /saml/slo
def slo
logout_response = if session.has_key? :transaction_id
OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings,
matches_request_id: session[:transaction_id])
else
OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings)
end
logger.info "LogoutResponse is: #{logout_response}"

# Validate the SAML Logout Response
if !logout_response.validate
logger.error 'The SAML Logout Response is invalid'
else
# Actually log out this session
logger.info "SLO completed for '#{session[:nameid]}'"
session[:nameid] = nil
session[:transaction_id] = nil

redirect_to ENV['SSO_FRONTEND_REDIRECTION'], allow_other_host: true
end
end

private

def settings
settings = OneLogin::RubySaml::Settings.new
url_base = "#{request.protocol}#{request.host_with_port}"

# settings.soft = true
settings.issuer = "#{url_base}/saml/metadata"
settings.assertion_consumer_service_url = "#{url_base}/saml/acs"
settings.assertion_consumer_logout_service_url = "#{url_base}/saml/slo"

# IdP section
settings.idp_entity_id = ENV['IDP_ENTITY_ID']
settings.idp_sso_target_url = ENV['IDP_SSO_TARGET_URL']
settings.idp_slo_target_url = ENV['IDP_SLO_TARGET_URL']
settings.idp_cert_fingerprint = ENV['IDP_CERT_FINGERPRINT']
settings.idp_cert_fingerprint_algorithm = ENV['IDP_CERT_FINGERPRINT_ALGORITHM']
settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'

settings
end
end
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class User < ApplicationRecord
dependent: :destroy

def validate_login_uniqueness
errors.add(:login, :taken) if User.where(login: login).where.not(id: id).exists?
errors.add(:login, :taken) if User.where(login:).where.not(id:).exists?
Comment thread
brunto marked this conversation as resolved.
end

def check_ldap_email
Expand Down
6 changes: 4 additions & 2 deletions app/policies/pia_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ def initialize(user, scope)

def resolve
if user.present? && user.is_functional_admin?
scope.all
scope.eager_load(:user_pias)
.all
else
scope.joins(:user_pias).merge(UserPia.where(user_id: user.id))
scope.eager_load(:user_pias)
.merge(UserPia.where(user_id: user.id))
end
end
end
Expand Down
19 changes: 18 additions & 1 deletion config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
# session. If you need permissions, you should implement that in a before filter.
# You can also supply a hash where the value is a boolean determining whether
# or not authentication should be aborted when the value is not present.
# config.authentication_keys = [:email]
config.authentication_keys = [:email]

# Configure parameters from the request object used for authentication. Each entry
# given should be a request method and it will automatically be passed to the
Expand Down Expand Up @@ -320,4 +320,21 @@
# When set to false, does not sign a user in automatically after their password is
# changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true

# # config.saml_default_user_key = :email # or whatever attribute you want to use as the user identifier
# config.saml_create_user = true # Automatically create users
# config.saml_update_user = true # Update user attributes after login
# config.saml_configure do |saml|
# url_base = "#{ENV['SAML_URL_BASE']}"
# saml.issuer = "#{url_base}/saml/metadata"
# saml.assertion_consumer_service_url = "#{url_base}/saml/acs"
# saml.assertion_consumer_logout_service_url = "#{url_base}/saml/logout"

# # IdP section
# saml.idp_entity_id = ENV['IDP_ENTITY_ID']
# saml.idp_sso_target_url = ENV['IDP_SSO_TARGET_URL']
# saml.idp_slo_target_url = ENV['IDP_SLO_TARGET_URL']
# saml.idp_cert = ENV['IDP_CERT']
# saml.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
# end
end
12 changes: 12 additions & 0 deletions config/initializers/session_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Rails.application.config.session_store :redis_store,
url: "#{ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379')}/0/session",
expire_after: 30.days,
key: "_piaback_session_#{Rails.env}",
domain: ENV.fetch("DOMAIN_NAME", "localhost"),
threadsafe: true,
secure: Rails.env.production?,
same_site: :lax,
httponly: true

Rails.application.config.middleware.use ActionDispatch::Cookies
Rails.application.config.middleware.use Rails.application.config.session_store, Rails.application.config.session_options
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@
resources :knowledge_bases do
resources :knowledges
end

get '/saml/metadata', to: 'saml#metadata'
get '/saml/sso', to: 'saml#sso'
get '/saml/logout', to: 'saml#logout'
post '/saml/acs', to: 'saml#consume'
get '/saml/slo', to: 'saml#slo'
end