Merge branch 'features/sso-improvements' into develop

This commit is contained in:
Andrej 2024-04-18 15:58:17 +02:00
commit c740452086
21 changed files with 467 additions and 64 deletions

View file

@ -4,6 +4,7 @@ source 'http://rubygems.org'
ruby '3.2.2'
gem 'activerecord-session_store'
gem 'bootsnap', require: false
gem 'devise', '~> 4.8.1'
gem 'devise_invitable'
@ -24,7 +25,9 @@ gem 'omniauth', '~> 2.1'
gem 'omniauth-azure-activedirectory-v2'
gem 'omniauth-linkedin-oauth2'
gem 'omniauth-okta', git: 'https://github.com/scinote-eln/omniauth-okta', branch: 'org_auth_server_support'
gem 'omniauth_openid_connect'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml'
# Gems for API implementation
gem 'active_model_serializers', '~> 0.10.7'
@ -91,7 +94,7 @@ gem 'graphviz'
gem 'cssbundling-rails'
gem 'jsbundling-rails'
gem 'tailwindcss-rails', '~> 2.0'
gem 'tailwindcss-rails', '~> 2.4'
gem 'base62' # Used for smart annotations
gem 'newrelic_rpm'

View file

@ -120,6 +120,13 @@ GEM
activesupport (= 7.0.8.1)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activerecord-session_store (2.1.0)
actionpack (>= 6.1)
activerecord (>= 6.1)
cgi (>= 0.3.6)
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 4)
railties (>= 6.1)
activestorage (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
@ -141,6 +148,7 @@ GEM
railties (>= 3.1)
aspector (0.14.0)
ast (2.4.2)
attr_required (1.0.1)
auto_strip_attributes (2.6.0)
activerecord (>= 4.0)
awesome_print (1.9.2)
@ -219,6 +227,7 @@ GEM
mail
case_transform (0.2)
activesupport
cgi (0.4.1)
childprocess (4.1.0)
chunky_png (1.4.0)
coderay (1.1.3)
@ -478,6 +487,25 @@ GEM
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-saml (2.1.0)
omniauth (~> 2.0)
ruby-saml (~> 1.12)
omniauth_openid_connect (0.7.1)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
openid_connect (2.2.0)
activemodel
attr_required (>= 1.0.0)
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
net-smtp
rack-oauth2 (~> 2.2)
swd (~> 2.0)
tzinfo
validate_email
validate_url
webfinger (~> 2.0)
orm_adapter (0.5.0)
overcommit (0.60.0)
childprocess (>= 0.6.3, < 5)
@ -518,6 +546,13 @@ GEM
rack (>= 1.0, < 3)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-oauth2 (2.2.0)
activesupport
attr_required
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (3.0.6)
rack
rack-test (2.1.0)
@ -626,6 +661,9 @@ GEM
rubocop (>= 1.33.0, < 2.0)
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby-saml (1.16.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.1.4)
ffi (~> 1.12)
rubyzip (2.3.2)
@ -663,13 +701,18 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
stream (0.5.5)
swd (2.0.2)
activesupport (>= 3)
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
sys-uname (1.2.3)
ffi (~> 1.1)
tailwindcss-rails (2.0.29)
tailwindcss-rails (2.4.0)
railties (>= 6.0.0)
tailwindcss-rails (2.0.29-arm64-darwin)
tailwindcss-rails (2.4.0-arm64-darwin)
railties (>= 6.0.0)
tailwindcss-rails (2.0.29-x86_64-linux)
tailwindcss-rails (2.4.0-x86_64-linux)
railties (>= 6.0.0)
thor (1.3.1)
tilt (2.2.0)
@ -688,6 +731,12 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
uri (0.13.0)
version_gem (1.1.3)
view_component (3.9.0)
@ -696,6 +745,10 @@ GEM
method_source (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
webfinger (2.1.2)
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
@ -722,6 +775,7 @@ PLATFORMS
DEPENDENCIES
active_model_serializers (~> 0.10.7)
activerecord-import
activerecord-session_store
acts_as_list
ajax-datatables-rails (~> 0.3.1)
aspector
@ -780,6 +834,8 @@ DEPENDENCIES
omniauth-linkedin-oauth2
omniauth-okta!
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml
omniauth_openid_connect
overcommit
pg (~> 1.5)
pg_search
@ -814,7 +870,7 @@ DEPENDENCIES
simplecov
sneaky-save!
sprockets-rails
tailwindcss-rails (~> 2.0)
tailwindcss-rails (~> 2.4)
timecop
turbolinks (~> 5.2.0)
tzinfo-data

View file

@ -36,7 +36,7 @@
timeoutID = setTimeout(functionCallback, timeoutTime);
}
function toogleDocumentTitle(timeString = null) {
function toggleDocumentTitle(timeString = null) {
var sleepEmoticon = String.fromCodePoint(0x1F62A);
var originalTitle = document.title.split(sleepEmoticon).pop().trim();
@ -70,21 +70,21 @@
function reviveSession() {
$.post($('meta[name=\'revive-url\']').attr('content'));
toogleDocumentTitle();
toggleDocumentTitle();
window.localStorage.removeItem('sessionEnd');
setSessionTimeout(initializeSessionCountdown, oneSecondTimeout);
}
function initializeSessionReviveCallbacks() {
$('#session-expire').modal().off('hide.bs.modal').on('hide.bs.modal', function() {
if (sessionExpireIn() > 0) {
if (sessionExpireIn() > 0 && sessionExpireIn() < expireLimit) {
reviveSession();
}
});
// for manual page reload
$(window).off('beforeunload').on('beforeunload', function() {
if (sessionExpireIn() > 0) {
if (sessionExpireIn() > 0 && sessionExpireIn() < expireLimit) {
reviveSession();
}
});
@ -98,7 +98,7 @@
initializeSessionCountdown();
} else if (expireIn > 0 && expireIn <= expireLimit) {
timeString = newTimerStr(expireIn / 1000);
toogleDocumentTitle(timeString);
toggleDocumentTitle(timeString);
$('.expiring').text(I18n.t('devise.sessions.expire_modal.session_end_in.header', { time: timeString }));
if (!$('#session-expire').hasClass('in')) {
@ -107,7 +107,7 @@
setSessionTimeout(sessionCountdown, oneSecondTimeout);
} else if (expireIn <= 0) {
toogleDocumentTitle();
toggleDocumentTitle();
$('#session-expire').modal('hide');
$('#session-finished').modal();
}
@ -130,7 +130,7 @@
}
setSessionTimeout(initializeSessionCountdown, oneSecondTimeout);
} else if (expireOn && !event.originalEvent.newValue) {
toogleDocumentTitle();
toggleDocumentTitle();
}
expireOn = event.originalEvent.newValue;

View file

@ -8,7 +8,7 @@ module Users
skip_before_action :verify_authenticity_token
before_action :sign_up_with_provider_enabled?,
only: :linkedin
before_action :check_sso_status, only: %i(customazureactivedirectory okta)
before_action :check_sso_status, only: %i(customazureactivedirectory okta openid_connect)
# You should configure your model like this:
# devise :omniauthable, omniauth_providers: [:twitter]
@ -46,17 +46,7 @@ module Users
if user.blank?
# Create new user and identity
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
user = User.new(full_name: full_name,
initials: generate_initials(full_name),
email: email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user = create_user_from_auth(email, auth)
sign_in_and_redirect(user, event: :authentication)
elsif provider_conf['auto_link_on_sign_in']
# Link to existing local account
@ -147,16 +137,7 @@ module Users
user = User.find_by(email: auth.info.email.downcase)
if user.blank?
# Create new user and identity
user = User.new(full_name: auth.info.name,
initials: generate_initials(auth.info.name),
email: auth.info.email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user = create_user_from_auth(email, auth)
else
# Link to existing local account
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
@ -177,6 +158,107 @@ module Users
end
end
def openid_connect
auth = request.env['omniauth.auth']
settings = ApplicationSettings.instance
provider_conf = settings.values['openid_connect']
raise StandardError, 'No matching OpenID Connect AD provider config found' if provider_conf.blank?
return redirect_to connected_accounts_path if current_user
email = auth.info.email
email ||= auth.dig(:extra, :raw_info, :id_token_claims, :emails)&.first
user = User.from_omniauth(auth)
# User found in database so just signing in
return sign_in_and_redirect(user) if user.present?
if email.blank?
# No email in the token so can not link or create user
error_message = I18n.t('devise.openid_connect.errors.no_email')
return redirect_to after_omniauth_failure_path_for(resource_name)
end
user = User.find_by(email: email.downcase)
if user.blank?
# Create new user and identity
user = create_user_from_auth(email, auth)
sign_in_and_redirect(user)
elsif provider_conf['auto_link_on_sign_in']
# Link to existing local account
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at) if user.confirmed_at.blank?
sign_in_and_redirect(user)
else
# Cannot do anything with it, so just return an error
error_message = I18n.t('devise.openid_connect.errors.no_local_user_map')
redirect_to after_omniauth_failure_path_for(resource_name)
end
rescue StandardError => e
Rails.logger.error e.message
Rails.logger.error e.backtrace.join("\n")
error_message = I18n.t('devise.openid_connect.errors.failed_to_save') if e.is_a?(ActiveRecord::RecordInvalid)
error_message ||= I18n.t('devise.openid_connect.errors.generic')
redirect_to after_omniauth_failure_path_for(resource_name)
ensure
if error_message
set_flash_message(:alert, :failure, kind: I18n.t('devise.openid_connect.provider_name'), reason: error_message)
else
set_flash_message(:notice, :success, kind: I18n.t('devise.openid_connect.provider_name'))
end
end
def saml
auth = request.env['omniauth.auth']
settings = ApplicationSettings.instance
provider_conf = settings.values['saml']
raise StandardError, 'No matching SAML provider config found' if provider_conf.blank?
return redirect_to connected_accounts_path if current_user
email = auth.info.email
user = User.from_omniauth(auth)
# User found in database so just signing in
return sign_in_and_redirect(user) if user.present?
if email.blank?
# No email in the token so can not link or create user
error_message = I18n.t('devise.saml.errors.no_email')
return redirect_to after_omniauth_failure_path_for(resource_name)
end
user = User.find_by(email: email.downcase)
if user.blank?
user = create_user_from_auth(email, auth)
sign_in_and_redirect(user)
elsif provider_conf['auto_link_on_sign_in']
# Link to existing local account
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at) if user.confirmed_at.blank?
sign_in_and_redirect(user)
else
# Cannot do anything with it, so just return an error
error_message = I18n.t('devise.saml.errors.no_local_user_map')
redirect_to after_omniauth_failure_path_for(resource_name)
end
rescue StandardError => e
Rails.logger.error e.message
Rails.logger.error e.backtrace.join("\n")
error_message = I18n.t('devise.saml.errors.failed_to_save') if e.is_a?(ActiveRecord::RecordInvalid)
error_message ||= I18n.t('devise.saml.errors.generic')
redirect_to after_omniauth_failure_path_for(resource_name)
ensure
if error_message
set_flash_message(:alert, :failure, kind: I18n.t('devise.saml.provider_name'), reason: error_message)
else
set_flash_message(:notice, :success, kind: I18n.t('devise.saml.provider_name'))
end
end
# More info at:
# https://github.com/plataformatec/devise#omniauth
@ -213,5 +295,33 @@ module Users
initials = initials.strip.blank? ? 'PLCH' : initials[0..3]
initials
end
def create_user_from_auth(email, auth)
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
user = User.new(full_name: full_name,
initials: generate_initials(full_name),
email: email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user
end
def create_user_from_auth(email, auth)
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
user = User.new(full_name: full_name,
initials: generate_initials(full_name),
email: email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user
end
end
end

View file

@ -21,7 +21,7 @@ class Users::PasswordsController < Devise::PasswordsController
if resource.errors.blank?
resource.unlock_access! if unlockable?(resource)
if !resource.two_factor_auth_enabled?
if !two_factor_auth_enabled_for(resource)
flash_message = resource.active_for_authentication? ? :updated : :updated_not_active
set_flash_message!(:notice, flash_message)
resource.after_database_authentication
@ -39,7 +39,11 @@ class Users::PasswordsController < Devise::PasswordsController
protected
def after_resetting_password_path_for(resource)
resource.two_factor_auth_enabled? ? new_session_path(resource_name) : after_sign_in_path_for(resource)
two_factor_auth_enabled_for(resource) ? new_session_path(resource_name) : after_sign_in_path_for(resource)
end
def two_factor_auth_enabled_for(user)
user.two_factor_auth_enabled?
end
# The path used after sending reset password instructions

View file

@ -199,12 +199,21 @@ module ApplicationHelper
ENV['SSO_ENABLED'] == 'true'
end
def okta_configured?
ApplicationSettings.instance.values['okta'].present?
def okta_enabled?
ApplicationSettings.instance.values.dig('okta', 'enabled')
end
def azure_ad_configured?
ApplicationSettings.instance.values['azure_ad_apps'].present?
def azure_ad_enabled?
provider_conf = ApplicationSettings.instance.values['azure_ad_apps']
provider_conf.present? && provider_conf[0]['enabled']
end
def saml_enabled?
ApplicationSettings.instance.values.dig('saml', 'enabled')
end
def openid_connect_enabled?
ApplicationSettings.instance.values.dig('openid_connect', 'enabled')
end
def wopi_enabled?

View file

@ -10,7 +10,9 @@
{{ experiment.name }}
</h4>
</div>
<div class="modal-body whitespace-pre-wrap" v-html="experiment.sa_description"></div>
<div class="modal-body">
<div class="[&_.atwho-user-container]:!whitespace-normal whitespace-pre-wrap" v-html="experiment.sa_description"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ i18n.t('general.close') }}</button>
</div>

View file

@ -1,7 +1,7 @@
<template>
<div class="group relative flex items-center group-hover:marker text-xs h-full w-full"
:style="{ lineHeight: 'unset' }">
<div class="flex gap-2"
<div class="flex gap-2 w-full"
:style="{ lineHeight: 'unset' }"
:class="{
'items-center text-sm': params.dtComponent.currentViewRender === 'table',

View file

@ -0,0 +1,24 @@
<div class="panel panel-default">
<div class="panel-body">
<div class="col-xs-8 col-sm-9 col-md-9 col-lg-9">
<strong><%= t('users.settings.account.connected_accounts.openid_connect.title') %></strong> <br>
<p><%= t('users.settings.account.connected_accounts.openid_connect.connect_hint') %></p>
</div>
<div class="pull-right">
<div>
<strong>
<%= t('users.settings.account.connected_accounts.openid_connect.connected') %>
<span class="sn-icon sn-icon-check" aria-hidden="true"></span>
</strong>
</div>
<div>
<%= link_to t('users.settings.account.connected_accounts.openid_connect.unlink_button'),
'#unlinkOpenIdConnectModal',
class: 'btn btn-danger',
data: { toggle: 'modal'} %>
</div>
</div>
</div>
</div>
<%= render partial: 'users/settings/account/connected_accounts/unlink_modals/openid_connect_modal', locals: { provider: provider } %>

View file

@ -0,0 +1,24 @@
<div class="panel panel-default">
<div class="panel-body">
<div class="col-xs-8 col-sm-9 col-md-9 col-lg-9">
<strong><%= t('users.settings.account.connected_accounts.saml.title') %></strong> <br>
<p><%= t('users.settings.account.connected_accounts.saml.connect_hint') %></p>
</div>
<div class="pull-right">
<div>
<strong>
<%= t('users.settings.account.connected_accounts.saml.connected') %>
<span class="sn-icon sn-icon-check" aria-hidden="true"></span>
</strong>
</div>
<div>
<%= link_to t('users.settings.account.connected_accounts.saml.unlink_button'),
'#unlinksamlModal',
class: 'btn btn-danger',
data: { toggle: 'modal'} %>
</div>
</div>
</div>
</div>
<%= render partial: 'users/settings/account/connected_accounts/unlink_modals/saml_modal', locals: { provider: provider } %>

View file

@ -0,0 +1,23 @@
<div class="modal fade" id="unlinkOpenIdConnectModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<%= t('general.close') %>">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" >
<%= t('users.settings.account.connected_accounts.openid_connect.unlink_modal.title') %>
</h4>
</div>
<div class="modal-body">
<p><%= t('users.settings.account.connected_accounts.openid_connect.unlink_modal.description_1') %></p>
</div>
<div class="modal-footer">
<%= form_tag(unlink_connected_account_path, method: :delete) do %>
<%= hidden_field_tag :provider, provider %>
<%= submit_tag t('users.settings.account.connected_accounts.openid_connect.unlink_modal.submit_button'), class: 'btn btn-danger' %>
<% end %>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,23 @@
<div class="modal fade" id="unlinksamlModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<%= t('general.close') %>">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" >
<%= t('users.settings.account.connected_accounts.saml.unlink_modal.title') %>
</h4>
</div>
<div class="modal-body">
<p><%= t('users.settings.account.connected_accounts.saml.unlink_modal.description_1') %></p>
</div>
<div class="modal-footer">
<%= form_tag(unlink_connected_account_path, method: :delete) do %>
<%= hidden_field_tag :provider, provider %>
<%= submit_tag t('users.settings.account.connected_accounts.saml.unlink_modal.submit_button'), class: 'btn btn-danger' %>
<% end %>
</div>
</div>
</div>
</div>

View file

@ -28,7 +28,7 @@
<% end -%>
<% if controller_name != 'passwords'%>
<%- if sso_enabled? && okta_configured? %>
<%- if sso_enabled? && okta_enabled? %>
<div class="okta-sign-in-actions">
<%= form_tag user_okta_omniauth_authorize_path, method: :post, id: 'oktaForm' do %>
<%= submit_tag t('devise.okta.sign_in_label'), class: 'btn btn-okta' %>
@ -42,10 +42,26 @@
<% end -%>
<% end -%>
<% if sso_enabled? && azure_ad_configured? %>
<% if sso_enabled? && azure_ad_enabled? %>
<div class="azure-sign-in-actions">
<%= render partial: "users/shared/azure_sign_in_links", locals: { resource_name: resource_name } %>
</div>
<% end %>
<%- if sso_enabled? && openid_connect_enabled? %>
<div class="azure-sign-in-actions">
<%= form_tag user_openid_connect_omniauth_authorize_path, method: :post do %>
<%= submit_tag t('devise.sessions.new.openid_connect_submit'), class: 'btn btn-primary' %>
<% end %>
</div>
<% end %>
<% if sso_enabled? && saml_enabled? %>
<div class="azure-sign-in-actions">
<%= form_tag user_saml_omniauth_authorize_path, method: :post do %>
<%= submit_tag t('devise.sessions.new.saml_submit'), class: 'btn btn-primary' %>
<% end %>
</div>
<% end %>
<% end %>
</div>

View file

@ -82,5 +82,7 @@ module Scinote
config.action_view.field_error_proc = Proc.new { |html_tag, instance|
"<div class=\"field_with_errors sci-input-container\">#{html_tag}</div>".html_safe
}
ActiveRecord::SessionStore::Session.serializer = :json
end
end

View file

@ -11,9 +11,9 @@ ActiveSupport::Reloader.to_prepare do
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data, :blob
policy.object_src :none
policy.script_src :self, :unsafe_eval, *Extends::EXTERNAL_SERVICES
policy.script_src :self, :unsafe_eval, *Extends::EXTERNAL_SCRIPT_SERVICES
policy.style_src :self, :https, :unsafe_inline, :data
policy.connect_src :self, :data, *Extends::EXTERNAL_SERVICES
policy.connect_src :self, :data, *Extends::EXTERNAL_CONNECT_SERVICES
# Specify URI for violation reports
# policy.report_uri "/csp-violation-report-endpoint"
@ -44,8 +44,8 @@ Rails.application.config.content_security_policy_nonce_directives = %w(script-sr
Rails.application.configure do
config.after_initialize do
if ActiveStorage::Blob.service.name == :amazon
Extends::EXTERNAL_SERVICES += [ActiveStorage::Blob.service.bucket.url]
Rails.application.config.content_security_policy.connect_src :self, :data, *Extends::EXTERNAL_SERVICES
Extends::EXTERNAL_CONNECT_SERVICES += [ActiveStorage::Blob.service.bucket.url]
Rails.application.config.content_security_policy.connect_src :self, :data, *Extends::EXTERNAL_CONNECT_SERVICES
end
end
end

View file

@ -146,7 +146,7 @@ class Extends
API_VERSIONS = ['v1']
# Array used for injecting names of additional authentication methods for API
API_PLUGABLE_AUTH_METHODS = [:azure_jwt_auth]
API_PLUGABLE_AUTH_METHODS = []
API_REPOSITORY_DATA_TYPE_MAPPINGS = { 'RepositoryTextValue' => 'text',
'RepositoryDateValue' => 'date',
@ -162,7 +162,7 @@ class Extends
'RepositoryStatusValue' => 'status',
'RepositoryStockValue' => 'stock' }
OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta)
OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta openid_connect saml)
INITIAL_USER_OPTIONS = {}
@ -589,22 +589,25 @@ class Extends
'FluicsLabelTemplate' => 'Fluics'
}
EXTERNAL_SERVICES = %w(
EXTERNAL_SCRIPT_SERVICES = %w(
https://marvinjs.chemicalize.com/
www.recaptcha.net/
www.gstatic.com/recaptcha/
)
EXTERNAL_CONNECT_SERVICES = %w(
https://www.protocols.io/
http://127.0.0.1:9100/
https://marvinjs.chemicalize.com/
newrelic.com
*.newrelic.com
*.nr-data.net
www.recaptcha.net/
www.gstatic.com/recaptcha/
extras.scinote.net
https://www.scinote.net
)
if Constants::ASSET_SYNC_URL && EXTERNAL_SERVICES.exclude?(Constants::ASSET_SYNC_URL)
if Constants::ASSET_SYNC_URL && EXTERNAL_CONNECT_SERVICES.exclude?(Constants::ASSET_SYNC_URL)
asset_sync_url = URI.parse(Constants::ASSET_SYNC_URL)
EXTERNAL_SERVICES << "#{asset_sync_url.scheme}://#{asset_sync_url.host}:#{asset_sync_url.port}"
EXTERNAL_CONNECT_SERVICES << "#{asset_sync_url.scheme}://#{asset_sync_url.host}:#{asset_sync_url.port}"
end
COLORED_BACKGROUND_ACTIONS = %w(

View file

@ -5,7 +5,7 @@ require 'omniauth/strategies/custom_azure_active_directory'
AZURE_SETUP_PROC = lambda do |env|
settings = ApplicationSettings.instance
providers = settings.values['azure_ad_apps'].select { |v| v['enable_sign_in'] }
raise StandardError, 'No Azure AD config available for sign in' if providers.blank?
raise StandardError, 'No Azure AD config available for sign in' unless providers.present? && providers[0]['enabled']
req = Rack::Request.new(env)
@ -32,10 +32,36 @@ AZURE_SETUP_PROC = lambda do |env|
env['omniauth.strategy'].options[:base_azure_url] = "#{conf_uri.scheme || 'https'}://#{conf_uri.host}"
end
OPENID_CONNECT_SETUP_PROC = lambda do |env|
settings = ApplicationSettings.instance
provider_conf = settings.values['openid_connect']
raise StandardError, 'No OpenID Connect config available for sign in' if provider_conf.blank?
client_options = {
identifier: provider_conf['client_id'],
secret: provider_conf['client_secret'],
redirect_uri: Rails.application.routes.url_helpers.user_openid_connect_omniauth_callback_url
}
unless provider_conf['discovery']
client_options[:host] = provider_conf['host']
client_options[:authorization_endpoint] = provider_conf['authorization_endpoint']
client_options[:token_endpoint] = provider_conf['token_endpoint']
client_options[:userinfo_endpoint] = provider_conf['userinfo_endpoint']
client_options[:jwks_uri] = provider_conf['jwks_uri']
end
env['omniauth.strategy'].options[:name] = 'openid_connect'
env['omniauth.strategy'].options[:scope] = %i(openid email profile)
env['omniauth.strategy'].options[:issuer] = provider_conf['issuer_url']
env['omniauth.strategy'].options[:discovery] = provider_conf['discovery'] == true
env['omniauth.strategy'].options[:client_options] = client_options
end
OKTA_SETUP_PROC = lambda do |env|
settings = ApplicationSettings.instance
provider_conf = settings.values['okta']
raise StandardError, 'No Okta config available for sign in' if provider_conf.blank?
raise StandardError, 'No Okta config available for sign in' unless provider_conf.present? && provider_conf['enabled']
oauth2_base_url =
if provider_conf['auth_server_id'].blank?
@ -63,12 +89,31 @@ OKTA_SETUP_PROC = lambda do |env|
env['omniauth.strategy'].options[:client_options] = client_options
end
SAML_SETUP_PROC = lambda do |env|
settings = ApplicationSettings.instance
provider_conf = settings.values['saml']
raise StandardError, 'No SAML config available for sign in' if provider_conf.blank?
env['omniauth.strategy'].options[:idp_sso_service_url] = provider_conf['idp_sso_service_url']
env['omniauth.strategy'].options[:idp_cert] = provider_conf['idp_cert']
env['omniauth.strategy'].options[:sp_entity_id] = provider_conf['sp_entity_id']
env['omniauth.strategy'].options[:uid_attribute] = 'uid'
end
Rails.application.config.middleware.use OmniAuth::Builder do
provider OmniAuth::Strategies::CustomAzureActiveDirectory, setup: AZURE_SETUP_PROC
end
Rails.application.config.middleware.use OmniAuth::Builder do
provider OmniAuth::Strategies::OpenIDConnect, setup: OPENID_CONNECT_SETUP_PROC
end
Rails.application.config.middleware.use OmniAuth::Builder do
provider OmniAuth::Strategies::Okta, setup: OKTA_SETUP_PROC
end
Rails.application.config.middleware.use OmniAuth::Builder do
provider OmniAuth::Strategies::SAML, setup: SAML_SETUP_PROC
end
OmniAuth.config.logger = Rails.logger

View file

@ -1,3 +1,5 @@
# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_scinote_session'
session_store = ENV['SCINOTE_SERVERSIDE_SESSIONS'] == 'true' ? :active_record_store : :cookie_store
Rails.application.config.session_store session_store, key: '_scinote_session'

View file

@ -34,6 +34,8 @@ en:
remember_me: "Remember me"
submit: "Log in"
azure_ad_submit: "Sign in with Azure AD"
openid_connect_submit: "Sign in with OpenID Connect"
saml_submit: "Sign in with SAML"
2fa:
title: "Two-factor authentication"
description: "Enter the one-time code found in your authenticator app to log in to SciNote."
@ -86,6 +88,13 @@ en:
no_local_user_map: "No local user record found"
no_email: "Email is missing in auth token"
failed_to_save: "Failed to create new user"
openid_connect:
provider_name: "OpenID Connect"
errors:
generic: "Failed to sign in user"
no_local_user_map: "No local user record found"
no_email: "Email is missing in auth token"
failed_to_save: "Failed to create new user"
okta:
provider_name: "Okta"
sign_in_label: "Sign in with Okta"
@ -93,6 +102,13 @@ en:
generic: "Failed to sign in user"
no_local_user_map: "No local user record found"
failed_to_save: "Failed to create new user"
saml:
provider_name: "SAML"
sign_in_label: "Sign in with SAML"
errors:
generic: "Failed to sign in user"
no_local_user_map: "No local user record found"
failed_to_save: "Failed to create new user"
doorkeeper:
errors:
@ -2883,13 +2899,13 @@ en:
not_connected: "You have no Connected accounts"
unlink_success: "Successfully unlinked"
azure_ad:
title: "Your Azure AD Account"
connect_hint: "Allows you to sign in with your Azure AD account."
title: "Your Microsoft Entra ID Account"
connect_hint: "Allows you to sign in with your Microsoft Entra ID account."
connected: "Connected"
unlink_button: "Unlink"
unlink_modal:
title: "Unlink Azure AD account?"
description_1: "Are you sure you would like unlink Azure AD and SciNote accounts?"
title: "Unlink Microsoft Entra ID account?"
description_1: "Are you sure you would like unlink Microsoft Entra ID and SciNote accounts?"
submit_button: "Submit"
okta:
title: "Your Okta Account"
@ -2900,6 +2916,24 @@ en:
title: "Unlink Okta account?"
description_1: "Are you sure you would like unlink Okta and SciNote accounts?"
submit_button: "Submit"
openid_connect:
title: "Your OpenID Connect Account"
connect_hint: "Allows you to sign in with your OpenID Connect account."
connected: "Connected"
unlink_button: "Unlink"
unlink_modal:
title: "Unlink OpenID Connect account?"
description_1: "Are you sure you would like unlink OpenID Connect and SciNote accounts?"
submit_button: "Submit"
saml:
title: "Your SAML Account"
connect_hint: "Allows you to sign in with your SAML account."
connected: "Connected"
unlink_button: "Unlink"
unlink_modal:
title: "Unlink SAML account?"
description_1: "Are you sure you would like unlink SAML and SciNote accounts?"
submit_button: "Submit"
errors:
not_found: "You have no Connected accounts for this provider"
generic: "Unable to unlink linked account"

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddSessionsTable < ActiveRecord::Migration[7.0]
def change
create_table :sessions do |t|
t.string :session_id, null: false
t.jsonb :data
t.timestamps
end
add_index :sessions, :session_id, unique: true
add_index :sessions, :updated_at
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_01_15_140943) do
ActiveRecord::Schema[7.0].define(version: 2024_01_18_094253) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gist"
enable_extension "pg_trgm"
@ -994,6 +994,15 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_15_140943) do
t.index ["user_id"], name: "index_results_on_user_id"
end
create_table "sessions", force: :cascade do |t|
t.string "session_id", null: false
t.jsonb "data"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["session_id"], name: "index_sessions_on_session_id", unique: true
t.index ["updated_at"], name: "index_sessions_on_updated_at"
end
create_table "settings", force: :cascade do |t|
t.text "type", null: false
t.jsonb "values", default: {}, null: false