Merge remote-tracking branch 'origin/release/7.4' into dev

pull/6827/head
Oliver Günther 7 years ago
commit 19ed50a6da
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 1
      app/controllers/two_factor_authentication/concerns/remember_token.rb
  2. 55
      app/controllers/two_factor_authentication/settings_controller.rb
  3. 80
      app/views/two_factor_authentication/settings.html.erb
  4. 30
      app/views/two_factor_authentication/upsale.html.erb
  5. 20
      config/locales/en.yml
  6. 3
      config/routes.rb
  7. 17
      lib/open_project/two_factor_authentication/engine.rb
  8. 32
      lib/open_project/two_factor_authentication/token_strategy_manager.rb
  9. 4
      openproject-two_factor_authentication.gemspec
  10. 3
      spec/controllers/two_factor_authentication/authentication_controller_spec.rb
  11. 3
      spec/controllers/two_factor_authentication/forced_registration/two_factor_devices_controller_spec.rb
  12. 3
      spec/controllers/two_factor_authentication/my/two_factor_devices_controller_spec.rb
  13. 3
      spec/controllers/two_factor_authentication/users/two_factor_devices_controller_spec.rb
  14. 5
      spec/lib/token_strategy_manager_spec.rb
  15. 12
      spec/services/token_service_spec.rb

@ -29,6 +29,7 @@ module ::TwoFactorAuthentication
cookies.encrypted[remember_cookie_name] = { cookies.encrypted[remember_cookie_name] = {
value: new_token!(@authenticated_user), value: new_token!(@authenticated_user),
httponly: true, httponly: true,
expires: remember_2fa_days.days.from_now,
secure: Setting.protocol == 'https' secure: Setting.protocol == 'https'
} }
end end

@ -0,0 +1,55 @@
module ::TwoFactorAuthentication
class SettingsController < ApplicationController
before_action :require_admin
before_action :check_enabled
layout 'admin'
menu_item :two_factor_authentication
def show
render template: 'two_factor_authentication/settings',
locals: {
settings: Setting.plugin_openproject_two_factor_authentication,
strategy_manager: manager,
configuration: manager.configuration
}
end
def update
current_settings = Setting.plugin_openproject_two_factor_authentication
begin
merge_settings!(current_settings, permitted_params)
manager.validate_configuration!
flash[:notice] = I18n.t(:notice_successful_update)
rescue ArgumentError => e
Setting.plugin_openproject_two_factor_authentication = current_settings
flash[:error] = I18n.t('two_factor_authentication.settings.failed_to_save_settings', message: e.message)
Rails.logger.error "Failed to save 2FA settings: #{e.message}"
end
redirect_to action: :show
end
private
def permitted_params
params.require(:settings).permit(:enforced, :allow_remember_for_days)
end
def merge_settings!(current, permitted)
Setting.plugin_openproject_two_factor_authentication = current.merge(
enforced: !!permitted[:enforced],
allow_remember_for_days: permitted[:allow_remember_for_days]
)
end
def check_enabled
render_403 unless manager.configurable_by_ui?
end
def manager
::OpenProject::TwoFactorAuthentication::TokenStrategyManager
end
end
end

