1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

(feat) cache auth provider config

This commit is contained in:
Sylvain 2023-03-29 18:01:16 +02:00
parent 41ed9b93f5
commit 66f740104a
19 changed files with 258 additions and 130 deletions

View File

@ -1,5 +1,6 @@
# Ignore bundler config.
config/database.yml
config/auth_provider.yml
# Ignore database files.
postgresql

1
.gitignore vendored
View File

@ -26,6 +26,7 @@
# Ignore application configurations
/config/application.yml
/config/database.yml
/config/auth_provider.yml
.env
*.DS_Store

View File

@ -22,8 +22,10 @@
- Fill the holes in the logical sequence of invoices references with nil invoices
- Updated the invoices chaining method with a more flexible model
- Fix a bug: broken display after a plan category was deleted
- [TODO DEPLOY] `rails fablab:restore_order_number` THEN `rails fablab:fix_references`
- Fix a security issue: updated json5 to 2.2.2 to fix [CVE-2022-46175](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-46175)
- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/mount-auth-provider.sh | bash`
- [TODO DEPLOY] `rails fablab:auth:write_provider`
- [TODO DEPLOY] `rails fablab:restore_order_number` THEN `rails fablab:fix_references`
## v5.9.1 2023 March 22

View File

@ -2,13 +2,12 @@
# Devise controller for handling client sessions
class SessionsController < Devise::SessionsController
def new
active_provider = AuthProvider.active
if active_provider.providable_type != DatabaseProvider.name
redirect_post "/users/auth/#{active_provider.strategy_name}", params: { authenticity_token: form_authenticity_token }
else
active_provider = Rails.configuration.auth_provider
if active_provider.providable_type == 'DatabaseProvider'
super
else
redirect_post "/users/auth/#{active_provider.strategy_name}", params: { authenticity_token: form_authenticity_token }
end
end
end

View File

@ -5,7 +5,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
require 'sso_logger'
logger = SsoLogger.new
active_provider = AuthProvider.active
active_provider = Rails.configuration.auth_provider
define_method active_provider.strategy_name do
logger.info "[Users::OmniauthCallbacksController##{active_provider.strategy_name}] initiated"
if request.env['omniauth.params'].blank?
@ -18,7 +18,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
logger.debug 'trying to create a new user'
# If the username is mapped, we just check its uniqueness as it would break the postgresql
# unique constraint otherwise. If the name is not unique, another unique is generated
if active_provider.sso_fields.include?('user.username')
if active_provider.db.sso_fields.include?('user.username')
logger.debug 'the username was already in use, generating a new one'
@user.username = generate_unique_username(@user.username)
end
@ -26,7 +26,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# unique random string, because:
# - if it is the same user, his email will be filled from the SSO when he merge his accounts
# - if it is not the same user, this will prevent the raise of PG::UniqueViolation
if active_provider.sso_fields.include?('user.email') && email_exists?(@user.email)
if active_provider.db.sso_fields.include?('user.email') && email_exists?(@user.email)
logger.debug 'the email was already in use, marking it as duplicate'
old_mail = @user.email
@user.email = "<#{old_mail}>#{Devise.friendly_token}-duplicate"
@ -46,13 +46,13 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
@user.email = User.find(@user.id).email
end
end
# For users imported from the SSO, we consider the SSO as a source of trust so the email is automatically validated
@user.confirmed_at = Time.current if active_provider.db.sso_fields.include?('user.email') && !email_exists?(@user.email)
# We BYPASS THE VALIDATION because, in case of a new user, we want to save him anyway, we'll ask him later to complete his profile (on first login).
# In case of an existing user, we trust the SSO validation as we want the SSO to have authority on users management and policy.
logger.debug 'saving the user'
unless @user.save(validate: false)
logger.error "unable to save the user, an error occurred : #{@user.errors.full_messages.join(', ')}"
end
logger.error "unable to save the user, an error occurred : #{@user.errors.full_messages.join(', ')}" unless @user.save(validate: false)
logger.debug 'signing-in the user and redirecting'
sign_in_and_redirect @user, event: :authentication # this will throw if @user is not activated
@ -77,7 +77,6 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
raise e
end
end
end
private

View File

@ -11,6 +11,10 @@ class AuthProvider < ApplicationRecord
def name
'DatabaseProvider::SimpleAuthProvider'
end
def strategy_name
"database-#{name.downcase.parameterize}"
end
end
PROVIDABLE_TYPES = %w[DatabaseProvider OAuth2Provider OpenIdConnectProvider].freeze

View File

@ -7,8 +7,8 @@ module SingleSignOnConcern
included do
# enable OmniAuth authentication only if needed
devise :omniauthable, omniauth_providers: [AuthProvider.active.strategy_name.to_sym] unless
AuthProvider.active.providable_type == DatabaseProvider.name
devise :omniauthable, omniauth_providers: [Rails.configuration.auth_provider.strategy_name.to_sym] unless
Rails.configuration.auth_provider.providable_type == 'DatabaseProvider'
## Retrieve the requested data in the User and user's Profile tables
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
@ -39,7 +39,7 @@ module SingleSignOnConcern
## link the current user to the given provider (omniauth attributes hash)
## and remove the auth_token to mark his account as "migrated"
def link_with_omniauth_provider(auth)
active_provider = AuthProvider.active
active_provider = Rails.configuration.auth_provider
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
if User.where(provider: auth.provider, uid: auth.uid).size.positive?
@ -104,7 +104,7 @@ module SingleSignOnConcern
def from_omniauth(auth)
logger = SsoLogger.new
logger.debug "[User::from_omniauth] initiated with parameter #{auth}"
active_provider = AuthProvider.active
active_provider = Rails.configuration.auth_provider
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
where(provider: auth.provider, uid: auth.uid).first_or_create.tap do |user|

View File

@ -16,20 +16,4 @@ class OpenIdConnectProvider < ApplicationRecord
validates :display, inclusion: { in: %w[page popup touch wap], allow_nil: true }
validates :prompt, inclusion: { in: %w[none login consent select_account], allow_nil: true }
validates :client_auth_method, inclusion: { in: %w[basic jwks] }
def scope
self[:scope]&.join(' ')
end
def config
OpenIdConnectProvider.columns.map(&:name).filter { |n| !n.start_with?('client__') && n != 'profile_url' }.map do |n|
[n, send(n)]
end.push(['client_options', client_config]).to_h
end
def client_config
OpenIdConnectProvider.columns.map(&:name).filter { |n| n.start_with?('client__') }.to_h do |n|
[n.sub('client__', ''), send(n)]
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
json.partial! 'api/auth_providers/auth_provider', auth_provider: provider
# OAuth 2.0
if provider.providable_type == 'OAuth2Provider'
json.providable_attributes do
json.extract! provider.providable, :id, :base_url, :token_endpoint, :authorization_endpoint, :profile_url, :client_id, :client_secret,
:scopes
end
end
if provider.providable_type == 'OpenIdConnectProvider'
json.providable_attributes do
json.extract! provider.providable, :id, :issuer, :discovery, :client_auth_method, :scope, :response_type, :response_mode,
:display, :prompt, :send_scope_to_token_endpoint, :client__identifier, :client__secret, :client__authorization_endpoint,
:client__token_endpoint, :client__userinfo_endpoint, :client__jwks_uri, :client__end_session_endpoint, :profile_url,
:post_logout_redirect_uri, :uid_field, :client__redirect_uri, :client__scheme, :client__host, :client__port
end
end

View File

@ -76,6 +76,9 @@ class FabManager::Application < Rails::Application
# disable ANSI color escape codes in active_record if NO_COLOR is defined.
config.colorize_logging = ENV['NO_COLOR'] ? false : true
require 'provider_config'
config.auth_provider = ProviderConfig.new
FabManager.activate_plugins!
config.action_view.sanitized_allowed_tags = %w[a acronym hr pre table b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p

View File

@ -227,17 +227,23 @@ Devise.setup do |config|
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo'
Rails.application.reloader.to_prepare do
active_provider = AuthProvider.active
if active_provider.providable_type == OAuth2Provider.name
active_provider = Rails.configuration.auth_provider
unless active_provider.nil?
# noinspection RubyCaseWithoutElseBlockInspection
case active_provider.providable_type
when 'OAuth2Provider'
require_relative '../../lib/omni_auth/oauth2'
config.omniauth OmniAuth::Strategies::SsoOauth2Provider.name.to_sym,
config.omniauth active_provider.strategy_name.to_sym,
active_provider.providable.client_id,
active_provider.providable.client_secret
elsif active_provider.providable_type == OpenIdConnectProvider.name
active_provider.providable.client_secret,
strategy_class: OmniAuth::Strategies::SsoOauth2Provider
when 'OpenIdConnectProvider'
require_relative '../../lib/omni_auth/openid_connect'
config.omniauth OmniAuth::Strategies::SsoOpenidConnectProvider.name.to_sym,
active_provider.providable.config
config.omniauth active_provider.strategy_name.to_sym,
active_provider.oidc_config.merge(
strategy_class: OmniAuth::Strategies::SsoOpenidConnectProvider
)
end
end

View File

@ -4,7 +4,7 @@ require 'sidekiq_unique_jobs/web'
require 'sidekiq-scheduler/web'
Rails.application.routes.draw do
if AuthProvider.active.providable_type == DatabaseProvider.name
if Rails.configuration.auth_provider.providable_type == 'DatabaseProvider'
# with local authentication we do not use omniAuth so we must differentiate the config
devise_for :users, controllers: {
registrations: 'registrations', sessions: 'sessions', confirmations: 'confirmations', passwords: 'passwords'

View File

@ -249,6 +249,9 @@ unless DatabaseProvider.count.positive?
provider.providable = db_provider
provider.status = 'active'
provider.save
require 'provider_config'
ProviderConfig.write_active_provider
end
end

View File

@ -5,76 +5,74 @@ require 'jsonpath'
require 'sso_logger'
require_relative '../data_mapping/mapper'
module OmniAuth::Strategies
# Authentication strategy provided trough oAuth 2.0
class SsoOauth2Provider < OmniAuth::Strategies::OAuth2
include OmniAuth::DataMapping::Mapper
# Authentication strategy provided trough oAuth 2.0
class OmniAuth::Strategies::SsoOauth2Provider < OmniAuth::Strategies::OAuth2
include OmniAuth::DataMapping::Mapper
def self.active_provider
active_provider = AuthProvider.active
if active_provider.providable_type != OAuth2Provider.name
raise "Trying to instantiate the wrong provider: Expected OAuth2Provider, received #{active_provider.providable_type}"
end
active_provider
def self.active_provider
active_provider = Rails.configuration.auth_provider
if active_provider.providable_type != 'OAuth2Provider'
raise "Trying to instantiate the wrong provider: Expected OAuth2Provider, received #{active_provider.providable_type}"
end
# Strategy name.
option :name, active_provider.strategy_name
active_provider
end
option :client_options,
site: active_provider.providable.base_url,
authorize_url: active_provider.providable.authorization_endpoint,
token_url: active_provider.providable.token_endpoint
# Strategy name.
option :name, active_provider.strategy_name
def authorize_params
super.tap do |params|
params[:scope] = OmniAuth::Strategies::SsoOauth2Provider.active_provider.providable.scopes
end
end
option :client_options,
site: active_provider.providable.base_url,
authorize_url: active_provider.providable.authorization_endpoint,
token_url: active_provider.providable.token_endpoint
def callback_url
url = Rails.application.config.action_controller.default_url_options
"#{url[:protocol]}://#{url[:host]}#{script_name}#{callback_path}"
end
uid { parsed_info[:'user.uid'] }
info do
{
mapping: parsed_info
}
end
extra do
{
raw_info: raw_info
}
end
# retrieve data from various url, querying each only once
def raw_info
logger = SsoLogger.new
@raw_info ||= {}
logger.debug "[raw_info] @raw_infos = #{@raw_info&.to_json}"
unless @raw_info.size.positive?
OmniAuth::Strategies::SsoOauth2Provider.active_provider.auth_provider_mappings.each do |mapping|
logger.debug "mapping = #{mapping&.to_json}"
next if @raw_info.key?(mapping.api_endpoint.to_sym)
logger.debug "api_endpoint = #{mapping.api_endpoint.to_sym}"
logger.debug "access_token = #{access_token&.to_json}"
logger.debug "token get = #{access_token.get(mapping.api_endpoint)}"
logger.debug "parsed = #{access_token.get(mapping.api_endpoint).parsed}"
@raw_info[mapping.api_endpoint.to_sym] = access_token.get(mapping.api_endpoint).parsed
end
end
@raw_info
end
def parsed_info
mapped_info(OmniAuth::Strategies::SsoOauth2Provider.active_provider.auth_provider_mappings, raw_info)
def authorize_params
super.tap do |params|
params[:scope] = OmniAuth::Strategies::SsoOauth2Provider.active_provider.providable.scopes
end
end
def callback_url
url = Rails.application.config.action_controller.default_url_options
"#{url[:protocol]}://#{url[:host]}#{script_name}#{callback_path}"
end
uid { parsed_info[:'user.uid'] }
info do
{
mapping: parsed_info
}
end
extra do
{
raw_info: raw_info
}
end
# retrieve data from various url, querying each only once
def raw_info
logger = SsoLogger.new
@raw_info ||= {}
logger.debug "[raw_info] @raw_infos = #{@raw_info&.to_json}"
unless @raw_info.size.positive?
OmniAuth::Strategies::SsoOauth2Provider.active_provider.auth_provider_mappings.each do |mapping|
logger.debug "mapping = #{mapping&.to_json}"
next if @raw_info.key?(mapping.api_endpoint.to_sym)
logger.debug "api_endpoint = #{mapping.api_endpoint.to_sym}"
logger.debug "access_token = #{access_token&.to_json}"
logger.debug "token get = #{access_token.get(mapping.api_endpoint)}"
logger.debug "parsed = #{access_token.get(mapping.api_endpoint).parsed}"
@raw_info[mapping.api_endpoint.to_sym] = access_token.get(mapping.api_endpoint).parsed
end
end
@raw_info
end
def parsed_info
mapped_info(OmniAuth::Strategies::SsoOauth2Provider.active_provider.auth_provider_mappings, raw_info)
end
end

View File

@ -3,34 +3,32 @@
require 'omniauth_openid_connect'
require_relative '../data_mapping/mapper'
module OmniAuth::Strategies
# Authentication strategy provided trough OpenID Connect
class SsoOpenidConnectProvider < OmniAuth::Strategies::OpenIDConnect
include OmniAuth::DataMapping::Mapper
# Authentication strategy provided trough OpenID Connect
class OmniAuth::Strategies::SsoOpenidConnectProvider < OmniAuth::Strategies::OpenIDConnect
include OmniAuth::DataMapping::Mapper
def self.active_provider
active_provider = AuthProvider.active
if active_provider.providable_type != OpenIdConnectProvider.name
raise "Trying to instantiate the wrong provider: Expected OpenIdConnectProvider, received #{active_provider.providable_type}"
end
active_provider
def self.active_provider
active_provider = Rails.configuration.auth_provider
if active_provider.providable_type != 'OpenIdConnectProvider'
raise "Trying to instantiate the wrong provider: Expected OpenIdConnectProvider, received #{active_provider.providable_type}"
end
# Strategy name.
option :name, active_provider.strategy_name
active_provider
end
info do
{
mapping: parsed_info
}
end
# Strategy name.
option :name, active_provider.strategy_name
def parsed_info
mapped_info(
OmniAuth::Strategies::SsoOpenidConnectProvider.active_provider.auth_provider_mappings,
user_info: user_info.raw_attributes
)
end
info do
{
mapping: parsed_info
}
end
def parsed_info
mapped_info(
OmniAuth::Strategies::SsoOpenidConnectProvider.active_provider.auth_provider_mappings,
user_info: user_info.raw_attributes
)
end
end

61
lib/provider_config.rb Normal file
View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
# Deals with the yml file keeping the configuration of the current authentication provider
class ProviderConfig
def initialize
@config = YAML.safe_load_file('config/auth_provider.yml').with_indifferent_access if File.exist?('config/auth_provider.yml')
end
def db
AuthProvider.find(@config[:id])
end
def oidc_config
return nil unless @config[:providable_type] == 'OpenIdConnectProvider'
(@config[:providable_attributes].keys.filter { |n| !n.start_with?('client__') && n != 'profile_url' }.map do |n|
val = @config[:providable_attributes][n]
val.join(' ') if n == 'scope'
[n, val]
end).push(
['client_options', @config[:providable_attributes].keys.filter { |n| n.start_with?('client__') }.to_h do |n|
[n.sub('client__', ''), @config[:providable_attributes][n]]
end]
).to_h
end
def method_missing(method, *args)
return map_value(@config[method]) if @config.key?(method)
return map_value(@config["#{method}_attributes"]) if @config.key?("#{method}_attributes")
super
end
def respond_to_missing?(name)
@config.key?(name) || @config.key("#{name}_attributes")
end
def self.write_active_provider
data = ApplicationController.render(
template: 'auth_provider/provider',
locals: { provider: AuthProvider.active },
handlers: [:jbuilder],
formats: [:json]
)
file_path = Rails.root.join('config/auth_provider.yml')
File.open(file_path, File::WRONLY | File::CREAT) do |file|
file.write(JSON.parse(data).to_yaml)
end
end
private
def map_value(item)
return Struct.new(*item.symbolize_keys.keys).new(*item.values) if item.is_a?(Hash)
return item.map { |v| map_value(v) } if item.is_a?(Array)
item
end
end

View File

@ -41,6 +41,10 @@ namespace :fablab do
User.all.each(&:generate_auth_migration_token)
end
# write the configuration to file
require 'provider_config'
ProviderConfig.write_active_provider
# ask the user to restart the application
next if Rails.env.test?
@ -71,5 +75,11 @@ namespace :fablab do
task current: :environment do
puts "Current active authentication provider: #{AuthProvider.active.name}"
end
desc 'write the provider config to a configuration file'
task write_provider: :environment do
require 'provider_config'
ProviderConfig.write_active_provider
end
end
end

View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
yq() {
docker run --rm -i -v "${PWD}:/workdir" --user "$UID" mikefarah/yq:4 "$@"
}
config()
{
echo -ne "Checking user... "
if [[ "$(whoami)" != "root" ]] && ! groups | grep docker
then
echo "Please add your current user to the docker group OR run this script as root."
echo "current user is not allowed to use docker, exiting..."
exit 1
fi
SERVICE="$(yq eval '.services.*.image | select(. == "sleede/fab-manager*") | path | .[-2]' docker-compose.yml)"
echo -e "\n"
}
add_mount()
{
if [[ ! $(yq eval ".services.$SERVICE.volumes.[] | select (. == \"*auth_provider.yml\")" docker-compose.yml) ]]; then
# change docker-compose.yml permissions for fix yq can't modify file issue
chmod 666 docker-compose.yml
yq -i eval ".services.$SERVICE.volumes += [\"\./config/auth_provider.yml:/usr/src/app/auth_provider.yml\"]" docker-compose.yml
chmod 644 docker-compose.yml
fi
}
proceed()
{
config
add_mount
}
proceed "$@"

View File

@ -19,6 +19,7 @@ services:
- ./log:/var/log/supervisor
- ./plugins:/usr/src/app/plugins
- ./accounting:/usr/src/app/accounting
- ./config/auth_provider.yml:/usr/src/app/auth_provider.yml
depends_on:
- postgres
- redis