diff --git a/.rubocop.yml b/.rubocop.yml index ac9797608..1edaa3623 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,12 +14,12 @@ AllCops: Layout/AccessModifierIndentation: EnforcedStyle: indent -Layout/AlignHash: +Layout/HashAlignment: EnforcedHashRocketStyle: key EnforcedColonStyle: key EnforcedLastArgumentHashStyle: ignore_implicit -Layout/AlignParameters: +Layout/ParameterAlignment: EnforcedStyle: with_first_parameter Style/AndOr: @@ -83,7 +83,7 @@ Naming/FileName: Enabled: false Exclude: [] -Layout/IndentFirstParameter: +Layout/FirstParameterIndentation: EnforcedStyle: consistent Style/For: @@ -111,10 +111,10 @@ Layout/IndentationConsistency: Layout/IndentationWidth: Width: 2 -Layout/IndentFirstArrayElement: +Layout/FirstArrayElementIndentation: EnforcedStyle: special_inside_parentheses -Layout/IndentFirstHashElement: +Layout/FirstHashElementIndentation: EnforcedStyle: special_inside_parentheses Style/Next: @@ -197,9 +197,9 @@ Naming/PredicateName: - is_ - has_ - have_ - NamePrefixBlacklist: + ForbiddenPrefixes: - is_ - NameWhitelist: + AllowedMethods: - is_a? Exclude: - spec/**/* @@ -282,7 +282,7 @@ Style/TernaryParentheses: EnforcedStyle: require_no_parentheses AllowSafeAssignment: true -Layout/TrailingBlankLines: +Layout/TrailingEmptyLines: EnforcedStyle: final_newline Style/TrailingCommaInArguments: @@ -411,7 +411,7 @@ Lint/UnusedMethodArgument: Lint/EachWithObjectArgument: Enabled: true -Lint/HandleExceptions: +Lint/SuppressedException: Enabled: false Lint/LiteralAsCondition: diff --git a/Gemfile b/Gemfile index b8aa1bbe1..9037af92e 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'yomu', git: 'https://github.com/biosistemika/yomu', branch: 'master' # Gems for OAuth2 subsystem gem 'doorkeeper', '>= 4.6' gem 'omniauth' +gem 'omniauth-azure-activedirectory' gem 'omniauth-linkedin-oauth2' # TODO: remove this when omniauth gem resolves CVE issues diff --git a/Gemfile.lock b/Gemfile.lock index bb345355b..3c44aae2e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -373,6 +373,9 @@ GEM omniauth (1.9.0) hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) + omniauth-azure-activedirectory (1.0.0) + jwt (~> 1.5) + omniauth (~> 1.1) omniauth-linkedin-oauth2 (1.0.0) omniauth-oauth2 omniauth-oauth2 (1.6.0) @@ -656,6 +659,7 @@ DEPENDENCIES newrelic_rpm nokogiri (~> 1.10.3) omniauth + omniauth-azure-activedirectory omniauth-linkedin-oauth2 omniauth-rails_csrf_protection (~> 0.1) overcommit diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index aa1faf9c4..7be1f04a1 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -380,6 +380,16 @@ a[data-toggle="tooltip"] { width: 100%; } +.azure-sign-in-actions { + margin-bottom: 10px; + margin-top: 10px; + + .btn-azure-ad { + background-color: $office-ms-word; + color: $color-white; + } +} + .navbar-secondary { -webkit-transition: all 0.5s ease; -moz-transition: all 0.5s ease; diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 79c75d232..13fcbd415 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Users class OmniauthCallbacksController < Devise::OmniauthCallbacksController include UsersGenerator @@ -13,6 +15,53 @@ module Users # def twitter # end + def customazureactivedirectory + auth = request.env['omniauth.auth'] + provider_id = auth.dig(:extra, :raw_info, :id_token_claims, :aud) + provider_conf = Rails.configuration.x.azure_ad_apps[provider_id] + raise StandardError, 'No matching Azure AD provider config found' if provider_conf.empty? + + auth.provider = provider_conf[:provider] + + 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) + if user + # User found in database so just sign in him + sign_in_and_redirect(user) + elsif email.present? + user = User.find_by(email: email) + + if user.blank? + # Create new user and identity + User.create_from_omniauth!(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) + sign_in_and_redirect(user) + else + # Cannot do anything with it, so just return an error + error_message = I18n.t('devise.azure.errors.no_local_user_map') + redirect_to after_omniauth_failure_path_for(resource_name) + end + end + rescue StandardError => e + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") + error_message = I18n.t('devise.azure.errors.failed_to_save') if e.is_a?(ActiveRecord::RecordInvalid) + error_message ||= I18n.t('devise.azure.errors.generic') + redirect_to after_omniauth_failure_path_for(resource_name) + ensure + if error_message + set_flash_message(:alert, :failure, kind: I18n.t('devise.azure.provider_name'), reason: error_message) + else + set_flash_message(:notice, :success, kind: I18n.t('devise.azure.provider_name')) + end + end + def linkedin auth_hash = request.env['omniauth.auth'] diff --git a/app/controllers/users/settings/account/connected_accounts_controller.rb b/app/controllers/users/settings/account/connected_accounts_controller.rb index 2edee3954..d5505b040 100644 --- a/app/controllers/users/settings/account/connected_accounts_controller.rb +++ b/app/controllers/users/settings/account/connected_accounts_controller.rb @@ -9,7 +9,20 @@ module Users end def destroy - current_user.user_identities.where(provider: params.require(:provider)).take&.destroy! + if Rails.configuration.x.azure_ad_apps.select { |_, v| v[:provider] == params[:provider] }.present? + provider = params[:provider] + else + flash[:error] = t('users.settings.account.connected_accounts.errors.not_found') + return + end + ActiveRecord::Base.transaction do + __send__("#{provider}_pre_destroy".to_sym) if respond_to?("#{provider}_pre_destroy".to_sym, true) + current_user.user_identities.where(provider: provider).take&.destroy! + end + flash[:success] = t('users.settings.account.connected_accounts.unlink_success') + rescue StandardError + flash[:error] ||= t('users.settings.account.connected_accounts.errors.generic') + ensure @linked_accounts = current_user.user_identities.pluck(:provider) render :index end diff --git a/app/helpers/addons_helper.rb b/app/helpers/addons_helper.rb index 150af4976..9a5622ff0 100644 --- a/app/helpers/addons_helper.rb +++ b/app/helpers/addons_helper.rb @@ -3,6 +3,6 @@ module AddonsHelper Rails::Engine .subclasses .select { |c| c.name.start_with?('Scinote') } - .map(&:parent) + .map(&:module_parent) end end diff --git a/app/models/user.rb b/app/models/user.rb index 149fe3a52..cce0d95a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -297,6 +297,20 @@ class User < ApplicationRecord .take end + def self.create_from_omniauth!(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 + # Search all active users for username & email. Can # also specify which team to ignore. def self.search( diff --git a/app/services/api/azure_jwt.rb b/app/services/api/azure_jwt.rb index f6804d2ce..e2076f043 100644 --- a/app/services/api/azure_jwt.rb +++ b/app/services/api/azure_jwt.rb @@ -42,7 +42,7 @@ module Api end # Decode token payload and verify it's signature. - payload, = JWT.decode( + payload, header = JWT.decode( token, OpenSSL::PKey::RSA.new(fetch_rsa_key(k_id, app_id)), true, @@ -54,7 +54,7 @@ module Api iss: app_config[:iss], nbf_leeway: LEEWAY ) - HashWithIndifferentAccess.new(payload) + [HashWithIndifferentAccess.new(payload), HashWithIndifferentAccess.new(header)] end end end diff --git a/app/views/users/shared/_azure_sign_in_links.html.erb b/app/views/users/shared/_azure_sign_in_links.html.erb new file mode 100644 index 000000000..fb5c5fab8 --- /dev/null +++ b/app/views/users/shared/_azure_sign_in_links.html.erb @@ -0,0 +1,7 @@ +<% Rails.configuration.x.azure_ad_apps.select { |uid, config| config[:enable_sign_in] }.each do |uid, config| %> +