@ -0,0 +1,80 @@
<% html_title(t(:label_administration), t('two_factor_authentication.settings.title')) -%>
<%= breadcrumb_toolbar(t('two_factor_authentication.settings.title')) %>
<section class="admin--edit-section">
<%= styled_form_tag({ action: :update },
method: :post,
id: 'update-ldap-group-settings-form') do %>
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= t('two_factor_authentication.settings.current_configuration') %></legend>
<p>
<%= t('two_factor_authentication.settings.text_configuration') %>
<br/>
<% configuration_link = OpenProject::Static::Links.links.fetch :configuration_guide %>
<%= link_to t('two_factor_authentication.settings.text_configuration_guide'), configuration_link[:href] %>
</p>
<div class="attributes-key-value">
<div class="attributes-key-value--key"><%= t('two_factor_authentication.settings.label_active_strategies') %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value">
<%= t(:label_none) if configuration[:active_strategies].empty? %>
<% configuration[:active_strategies].each do |key| %>
<span>
<%= t("two_factor_authentication.strategies.#{key}") %>
</span>
<br/>
<% end %>
</div>
</div>
<div class="attributes-key-value--key"><%= t('two_factor_authentication.settings.label_enforced') %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value">
<span><%= !!configuration[:enforced] %></span>
</div>
</div>
<div class="attributes-key-value--key"><%= t('two_factor_authentication.settings.label_remember') %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value">
<% if configuration[:allow_remember_for_days].to_i == 0 %>
<span><%= t(:label_disabled) %></span>
<% else %>
<span><%= configuration[:allow_remember_for_days] %> (<%= t(:label_day_plural) %>)</span>
<% end %>
</div>
</div>
</div>
</fieldset>
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= t(:label_settings) %></legend>
<div class="form--field">
<label class="form--label" for='settings[enforced]'><%= t('two_factor_authentication.settings.label_enforced') %></label>
<div class="form--field-container ">
<%= styled_check_box_tag 'settings[enforced]',
'1',
!!configuration[:enforced],
disabled: strategy_manager.enforced_by_configuration?(:enforced) || configuration[:active_strategies].empty?,
container_class: '-middle' %>
</div>
<div class="form--field-instructions">
<%= I18n.t('two_factor_authentication.settings.text_enforced') %>
</div>
</div>
<div class="form--field">
<label class="form--label" for='settings[allow_remember_for_days]'><%= t('two_factor_authentication.settings.label_remember') %></label>
<div class="form--field-container">
<%= styled_number_field_tag 'settings[allow_remember_for_days]',
configuration[:allow_remember_for_days],
min: '0',
max: '365',
step: '1',
disabled: strategy_manager.enforced_by_configuration?(:allow_remember_for_days),
container_class: '-middle' %>
</div>
<div class="form--field-instructions">
<%= I18n.t('two_factor_authentication.settings.text_remember') %>
</div>
</div>
</fieldset>
<%= styled_submit_tag l(:button_apply), class: '-highlight' %>
<% end %>
</section>

@ -0,0 +1,30 @@
<% html_title(t(:label_administration), t('two_factor_authentication.settings.title')) -%>
<%= breadcrumb_toolbar(t('two_factor_authentication.settings.title')) %>
<div class="notification-box upsale-notification">
<div class="notification-box--content">
<h3><%= t('admin.enterprise.upgrade_to_ee') %></h3>
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<ul class="">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
</p>
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=2fa",
{ class: 'button -alt-highlight',
aria: {label: t('admin.enterprise.order')},
title: t('admin.enterprise.order')}) do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= t('admin.enterprise.order') %></span>
<% end %>
</div>
</div>

