From a3430f011f1adceaef8484f38a57018712a18ad2 Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Tue, 24 Jan 2017 22:09:58 +0100 Subject: [PATCH] Support 2FA requirement per-group --- app/controllers/admin/groups_controller.rb | 4 +- app/controllers/application_controller.rb | 12 +- app/controllers/groups_controller.rb | 4 +- app/models/group.rb | 11 + app/models/members/group_member.rb | 5 + app/models/user.rb | 9 + app/views/admin/groups/_form.html.haml | 2 +- .../groups/_group_admin_settings.html.haml | 28 +++ .../groups/_group_lfs_settings.html.haml | 11 - app/views/groups/edit.html.haml | 2 +- .../feature-enforce-2fa-per-group.yml | 4 + ...47_add_two_factor_columns_to_namespaces.rb | 21 ++ ...4193205_add_two_factor_columns_to_users.rb | 17 ++ db/schema.rb | 5 + ...o_factor_authentication_group_settings.png | Bin 0 -> 44874 bytes doc/security/two_factor_authentication.md | 17 +- .../application_controller_spec.rb | 201 +++++++++++++++++- spec/models/group_spec.rb | 42 ++++ spec/models/members/group_member_spec.rb | 17 +- spec/models/user_spec.rb | 42 ++++ 20 files changed, 433 insertions(+), 21 deletions(-) create mode 100644 app/views/groups/_group_admin_settings.html.haml delete mode 100644 app/views/groups/_group_lfs_settings.html.haml create mode 100644 changelogs/unreleased/feature-enforce-2fa-per-group.yml create mode 100644 db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb create mode 100644 db/migrate/20170124193205_add_two_factor_columns_to_users.rb create mode 100644 doc/security/img/two_factor_authentication_group_settings.png diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index cea3d088e94..f28bbdeff5a 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -72,7 +72,9 @@ class Admin::GroupsController < Admin::ApplicationController :name, :path, :request_access_enabled, - :visibility_level + :visibility_level, + :require_two_factor_authentication, + :two_factor_grace_period ] end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b197fd2157e..28c4380ca84 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -267,11 +267,19 @@ class ApplicationController < ActionController::Base end def two_factor_authentication_required? - current_application_settings.require_two_factor_authentication + current_application_settings.require_two_factor_authentication || + current_user.try(:require_two_factor_authentication) end def two_factor_grace_period - current_application_settings.two_factor_grace_period + if current_user.try(:require_two_factor_authentication) + [ + current_application_settings.two_factor_grace_period, + current_user.two_factor_grace_period + ].min + else + current_application_settings.two_factor_grace_period + end end def two_factor_grace_period_expired? diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 05f9ee1ee90..5f90df579a8 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -150,7 +150,9 @@ class GroupsController < Groups::ApplicationController :visibility_level, :parent_id, :create_chat_team, - :chat_team_name + :chat_team_name, + :require_two_factor_authentication, + :two_factor_grace_period ] end diff --git a/app/models/group.rb b/app/models/group.rb index 60274386103..106084175ff 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -27,11 +27,14 @@ class Group < Namespace validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } + mount_uploader :avatar, AvatarUploader has_many :uploads, as: :model, dependent: :destroy after_create :post_create_hook after_destroy :post_destroy_hook + after_save :update_two_factor_requirement class << self # Searches for groups matching the given query. @@ -223,4 +226,12 @@ class Group < Namespace type: public? ? 'O' : 'I' # Open vs Invite-only } end + + protected + + def update_two_factor_requirement + return unless require_two_factor_authentication_changed? || two_factor_grace_period_changed? + + users.find_each(&:update_two_factor_requirement) + end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 446f9f8f8a7..483425cd30f 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -3,11 +3,16 @@ class GroupMember < Member belongs_to :group, foreign_key: 'source_id' + delegate :update_two_factor_requirement, to: :user + # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE validates :source_type, format: { with: /\ANamespace\z/ } default_scope { where(source_type: SOURCE_TYPE) } + after_create :update_two_factor_requirement, unless: :invite? + after_destroy :update_two_factor_requirement, unless: :invite? + def self.access_level_roles Gitlab::Access.options_with_owner end diff --git a/app/models/user.rb b/app/models/user.rb index 95a766f2ede..564e99df77b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -963,6 +963,15 @@ class User < ActiveRecord::Base super end + def update_two_factor_requirement + periods = groups.where(require_two_factor_authentication: true).pluck(:two_factor_grace_period) + + self.require_two_factor_authentication = periods.any? + self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period'] + + save + end + private def ci_projects_union diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 589f4557b52..d9f05003904 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -13,7 +13,7 @@ .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f - = render 'groups/group_lfs_settings', f: f + = render 'groups/group_admin_settings', f: f - if @group.new_record? .form-group diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml new file mode 100644 index 00000000000..2ace1e2dd1e --- /dev/null +++ b/app/views/groups/_group_admin_settings.html.haml @@ -0,0 +1,28 @@ +- if current_user.admin? + .form-group + = f.label :lfs_enabled, 'Large File Storage', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled, checked: @group.lfs_enabled? + %strong + Allow projects within this group to use Git LFS + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + %br/ + %span.descr This setting can be overridden in each project. + +- if can? current_user, :admin_group, @group + .form-group + = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :require_two_factor_authentication do + = f.check_box :require_two_factor_authentication + %strong + Require all users in this group to setup Two-factor authentication + = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.text_field :two_factor_grace_period, class: 'form-control' + .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml deleted file mode 100644 index 3c622ca5c3c..00000000000 --- a/app/views/groups/_group_lfs_settings.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- if current_user.admin? - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :lfs_enabled do - = f.check_box :lfs_enabled, checked: @group.lfs_enabled? - %strong - Allow projects within this group to use Git LFS - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - %br/ - %span.descr This setting can be overridden in each project. diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 80a77dab97f..00ff40224ba 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -27,7 +27,7 @@ .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f - = render 'group_lfs_settings', f: f + = render 'group_admin_settings', f: f .form-group %hr diff --git a/changelogs/unreleased/feature-enforce-2fa-per-group.yml b/changelogs/unreleased/feature-enforce-2fa-per-group.yml new file mode 100644 index 00000000000..6dd99e4245f --- /dev/null +++ b/changelogs/unreleased/feature-enforce-2fa-per-group.yml @@ -0,0 +1,4 @@ +--- +title: Support 2FA requirement per-group +merge_request: 8763 +author: Markus Koller diff --git a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb new file mode 100644 index 00000000000..ca4429c676c --- /dev/null +++ b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb @@ -0,0 +1,21 @@ +class AddTwoFactorColumnsToNamespaces < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:namespaces, :require_two_factor_authentication, :boolean, default: false) + add_column_with_default(:namespaces, :two_factor_grace_period, :integer, default: 48) + + add_concurrent_index(:namespaces, :require_two_factor_authentication) + end + + def down + remove_column(:namespaces, :require_two_factor_authentication) + remove_column(:namespaces, :two_factor_grace_period) + + remove_index(:namespaces, :require_two_factor_authentication) if index_exists?(:namespaces, :require_two_factor_authentication) + end +end diff --git a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb new file mode 100644 index 00000000000..bef1b2062c8 --- /dev/null +++ b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb @@ -0,0 +1,17 @@ +class AddTwoFactorColumnsToUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:users, :require_two_factor_authentication, :boolean, default: false) + add_column_with_default(:users, :two_factor_grace_period, :integer, default: 48) + end + + def down + remove_column(:users, :require_two_factor_authentication) + remove_column(:users, :two_factor_grace_period) + end +end diff --git a/db/schema.rb b/db/schema.rb index ccf18d07179..2d2507828d4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -692,6 +692,8 @@ ActiveRecord::Schema.define(version: 20170402231018) do t.text "description_html" t.boolean "lfs_enabled" t.integer "parent_id" + t.boolean "require_two_factor_authentication", default: false, null: false + t.integer "two_factor_grace_period", default: 48, null: false end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree @@ -702,6 +704,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} + add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: :cascade do |t| @@ -1246,6 +1249,8 @@ ActiveRecord::Schema.define(version: 20170402231018) do t.boolean "authorized_projects_populated" t.boolean "ghost" t.boolean "notified_of_own_activity" + t.boolean "require_two_factor_authentication", default: false, null: false + t.integer "two_factor_grace_period", default: 48, null: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/security/img/two_factor_authentication_group_settings.png b/doc/security/img/two_factor_authentication_group_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..a1b3c58bfdc6dfb12e406484a62f3cca83b40b18 GIT binary patch literal 44874 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 literal 0 HcmV?d00001 diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index c8499380c18..f02f7b807cf 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -8,7 +8,7 @@ their phone. You can read more about it here: [Two-factor Authentication (2FA)](../profile/two_factor_authentication.md) -## Enabling 2FA +## Enforcing 2FA for all users Users on GitLab, can enable it without any admin's intervention. If you want to enforce everyone to setup 2FA, you can choose from two different ways: @@ -28,6 +28,21 @@ period to `0`. --- +## Enforcing 2FA for all users in a group + +If you want to enforce 2FA only for certain groups, you can enable it in the +group settings and specify a grace period as above. To change this setting you +need to be administrator or owner of the group. + +If there are multiple 2FA requirements (i.e. group + all users, or multiple +groups) the shortest grace period will be used. + +--- + +![Two factor authentication group settings](img/two_factor_authentication_group_settings.png) + +--- + ## Disabling 2FA for everyone There may be some special situations where you want to disable 2FA for everyone diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 81cbccd5436..64cfb87da5d 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -100,8 +100,6 @@ describe ApplicationController do end describe '#route_not_found' do - let(:controller) { ApplicationController.new } - it 'renders 404 if authenticated' do allow(controller).to receive(:current_user).and_return(user) expect(controller).to receive(:not_found) @@ -115,4 +113,203 @@ describe ApplicationController do controller.send(:route_not_found) end end + + context 'two-factor authentication' do + let(:controller) { ApplicationController.new } + + describe '#check_2fa_requirement' do + subject { controller.send :check_2fa_requirement } + + it 'does not redirect if 2FA is not required' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(false) + expect(controller).not_to receive(:redirect_to) + + subject + end + + it 'does not redirect if user is not logged in' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(true) + allow(controller).to receive(:current_user).and_return(nil) + expect(controller).not_to receive(:redirect_to) + + subject + end + + it 'does not redirect if user has 2FA enabled' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(true) + allow(controller).to receive(:current_user).twice.and_return(user) + allow(user).to receive(:two_factor_enabled?).and_return(true) + expect(controller).not_to receive(:redirect_to) + + subject + end + + it 'does not redirect if 2FA setup can be skipped' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(true) + allow(controller).to receive(:current_user).twice.and_return(user) + allow(user).to receive(:two_factor_enabled?).and_return(false) + allow(controller).to receive(:skip_two_factor?).and_return(true) + expect(controller).not_to receive(:redirect_to) + + subject + end + + it 'redirects to 2FA setup otherwise' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(true) + allow(controller).to receive(:current_user).twice.and_return(user) + allow(user).to receive(:two_factor_enabled?).and_return(false) + allow(controller).to receive(:skip_two_factor?).and_return(false) + allow(controller).to receive(:profile_two_factor_auth_path) + expect(controller).to receive(:redirect_to) + + subject + end + end + + describe '#two_factor_authentication_required?' do + subject { controller.send :two_factor_authentication_required? } + + it 'returns false if no 2FA requirement is present' do + allow(controller).to receive(:current_user).and_return(nil) + + expect(subject).to be_falsey + end + + it 'returns true if a 2FA requirement is set in the application settings' do + stub_application_setting require_two_factor_authentication: true + allow(controller).to receive(:current_user).and_return(nil) + + expect(subject).to be_truthy + end + + it 'returns true if a 2FA requirement is set on the user' do + user.require_two_factor_authentication = true + allow(controller).to receive(:current_user).and_return(user) + + expect(subject).to be_truthy + end + end + + describe '#two_factor_grace_period' do + subject { controller.send :two_factor_grace_period } + + it 'returns the grace period from the application settings' do + stub_application_setting two_factor_grace_period: 23 + allow(controller).to receive(:current_user).and_return(nil) + + expect(subject).to eq 23 + end + + context 'with a 2FA requirement set on the user' do + let(:user) { create :user, require_two_factor_authentication: true, two_factor_grace_period: 23 } + + it 'returns the user grace period if lower than the application grace period' do + stub_application_setting two_factor_grace_period: 24 + allow(controller).to receive(:current_user).and_return(user) + + expect(subject).to eq 23 + end + + it 'returns the application grace period if lower than the user grace period' do + stub_application_setting two_factor_grace_period: 22 + allow(controller).to receive(:current_user).and_return(user) + + expect(subject).to eq 22 + end + end + end + + describe '#two_factor_grace_period_expired?' do + subject { controller.send :two_factor_grace_period_expired? } + + before do + allow(controller).to receive(:current_user).and_return(user) + end + + it 'returns false if the user has not started their grace period yet' do + expect(subject).to be_falsey + end + + context 'with grace period started' do + let(:user) { create :user, otp_grace_period_started_at: 2.hours.ago } + + it 'returns true if the grace period has expired' do + allow(controller).to receive(:two_factor_grace_period).and_return(1) + + expect(subject).to be_truthy + end + + it 'returns false if the grace period is still active' do + allow(controller).to receive(:two_factor_grace_period).and_return(3) + + expect(subject).to be_falsey + end + end + end + + describe '#two_factor_skippable' do + subject { controller.send :two_factor_skippable? } + + before do + allow(controller).to receive(:current_user).and_return(user) + end + + it 'returns false if 2FA is not required' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(false) + + expect(subject).to be_falsey + end + + it 'returns false if the user has already enabled 2FA' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(true) + allow(user).to receive(:two_factor_enabled?).and_return(true) + + expect(subject).to be_falsey + end + + it 'returns false if the 2FA grace period has expired' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(true) + allow(user).to receive(:two_factor_enabled?).and_return(false) + allow(controller).to receive(:two_factor_grace_period_expired?).and_return(true) + + expect(subject).to be_falsey + end + + it 'returns true otherwise' do + allow(controller).to receive(:two_factor_authentication_required?).and_return(true) + allow(user).to receive(:two_factor_enabled?).and_return(false) + allow(controller).to receive(:two_factor_grace_period_expired?).and_return(false) + + expect(subject).to be_truthy + end + end + + describe '#skip_two_factor?' do + subject { controller.send :skip_two_factor? } + + it 'returns false if 2FA setup was not skipped' do + allow(controller).to receive(:session).and_return({}) + + expect(subject).to be_falsey + end + + context 'with 2FA setup skipped' do + before do + allow(controller).to receive(:session).and_return({ skip_tfa: 2.hours.from_now }) + end + + it 'returns false if the grace period has expired' do + Timecop.freeze(3.hours.from_now) do + expect(subject).to be_falsey + end + end + + it 'returns true if the grace period is still active' do + Timecop.freeze(1.hour.from_now) do + expect(subject).to be_truthy + end + end + end + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 5d87938235a..8ffde6f7fbb 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -55,6 +55,8 @@ describe Group, models: true do it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_presence_of :path } it { is_expected.not_to validate_presence_of :owner } + it { is_expected.to validate_presence_of :two_factor_grace_period } + it { is_expected.to validate_numericality_of(:two_factor_grace_period).is_greater_than_or_equal_to(0) } end describe '.visible_to_user' do @@ -315,4 +317,44 @@ describe Group, models: true do to include(master.id, developer.id) end end + + describe '#update_two_factor_requirement' do + let(:user) { create(:user) } + + before do + group.add_user(user, GroupMember::OWNER) + end + + it 'is called when require_two_factor_authentication is changed' do + expect_any_instance_of(User).to receive(:update_two_factor_requirement) + + group.update!(require_two_factor_authentication: true) + end + + it 'is called when two_factor_grace_period is changed' do + expect_any_instance_of(User).to receive(:update_two_factor_requirement) + + group.update!(two_factor_grace_period: 23) + end + + it 'is not called when other attributes are changed' do + expect_any_instance_of(User).not_to receive(:update_two_factor_requirement) + + group.update!(description: 'foobar') + end + + it 'calls #update_two_factor_requirement on each group member' do + other_user = create(:user) + group.add_user(other_user, GroupMember::OWNER) + + calls = 0 + allow_any_instance_of(User).to receive(:update_two_factor_requirement) do + calls += 1 + end + + group.update!(require_two_factor_authentication: true, two_factor_grace_period: 23) + + expect(calls).to eq 2 + end + end end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 370aeb9e0a9..024380b7ebb 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -61,7 +61,7 @@ describe GroupMember, models: true do describe '#after_accept_request' do it 'calls NotificationService.accept_group_access_request' do - member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now) + member = create(:group_member, user: build(:user), requested_at: Time.now) expect_any_instance_of(NotificationService).to receive(:new_group_member) @@ -75,4 +75,19 @@ describe GroupMember, models: true do it { is_expected.to eq 'Group' } end end + + describe '#update_two_factor_requirement' do + let(:user) { build :user } + let(:group_member) { build :group_member, user: user } + + it 'is called after creation and deletion' do + expect(user).to receive(:update_two_factor_requirement) + + group_member.save + + expect(user).to receive(:update_two_factor_requirement) + + group_member.destroy + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a9e37be1157..b2f686a1819 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1521,4 +1521,46 @@ describe User, models: true do end end end + + describe '#update_two_factor_requirement' do + let(:user) { create :user } + + context 'with 2FA requirement on groups' do + let(:group1) { create :group, require_two_factor_authentication: true, two_factor_grace_period: 23 } + let(:group2) { create :group, require_two_factor_authentication: true, two_factor_grace_period: 32 } + + before do + group1.add_user(user, GroupMember::OWNER) + group2.add_user(user, GroupMember::OWNER) + + user.update_two_factor_requirement + end + + it 'requires 2FA' do + expect(user.require_two_factor_authentication).to be true + end + + it 'uses the shortest grace period' do + expect(user.two_factor_grace_period).to be 23 + end + end + + context 'without 2FA requirement on groups' do + let(:group) { create :group } + + before do + group.add_user(user, GroupMember::OWNER) + + user.update_two_factor_requirement + end + + it 'does not require 2FA' do + expect(user.require_two_factor_authentication).to be false + end + + it 'falls back to the default grace period' do + expect(user.two_factor_grace_period).to be 48 + end + end + end end