@ -44,6 +44,21 @@ en:
enter_backup_code_title: Enter backup code enter_backup_code_title: Enter backup code
enter_backup_code_text: Please enter a valid backup code from your list of codes in case you can no longer access your registered 2FA devices. enter_backup_code_text: Please enter a valid backup code from your list of codes in case you can no longer access your registered 2FA devices.
other_device: 'Use another device or backup code' other_device: 'Use another device or backup code'
settings:
title: '2FA settings'
current_configuration: 'Current configuration'
label_active_strategies: 'Active 2FA strategies'
label_enforced: 'Enforce 2FA'
label_remember: 'Remember 2FA login'
text_configuration: |
Note: These values represent the current application-wide configuration. You cannot disable settings enforced by the configuration or change the current active strategies, since they require a server restart.
text_configuration_guide: For more information, check the configuration guide.
text_enforced: 'Enable this setting to force all users to register a 2FA device on their next login. Can only be disabled when not enforced by configuration.'
text_remember: |
Set this to greater than zero to allow users to remember their 2FA authentication for the given number of days.
They will not be requested to re-enter it during that period. Can only be set when not enforced by configuration.
error_invalid_settings: 'The 2FA strategies you selected are invalid'
failed_to_save_settings: 'Failed to update 2FA settings: %{message}'
admin: admin:
self_edit_path: 'To add or modify your own 2FA devices, please go to %{self_edit_link}' self_edit_path: 'To add or modify your own 2FA devices, please go to %{self_edit_link}'
self_edit_link_name: 'Two-factor authentication on your account page' self_edit_link_name: 'Two-factor authentication on your account page'
@ -118,6 +133,11 @@ en:
restdt: restdt:
delivery_failed_with_code: 'Token delivery failed. (Error code %{code})' delivery_failed_with_code: 'Token delivery failed. (Error code %{code})'
strategies:
totp: 'Authenticator application'
sns: 'Amazon SNS'
resdt: 'SMS Rest API'
mobile_transmit_notification: "A one-time password has been sent to your cell phone." mobile_transmit_notification: "A one-time password has been sent to your cell phone."
label_two_factor_authentication: 'Two-factor authentication' label_two_factor_authentication: 'Two-factor authentication'

@ -6,6 +6,9 @@ OpenProject::Application::routes.draw do
post :retry, to: 'authentication#retry' post :retry, to: 'authentication#retry'
get :backup_code, to: 'authentication#enter_backup_code' get :backup_code, to: 'authentication#enter_backup_code'
post :backup_code, to: 'authentication#verify_backup_code' post :backup_code, to: 'authentication#verify_backup_code'
get :settings, to: 'settings#show', as: 'settings_2fa'
post :settings, to: 'settings#update', as: 'update_settings_2fa'
end end
scope 'two_factor_authentication' do # Avoids adding the namespace prefix scope 'two_factor_authentication' do # Avoids adding the namespace prefix

@ -10,11 +10,16 @@ module OpenProject::TwoFactorAuthentication
author_url: 'http://openproject.com', author_url: 'http://openproject.com',
settings: { settings: {
default: { default: {
# Only app-based 2FA allowed per default
# (will be added in token strategy manager)
active_strategies: [],
# Don't force users to register device
enforced: false, enforced: false,
active_strategies: [] # Don't allow remember cookie
allow_remember_for_days: 0
} }
}, },
requires_openproject: '>= 4.0.0' do requires_openproject: '>= 7.2.0' do
menu :my_menu, menu :my_menu,
:two_factor_authentication, :two_factor_authentication,
{ controller: 'two_factor_authentication/my/two_factor_devices', action: :index }, { controller: 'two_factor_authentication/my/two_factor_devices', action: :index },
@ -22,6 +27,14 @@ module OpenProject::TwoFactorAuthentication
after: :password, after: :password,
if: ->(*) { ::OpenProject::TwoFactorAuthentication::TokenStrategyManager.enabled? }, if: ->(*) { ::OpenProject::TwoFactorAuthentication::TokenStrategyManager.enabled? },
icon: 'icon2 icon-types' icon: 'icon2 icon-types'
menu :admin_menu,
:two_factor_authentication,
{ controller: 'two_factor_authentication/settings', action: :show },
caption: ->(*) { I18n.t('two_factor_authentication.label_two_factor_authentication') },
after: :ldap_authentication,
if: ->(*) { ::OpenProject::TwoFactorAuthentication::TokenStrategyManager.configurable_by_ui? },
icon: 'icon2 icon-types'
end end
initializer 'two_factor_authentication.precompile_assets' do |app| initializer 'two_factor_authentication.precompile_assets' do |app|

@ -54,6 +54,12 @@ module OpenProject::TwoFactorAuthentication
!!configuration[:enforced] !!configuration[:enforced]
end end
##
# Determine whether the plugin settings can be changed from the UI
def configurable_by_ui?
!configuration[:hide_settings_menu_item]
end
def allow_remember_for_days def allow_remember_for_days
configuration[:allow_remember_for_days].to_i configuration[:allow_remember_for_days].to_i
end end
@ -68,6 +74,8 @@ module OpenProject::TwoFactorAuthentication
# Fetch all active strategies # Fetch all active strategies
def active_strategies def active_strategies
configuration.fetch(:active_strategies, []) configuration.fetch(:active_strategies, [])
.map(&:to_s)
.uniq
.map { |strategy| lookup_active_strategy strategy } .map { |strategy| lookup_active_strategy strategy }
end end
@ -100,16 +108,32 @@ module OpenProject::TwoFactorAuthentication
config config
end end
def merge_with_settings!(config, settings) def enforced_by_configuration?(key)
# Allow enforcing from settings if not true in configuration (OpenProject::Configuration['2fa'] || {}).has_key? key
unless config[:enforced]
config[:enforced] = settings[:enforced]
end end
def merge_with_settings!(config, settings)
predefined_strategies = config.fetch(:active_strategies, []) predefined_strategies = config.fetch(:active_strategies, [])
additional_strategies = settings.fetch(:active_strategies, []) additional_strategies = settings.fetch(:active_strategies, [])
config[:active_strategies] = predefined_strategies | additional_strategies config[:active_strategies] = predefined_strategies | additional_strategies
# Always enable totp if nothing is enabled
config[:active_strategies] << :totp if add_default_strategy?(config)
# Allow enforcing from settings if not true in configuration
config[:enforced] ||= settings[:enforced]
config[:allow_remember_for_days] = config.fetch(:allow_remember_for_days, settings[:allow_remember_for_days])
end
def add_default_strategy?(config)
config[:active_strategies].empty?
end
def available_strategies
{
totp: I18n.t("activerecord.models.two_factor_authentication/device/totp"),
sns: I18n.t("activerecord.models.two_factor_authentication/device/sms"),
restdt: I18n.t("activerecord.models.two_factor_authentication/device/restdt")
}
end end
def lookup_active_strategy(klazz) def lookup_active_strategy(klazz)

@ -10,10 +10,10 @@ Gem::Specification.new do |s|
s.version = OpenProject::TwoFactorAuthentication::VERSION s.version = OpenProject::TwoFactorAuthentication::VERSION
s.authors = "OpenProject GmbH" s.authors = "OpenProject GmbH"
s.email = "info@openproject.com" s.email = "info@openproject.com"
s.homepage = "https://community.openproject.org/projects/mobile-otp" s.homepage = "https://community.openproject.org/projects/two-factor-authentication"
s.summary = "OpenProject Two-factor authentication" s.summary = "OpenProject Two-factor authentication"
s.description = "This OpenProject plugin authenticates your users using two-factor authentication by means of one-time password " \ s.description = "This OpenProject plugin authenticates your users using two-factor authentication by means of one-time password " \
"through the TOTP standard (Google Authenticator) or sent to the user\'s cell phone via SMS or voice call" "through the TOTP standard (Google Authenticator) or sent to the user's cell phone via SMS or voice call"
s.files = Dir["{app,config,db,lib}/**/*", "CHANGELOG.md", "README.rdoc"] s.files = Dir["{app,config,db,lib}/**/*", "CHANGELOG.md", "README.rdoc"]
s.test_files = Dir["spec/**/*"] s.test_files = Dir["spec/**/*"]

@ -24,6 +24,9 @@ describe ::TwoFactorAuthentication::AuthenticationController, with_2fa_ee: true,
describe 'with no active strategy, but 2FA enforced as configuration', with_config: { '2fa' => { active_strategies: [], enforced: true } } do describe 'with no active strategy, but 2FA enforced as configuration', with_config: { '2fa' => { active_strategies: [], enforced: true } } do
before do before do
allow(OpenProject::TwoFactorAuthentication::TokenStrategyManager)
.to receive(:add_default_strategy?)
.and_return false
session[:authenticated_user_id] = user.id session[:authenticated_user_id] = user.id
get :request_otp get :request_otp
end end

@ -20,6 +20,9 @@ describe ::TwoFactorAuthentication::ForcedRegistration::TwoFactorDevicesControll
allow(OpenProject::Configuration) allow(OpenProject::Configuration)
.to receive(:[]).with('2fa') .to receive(:[]).with('2fa')
.and_return({ active_strategies: active_strategies }.merge(config).with_indifferent_access) .and_return({ active_strategies: active_strategies }.merge(config).with_indifferent_access)
allow(OpenProject::TwoFactorAuthentication::TokenStrategyManager)
.to receive(:add_default_strategy?)
.and_return false
end end
describe 'accessing' do describe 'accessing' do

@ -14,6 +14,9 @@ describe ::TwoFactorAuthentication::My::TwoFactorDevicesController, with_2fa_ee:
allow(OpenProject::Configuration) allow(OpenProject::Configuration)
.to receive(:[]).with('2fa') .to receive(:[]).with('2fa')
.and_return({ active_strategies: active_strategies }.merge(config).with_indifferent_access) .and_return({ active_strategies: active_strategies }.merge(config).with_indifferent_access)
allow(OpenProject::TwoFactorAuthentication::TokenStrategyManager)
.to receive(:add_default_strategy?)
.and_return false
end end
describe 'accessing' do describe 'accessing' do

@ -15,6 +15,9 @@ describe ::TwoFactorAuthentication::Users::TwoFactorDevicesController, with_2fa_
allow(OpenProject::Configuration) allow(OpenProject::Configuration)
.to receive(:[]).with('2fa') .to receive(:[]).with('2fa')
.and_return({ active_strategies: active_strategies }.merge(config).with_indifferent_access) .and_return({ active_strategies: active_strategies }.merge(config).with_indifferent_access)
allow(OpenProject::TwoFactorAuthentication::TokenStrategyManager)
.to receive(:add_default_strategy?)
.and_return false
end end
describe 'accessing' do describe 'accessing' do

@ -122,6 +122,11 @@ describe ::OpenProject::TwoFactorAuthentication::TokenStrategyManager do
subject { described_class.validate_active_strategies! } subject { described_class.validate_active_strategies! }
context 'when no strategy is set' do context 'when no strategy is set' do
let(:active_strategies) { [] } let(:active_strategies) { [] }
before do
allow(OpenProject::TwoFactorAuthentication::TokenStrategyManager)
.to receive(:add_default_strategy?)
.and_return false
end
context 'and enforced is false' do context 'and enforced is false' do
let(:enforced) { false } let(:enforced) { false }

@ -25,6 +25,12 @@ describe ::TwoFactorAuthentication::TokenService, with_2fa_ee: true do
let(:active_strategies) { [] } let(:active_strategies) { [] }
context 'when enforced' do context 'when enforced' do
before do
allow(OpenProject::TwoFactorAuthentication::TokenStrategyManager)
.to receive(:add_default_strategy?)
.and_return false
end
let(:enforced) { true } let(:enforced) { true }
it 'requires a token' do it 'requires a token' do
expect(subject.requires_token?).to be_truthy expect(subject.requires_token?).to be_truthy
@ -38,6 +44,12 @@ describe ::TwoFactorAuthentication::TokenService, with_2fa_ee: true do
context 'when not enforced' do context 'when not enforced' do
let(:enforced) { false } let(:enforced) { false }
before do
allow(OpenProject::TwoFactorAuthentication::TokenStrategyManager)
.to receive(:add_default_strategy?)
.and_return false
end
it 'requires no token' do it 'requires no token' do
expect(subject.requires_token?).to be_falsey expect(subject.requires_token?).to be_falsey
end end

Loading…
Cancel
Save