Commit cf4ba269 authored by Manoj M J's avatar Manoj M J Committed by Imre Farkas

Do not apply instance level PAT expiry policy to group managed accounts

This change makes sure that the instance level PAT expiry policy
settings do not apply to users in group managed accounts.
This is the first step towards implementing
https://gitlab.com/gitlab-org/gitlab/issues/118893
parent 4a0d62b6
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
= render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group = render 'groups/settings/subgroup_creation_level', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f = render 'groups/settings/two_factor_auth', f: f
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render_if_exists 'groups/member_lock_setting', f: f, group: @group = render_if_exists 'groups/member_lock_setting', f: f, group: @group
= f.submit _('Save changes'), class: 'btn btn-success prepend-top-default js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' } = f.submit _('Save changes'), class: 'btn btn-success prepend-top-default js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
...@@ -112,6 +112,32 @@ To access the Credentials inventory of a group, navigate to **{shield}** **Secur ...@@ -112,6 +112,32 @@ To access the Credentials inventory of a group, navigate to **{shield}** **Secur
This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md). This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md).
##### Limiting lifetime of personal access tokens of users in Group-managed accounts **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118893) in GitLab 12.10.
Users in a group managed account can optionally specify an expiration date for
[personal access tokens](../../profile/personal_access_tokens.md).
This expiration date is not a requirement, and can be set to any arbitrary date.
Since personal access tokens are the only token needed for programmatic access to GitLab, organizations with security requirements may want to enforce more protection to require regular rotation of these tokens.
###### Setting a limit
Only a GitLab administrator or an owner of a Group-managed account can set a limit. Leaving it empty means that the [instance level restrictions](../../admin_area/settings/account_and_limit_settings.md#limiting-lifetime-of-personal-access-tokens-ultimate-only) on the lifetime of personal access tokens will apply.
To set a limit on how long personal access tokens are valid for users in a group managed account:
1. Navigate to the **{settings}** **Settings > General** page in your group's sidebar.
1. Expand the **Permissions, LFS, 2FA** section.
1. Fill in the **Maximum allowable lifetime for personal access tokens (days)** field.
1. Click **Save changes**.
Once a lifetime for personal access tokens is set, GitLab will:
- Apply the lifetime for new personal access tokens, and require users managed by the group to set an expiration date that is no later than the allowed lifetime.
- After three hours, revoke old tokens with no expiration date or with a lifetime longer than the allowed lifetime. Three hours is given to allow administrators/group owner to change the allowed lifetime, or remove it, before revocation takes place.
##### Outer forks restriction for Group-managed accounts ##### Outer forks restriction for Group-managed accounts
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9.
......
...@@ -66,6 +66,7 @@ module EE ...@@ -66,6 +66,7 @@ module EE
params_ee << :ip_restriction_ranges if current_group&.feature_available?(:group_ip_restriction) params_ee << :ip_restriction_ranges if current_group&.feature_available?(:group_ip_restriction)
params_ee << { allowed_email_domain_attributes: [:id, :domain] } if current_group&.feature_available?(:group_allowed_email_domains) params_ee << { allowed_email_domain_attributes: [:id, :domain] } if current_group&.feature_available?(:group_allowed_email_domains)
params_ee << :max_pages_size if can?(current_user, :update_max_pages_size) params_ee << :max_pages_size if can?(current_user, :update_max_pages_size)
params_ee << :max_personal_access_token_lifetime if current_group&.personal_access_token_expiration_policy_available?
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module PersonalAccessTokensHelper module PersonalAccessTokensHelper
include Gitlab::Utils::StrongMemoize
def personal_access_token_expiration_policy_enabled?
return group_level_personal_access_token_expiration_policy_enabled? if current_user.group_managed_account?
instance_level_personal_access_token_expiration_policy_enabled?
end
def personal_access_token_max_expiry_date
return group_level_personal_access_token_max_expiry_date if current_user.group_managed_account?
instance_level_personal_access_token_max_expiry_date
end
def personal_access_token_expiration_policy_licensed? def personal_access_token_expiration_policy_licensed?
License.feature_available?(:personal_access_token_expiration_policy) License.feature_available?(:personal_access_token_expiration_policy)
end end
def personal_access_token_expiration_policy_enabled? private
Gitlab::CurrentSettings.max_personal_access_token_lifetime && personal_access_token_expiration_policy_licensed?
def instance_level_personal_access_token_expiration_policy_enabled?
instance_level_personal_access_token_max_expiry_date && personal_access_token_expiration_policy_licensed?
end
def instance_level_personal_access_token_max_expiry_date
::Gitlab::CurrentSettings.max_personal_access_token_lifetime_from_now
end
def group_level_personal_access_token_expiration_policy_enabled?
group_level_personal_access_token_max_expiry_date && personal_access_token_expiration_policy_licensed?
end
def group_level_personal_access_token_max_expiry_date
current_user.managing_group.max_personal_access_token_lifetime_from_now
end end
end end
...@@ -274,7 +274,7 @@ module EE ...@@ -274,7 +274,7 @@ module EE
def update_personal_access_tokens_lifetime def update_personal_access_tokens_lifetime
return unless max_personal_access_token_lifetime.present? && License.feature_available?(:personal_access_token_expiration_policy) return unless max_personal_access_token_lifetime.present? && License.feature_available?(:personal_access_token_expiration_policy)
::PersonalAccessTokens::UpdateLifetimeService.new.execute ::PersonalAccessTokens::Instance::UpdateLifetimeService.new.execute
end end
def mirror_max_delay_in_minutes def mirror_max_delay_in_minutes
......
...@@ -56,6 +56,10 @@ module EE ...@@ -56,6 +56,10 @@ module EE
validates :repository_size_limit, validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true } numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
validates :max_personal_access_token_lifetime,
allow_blank: true,
numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 365 }
validate :custom_project_templates_group_allowed, if: :custom_project_templates_group_id_changed? validate :custom_project_templates_group_allowed, if: :custom_project_templates_group_id_changed?
scope :aimed_for_deletion, -> (date) { joins(:deletion_schedule).where('group_deletion_schedules.marked_for_deletion_on <= ?', date) } scope :aimed_for_deletion, -> (date) { joins(:deletion_schedule).where('group_deletion_schedules.marked_for_deletion_on <= ?', date) }
...@@ -65,6 +69,17 @@ module EE ...@@ -65,6 +69,17 @@ module EE
joins(:ldap_group_links).where(ldap_group_links: { provider: provider }) joins(:ldap_group_links).where(ldap_group_links: { provider: provider })
end end
scope :with_managed_accounts_enabled, -> {
joins(:saml_provider).where(saml_providers:
{
enabled: true,
enforced_sso: true,
enforced_group_managed_accounts: true
})
}
scope :with_no_pat_expiry_policy, -> { where(max_personal_access_token_lifetime: nil) }
scope :with_project_templates, -> { where.not(custom_project_templates_group_id: nil) } scope :with_project_templates, -> { where.not(custom_project_templates_group_id: nil) }
scope :with_custom_file_templates, -> do scope :with_custom_file_templates, -> do
...@@ -311,6 +326,24 @@ module EE ...@@ -311,6 +326,24 @@ module EE
) )
end end
def max_personal_access_token_lifetime_from_now
if max_personal_access_token_lifetime.present?
max_personal_access_token_lifetime.days.from_now
else
::Gitlab::CurrentSettings.max_personal_access_token_lifetime_from_now
end
end
def personal_access_token_expiration_policy_available?
enforced_group_managed_accounts? && License.feature_available?(:personal_access_token_expiration_policy)
end
def update_personal_access_tokens_lifetime
return unless max_personal_access_token_lifetime.present? && personal_access_token_expiration_policy_available?
::PersonalAccessTokens::Groups::UpdateLifetimeService.new(self).execute
end
private private
def custom_project_templates_group_allowed def custom_project_templates_group_allowed
......
...@@ -15,9 +15,9 @@ module EE ...@@ -15,9 +15,9 @@ module EE
scope :with_no_expires_at, -> { where(revoked: false, expires_at: nil) } scope :with_no_expires_at, -> { where(revoked: false, expires_at: nil) }
scope :with_expires_at_after, ->(max_lifetime) { where(revoked: false).where('expires_at > ?', max_lifetime) } scope :with_expires_at_after, ->(max_lifetime) { where(revoked: false).where('expires_at > ?', max_lifetime) }
with_options if: :max_personal_access_token_lifetime_enabled? do with_options if: :expiration_policy_enabled? do
validates :expires_at, presence: true validates :expires_at, presence: true
validate :expires_at_before_max_lifetime validate :expires_at_before_max_expiry_date
end end
end end
...@@ -38,20 +38,42 @@ module EE ...@@ -38,20 +38,42 @@ module EE
private private
def max_expiration_date def expiration_policy_enabled?
strong_memoize(:max_expiration_date) do return group_level_expiration_policy_enabled? if user.group_managed_account?
::Gitlab::CurrentSettings.max_personal_access_token_lifetime_from_now
end instance_level_expiration_policy_enabled?
end
def instance_level_expiration_policy_enabled?
expiration_policy_licensed? && instance_level_max_expiry_date
end
def max_expiry_date
return group_level_max_expiry_date if user.group_managed_account?
instance_level_max_expiry_date
end end
def max_personal_access_token_lifetime_enabled? def instance_level_max_expiry_date
max_expiration_date && License.feature_available?(:personal_access_token_expiration_policy) ::Gitlab::CurrentSettings.max_personal_access_token_lifetime_from_now
end end
def expires_at_before_max_lifetime def expires_at_before_max_expiry_date
return if expires_at.blank? return if expires_at.blank?
errors.add(:expires_at, :invalid) if expires_at > max_expiration_date errors.add(:expires_at, :invalid) if expires_at > max_expiry_date
end
def expiration_policy_licensed?
License.feature_available?(:personal_access_token_expiration_policy)
end
def group_level_expiration_policy_enabled?
expiration_policy_licensed? && group_level_max_expiry_date
end
def group_level_max_expiry_date
user.managing_group.max_personal_access_token_lifetime_from_now
end end
end end
end end
...@@ -66,6 +66,8 @@ module EE ...@@ -66,6 +66,8 @@ module EE
scope scope
} }
scope :managed_by, ->(group) { where(managing_group: group) }
scope :excluding_guests, -> { joins(:members).merge(::Member.non_guests).distinct } scope :excluding_guests, -> { joins(:members).merge(::Member.non_guests).distinct }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) } scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
......
...@@ -23,6 +23,15 @@ module EE ...@@ -23,6 +23,15 @@ module EE
private private
override :after_update
def after_update
super
if group.saved_change_to_max_personal_access_token_lifetime?
group.update_personal_access_tokens_lifetime
end
end
override :before_assignment_hook override :before_assignment_hook
def before_assignment_hook(group, params) def before_assignment_hook(group, params)
# Repository size limit comes as MB from the view # Repository size limit comes as MB from the view
......
# frozen_string_literal: true
module PersonalAccessTokens
module Groups
class UpdateLifetimeService < PersonalAccessTokens::Instance::UpdateLifetimeService
extend ::Gitlab::Utils::Override
def initialize(group)
@group = group
end
private
attr_reader :group
override :perform
def perform
::PersonalAccessTokens::Groups::PolicyWorker.perform_in(DEFAULT_LEASE_TIMEOUT, group.id)
end
# Used by ExclusiveLeaseGuard
# This should be unique per group
override :lease_key
def lease_key
"#{super}:group_id:#{group.id}"
end
end
end
end
# frozen_string_literal: true
module PersonalAccessTokens
module Instance
class UpdateLifetimeService
include ExclusiveLeaseGuard
DEFAULT_LEASE_TIMEOUT = 3.hours.to_i
def execute
try_obtain_lease do
perform
end
end
private
def perform
::PersonalAccessTokens::Instance::PolicyWorker.perform_in(DEFAULT_LEASE_TIMEOUT)
end
# Used by ExclusiveLeaseGuard
def lease_timeout
DEFAULT_LEASE_TIMEOUT
end
# Used by ExclusiveLeaseGuard
# Overriding value as we never release the lease
# before the timeout in order to prevent multiple
# PersonalAccessTokens::Instance::PolicyWorker to start in
# a short span of time
def lease_release?
false
end
end
end
end
# frozen_string_literal: true
module PersonalAccessTokens
class UpdateLifetimeService
include ExclusiveLeaseGuard
DEFAULT_LEASE_TIMEOUT = 3.hours.to_i
def execute
try_obtain_lease do
::PersonalAccessTokens::PolicyWorker.perform_in(DEFAULT_LEASE_TIMEOUT)
end
end
private
# Used by ExclusiveLeaseGuard
def lease_timeout
DEFAULT_LEASE_TIMEOUT
end
# Used by ExclusiveLeaseGuard
# Overriding value as we never release the lease
# before the timeout in order to prevent multiple
# PersonalAccessTokens::PolicyWorker to start in
# a short span of time
def lease_release?
false
end
end
end
- return unless group.personal_access_token_expiration_policy_available?
- instance_level_policy = ::Gitlab::CurrentSettings.max_personal_access_token_lifetime
- instance_level_policy_in_words = instance_level_policy ? n_("%{no_of_days} day", "%{no_of_days} days", instance_level_policy) % { no_of_days: instance_level_policy } : _('no expiration')
.form-group
= f.label :max_personal_access_token_lifetime, _('Maximum allowable lifetime for personal access token (days)'), class: 'label-light'
= f.number_field :max_personal_access_token_lifetime, class: 'form-control form-control-sm w-auto'
%span.form-text.text-muted#max_personal_access_token_lifetime= _('If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked.') % { instance_level_policy_in_words: instance_level_policy_in_words }
- return unless personal_access_token_expiration_policy_enabled? - return unless personal_access_token_expiration_policy_enabled?
.bs-callout.bs-callout-danger .bs-callout.bs-callout-danger
= _('Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}.') % { maximum_allowable_date: ::Gitlab::CurrentSettings.max_personal_access_token_lifetime_from_now } = _('Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}.') % { maximum_allowable_date: personal_access_token_max_expiry_date.to_date }
...@@ -402,6 +402,20 @@ ...@@ -402,6 +402,20 @@
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent:
- :name: personal_access_tokens:personal_access_tokens_groups_policy
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :name: personal_access_tokens:personal_access_tokens_instance_policy
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: personal_access_tokens:personal_access_tokens_policy - :name: personal_access_tokens:personal_access_tokens_policy
:feature_category: :authentication_and_authorization :feature_category: :authentication_and_authorization
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module PersonalAccessTokens
module Groups
class PolicyWorker
include ApplicationWorker
idempotent!
queue_namespace :personal_access_tokens
feature_category :authentication_and_authorization
def perform(group_id)
group = ::Group.find_by_id(group_id)
return unless group
expiration_date = group.max_personal_access_token_lifetime_from_now
return unless expiration_date
::User.managed_by(group).with_invalid_expires_at_tokens(expiration_date).find_each do |user|
PersonalAccessTokens::RevokeInvalidTokens.new(user, expiration_date).execute
end
end
end
end
end
# frozen_string_literal: true
module PersonalAccessTokens
module Instance
class PolicyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
queue_namespace :personal_access_tokens
feature_category :authentication_and_authorization
def perform
expiration_date = ::Gitlab::CurrentSettings.max_personal_access_token_lifetime_from_now
return unless expiration_date
# for users who are not managed by any group
User.not_managed.with_invalid_expires_at_tokens(expiration_date).find_each do |user|
PersonalAccessTokens::RevokeInvalidTokens.new(user, expiration_date).execute
end
# for users who are managed by groups, but these groups follow the instance level expiry policy
::Group.with_managed_accounts_enabled.with_no_pat_expiry_policy.find_each do |group|
User.managed_by(group).with_invalid_expires_at_tokens(expiration_date).find_each do |user|
PersonalAccessTokens::RevokeInvalidTokens.new(user, expiration_date).execute
end
end
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/213791
# Deprecate this worker in GitLab 13.0 in favor of PersonalAccessTokens::Instance::PolicyWorker
module PersonalAccessTokens module PersonalAccessTokens
class PolicyWorker # rubocop:disable Scalability/IdempotentWorker class PolicyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker include ApplicationWorker
......
---
title: Allow GMA groups to specify their own PAT expiry setting
merge_request: 25963
author:
type: added
...@@ -293,5 +293,81 @@ describe GroupsController do ...@@ -293,5 +293,81 @@ describe GroupsController do
end end
end end
end end
context 'when `max_personal_access_token_lifetime` is specified' do
let!(:managed_group) do
create(:group_with_managed_accounts, :private, max_personal_access_token_lifetime: 1)
end
let(:user) { create(:user, :group_managed, managing_group: managed_group ) }
let(:params) { { max_personal_access_token_lifetime: max_personal_access_token_lifetime } }
let(:max_personal_access_token_lifetime) { 10 }
subject do
put :update, params: { id: managed_group.to_param, group: params }
end
before do
allow_any_instance_of(EE::Group).to receive(:enforced_group_managed_accounts?).and_return(true)
managed_group.add_owner(user)
sign_in(user)
end
context 'without `personal_access_token_expiration_policy` licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: false)
end
it 'does not update the attribute' do
expect { subject }.not_to change { managed_group.reload.max_personal_access_token_lifetime }
end
it "doesn't call the update lifetime service" do
expect(::PersonalAccessTokens::Groups::UpdateLifetimeService).not_to receive(:new)
subject
end
end
context 'with personal_access_token_expiration_policy licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: true)
end
context 'when `max_personal_access_token_lifetime` is updated to a non-null value' do
it 'updates the attribute' do
subject
expect(managed_group.reload.max_personal_access_token_lifetime).to eq(max_personal_access_token_lifetime)
end
it 'executes the update lifetime service' do
expect_next_instance_of(::PersonalAccessTokens::Groups::UpdateLifetimeService, managed_group) do |service|
expect(service).to receive(:execute)
end
subject
end
end
context 'when `max_personal_access_token_lifetime` is updated to null value' do
let(:max_personal_access_token_lifetime) { nil }
it 'updates the attribute' do
subject
expect(managed_group.reload.max_personal_access_token_lifetime).to eq(max_personal_access_token_lifetime)
end
it "doesn't call the update lifetime service" do
expect(::PersonalAccessTokens::Groups::UpdateLifetimeService).not_to receive(:new)
subject
end
end
end
end
end end
end end
...@@ -3,50 +3,144 @@ ...@@ -3,50 +3,144 @@
require 'spec_helper' require 'spec_helper'
describe PersonalAccessTokensHelper do describe PersonalAccessTokensHelper do
describe '#personal_access_token_expiration_policy_licensed?' do let(:group) do
subject { helper.personal_access_token_expiration_policy_licensed? } build(:group, max_personal_access_token_lifetime: group_level_max_personal_access_token_lifetime)
end
let(:group_level_max_personal_access_token_lifetime) { nil }
let(:instance_level_max_personal_access_token_lifetime) { nil }
let(:user) { build(:user) }
let(:managed_user) { build(:user, managing_group: group) }
before do
allow(helper).to receive(:current_user) { user }
stub_application_setting(max_personal_access_token_lifetime: instance_level_max_personal_access_token_lifetime)
end
describe '#personal_access_token_expiration_policy_enabled?' do
subject { helper.personal_access_token_expiration_policy_enabled? }
context 'when is not licensed' do context 'with `personal_access_token_expiration_policy` licensed' do
before do before do
stub_licensed_features(personal_access_token_expiration_policy: false) stub_licensed_features(personal_access_token_expiration_policy: true)
end end
it { is_expected.to be_falsey } shared_examples_for 'instance level PAT expiry setting' do
context 'the instance has an expiry setting' do
let(:instance_level_max_personal_access_token_lifetime) { 20 }
it { is_expected.to be_truthy }
end
context 'the instance does not have an expiry setting' do
it { is_expected.to be_falsey }
end
end
context 'when the current user belongs to a managed group' do
let(:user) { managed_user }
context 'when the managed group has a PAT expiry policy' do
let(:group_level_max_personal_access_token_lifetime) { 10 }
it { is_expected.to be_truthy }
end
context 'when the managed group does not have a PAT expiry setting' do
it_behaves_like 'instance level PAT expiry setting'
end
end
context 'when the current user does not belong to a managed group' do
it_behaves_like 'instance level PAT expiry setting'
end
end end
context 'when is licensed' do context 'with `personal_access_token_expiration_policy` not licensed' do
before do before do
stub_licensed_features(personal_access_token_expiration_policy: true) stub_licensed_features(personal_access_token_expiration_policy: false)
end end
it { is_expected.to be_truthy } shared_examples_for 'instance level PAT expiry setting' do
context 'the instance has an expiry setting' do
let(:instance_level_max_personal_access_token_lifetime) { 20 }
it { is_expected.to be_falsey }
end
context 'the instance does not have an expiry setting' do
it { is_expected.to be_falsey }
end
end
context 'when the current user belongs to a managed group' do
let(:user) { managed_user }
context 'when the managed group has a PAT expiry policy' do
let(:group_level_max_personal_access_token_lifetime) { 10 }
it { is_expected.to be_falsey }
end
context 'when the managed group does not have a PAT expiry setting' do
it_behaves_like 'instance level PAT expiry setting'
end
end
context 'when the current user does not belong to a managed group' do
it_behaves_like 'instance level PAT expiry setting'
end
end end
end end
describe '#personal_access_token_expiration_policy_enabled?' do describe '#personal_access_token_max_expiry_date' do
subject { helper.personal_access_token_expiration_policy_enabled? } subject { helper.personal_access_token_max_expiry_date }
context 'when is licensed and used' do shared_examples_for 'instance level PAT expiry setting' do
before do context 'the instance has an expiry setting' do
stub_licensed_features(personal_access_token_expiration_policy: true) let(:instance_level_max_personal_access_token_lifetime) { 20 }
stub_application_setting(max_personal_access_token_lifetime: 1)
it { is_expected.to be_like_time(20.days.from_now) }
end end
it { is_expected.to be_truthy } context 'the instance does not have an expiry setting' do
it { is_expected.to be_nil }
end
end end
context 'when is not licensed' do context 'when the current user belongs to a managed group' do
before do let(:user) { managed_user }
stub_licensed_features(personal_access_token_expiration_policy: false)
context 'when the managed group has a PAT expiry policy' do
let(:group_level_max_personal_access_token_lifetime) { 10 }
it { is_expected.to be_like_time(10.days.from_now) }
end end
it { is_expected.to be_falsey } context 'when the managed group does not have a PAT expiry setting' do
it_behaves_like 'instance level PAT expiry setting'
end
end
context 'when the current user does not belong to a managed group' do
it_behaves_like 'instance level PAT expiry setting'
end end
end
describe '#personal_access_token_expiration_policy_licensed?' do
subject { helper.personal_access_token_expiration_policy_licensed? }
context 'when is licensed but not used' do context 'with `personal_access_token_expiration_policy` licensed' do
before do before do
stub_licensed_features(personal_access_token_expiration_policy: true) stub_licensed_features(personal_access_token_expiration_policy: true)
stub_application_setting(max_personal_access_token_lifetime: nil) end
it { is_expected.to be_truthy }
end
context 'with `personal_access_token_expiration_policy` not licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: false)
end end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
......
...@@ -514,7 +514,7 @@ describe ApplicationSetting do ...@@ -514,7 +514,7 @@ describe ApplicationSetting do
end end
it "doesn't call the update lifetime service" do it "doesn't call the update lifetime service" do
expect(::PersonalAccessTokens::UpdateLifetimeService).not_to receive(:new) expect(::PersonalAccessTokens::Instance::UpdateLifetimeService).not_to receive(:new)
setting.save setting.save
end end
...@@ -527,7 +527,7 @@ describe ApplicationSetting do ...@@ -527,7 +527,7 @@ describe ApplicationSetting do
end end
it 'executes the update lifetime service' do it 'executes the update lifetime service' do
expect_next_instance_of(::PersonalAccessTokens::UpdateLifetimeService) do |service| expect_next_instance_of(::PersonalAccessTokens::Instance::UpdateLifetimeService) do |service|
expect(service).to receive(:execute) expect(service).to receive(:execute)
end end
......
...@@ -46,7 +46,8 @@ describe PersonalAccessToken do ...@@ -46,7 +46,8 @@ describe PersonalAccessToken do
end end
describe 'validations' do describe 'validations' do
let(:personal_access_token) { build(:personal_access_token) } let(:user) { build(:user) }
let(:personal_access_token) { build(:personal_access_token, user: user) }
it 'allows to define expires_at' do it 'allows to define expires_at' do
personal_access_token.expires_at = 1.day.from_now personal_access_token.expires_at = 1.day.from_now
...@@ -61,18 +62,28 @@ describe PersonalAccessToken do ...@@ -61,18 +62,28 @@ describe PersonalAccessToken do
end end
context 'with expiration policy' do context 'with expiration policy' do
let(:pat_expiration_policy) { 30 } let(:instance_level_pat_expiration_policy) { 30 }
let(:max_expiration_date) { pat_expiration_policy.days.from_now } let(:instance_level_max_expiration_date) { instance_level_pat_expiration_policy.days.from_now }
before do before do
stub_ee_application_setting(max_personal_access_token_lifetime: pat_expiration_policy) stub_ee_application_setting(max_personal_access_token_lifetime: instance_level_pat_expiration_policy)
end end
context 'when the feature is licensed' do shared_examples_for 'PAT expiry rules are not enforced' do
before do it 'allows expiry to be after the max_personal_access_token_lifetime' do
stub_licensed_features(personal_access_token_expiration_policy: true) personal_access_token.expires_at = max_expiration_date + 1.day
expect(personal_access_token).to be_valid
end end
it 'can be blank' do
personal_access_token.expires_at = nil
expect(personal_access_token).to be_valid
end
end
shared_examples_for 'PAT expiry rules are enforced' do
it 'requires to be less or equal than the max_personal_access_token_lifetime' do it 'requires to be less or equal than the max_personal_access_token_lifetime' do
personal_access_token.expires_at = max_expiration_date + 1.day personal_access_token.expires_at = max_expiration_date + 1.day
...@@ -88,15 +99,61 @@ describe PersonalAccessToken do ...@@ -88,15 +99,61 @@ describe PersonalAccessToken do
end end
end end
context 'when the feature is licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: true)
end
context 'when the user does not belong to a managed group' do
it_behaves_like 'PAT expiry rules are enforced' do
let(:max_expiration_date) { instance_level_max_expiration_date }
end
end
context 'when the user belongs to a managed group' do
let(:group_level_pat_expiration_policy) { nil }
let(:group) do
build(:group_with_managed_accounts, max_personal_access_token_lifetime: group_level_pat_expiration_policy)
end
let(:user) { build(:user, managing_group: group) }
context 'when the group has enforced a PAT expiry rule' do
let(:group_level_pat_expiration_policy) { 20 }
let(:group_level_max_expiration_date) { group_level_pat_expiration_policy.days.from_now }
it_behaves_like 'PAT expiry rules are enforced' do
let(:max_expiration_date) { group_level_max_expiration_date }
end
end
context 'when the group has not enforced a PAT expiry setting' do
context 'when the instance has enforced a PAT expiry setting' do
it_behaves_like 'PAT expiry rules are enforced' do
let(:max_expiration_date) { instance_level_max_expiration_date }
end
end
context 'when the instance does not enforce a PAT expiry setting' do
before do
stub_ee_application_setting(max_personal_access_token_lifetime: nil)
end
it_behaves_like 'PAT expiry rules are not enforced' do
let(:max_expiration_date) { instance_level_max_expiration_date }
end
end
end
end
end
context 'when the feature is not available' do context 'when the feature is not available' do
before do before do
stub_licensed_features(personal_access_token_expiration_policy: false) stub_licensed_features(personal_access_token_expiration_policy: false)
end end
it 'allows to be after the max_personal_access_token_lifetime' do it_behaves_like 'PAT expiry rules are not enforced' do
personal_access_token.expires_at = max_expiration_date + 1.day let(:max_expiration_date) { instance_level_max_expiration_date }
expect(personal_access_token).to be_valid
end end
end end
end end
......
...@@ -78,9 +78,42 @@ describe Group do ...@@ -78,9 +78,42 @@ describe Group do
expect(described_class.for_epics(epics)).to contain_exactly(epic1.group) expect(described_class.for_epics(epics)).to contain_exactly(epic1.group)
end end
end end
describe '.with_managed_accounts_enabled' do
subject { described_class.with_managed_accounts_enabled }
let!(:group_with_with_managed_accounts_enabled) { create(:group_with_managed_accounts) }
let!(:group_without_managed_accounts_enabled) { create(:group) }
it 'includes the groups that has managed accounts enabled' do
expect(subject).to contain_exactly(group_with_with_managed_accounts_enabled)
end
end
describe '.with_no_pat_expiry_policy' do
subject { described_class.with_no_pat_expiry_policy }
let!(:group_with_pat_expiry_policy) { create(:group, max_personal_access_token_lifetime: 1) }
let!(:group_with_no_pat_expiry_policy) { create(:group, max_personal_access_token_lifetime: nil) }
it 'includes the groups that has no PAT expiry policy set' do
expect(subject).to contain_exactly(group_with_no_pat_expiry_policy)
end
end
end end
describe 'validations' do describe 'validations' do
context 'max_personal_access_token_lifetime' do
it { is_expected.to allow_value(1).for(:max_personal_access_token_lifetime) }
it { is_expected.to allow_value(nil).for(:max_personal_access_token_lifetime) }
it { is_expected.to allow_value(10).for(:max_personal_access_token_lifetime) }
it { is_expected.to allow_value(365).for(:max_personal_access_token_lifetime) }
it { is_expected.not_to allow_value("value").for(:max_personal_access_token_lifetime) }
it { is_expected.not_to allow_value(2.5).for(:max_personal_access_token_lifetime) }
it { is_expected.not_to allow_value(-5).for(:max_personal_access_token_lifetime) }
it { is_expected.not_to allow_value(366).for(:max_personal_access_token_lifetime) }
end
context 'validates if custom_project_templates_group_id is allowed' do context 'validates if custom_project_templates_group_id is allowed' do
let(:subgroup_1) { create(:group, parent: group) } let(:subgroup_1) { create(:group, parent: group) }
...@@ -757,4 +790,120 @@ describe Group do ...@@ -757,4 +790,120 @@ describe Group do
end end
end end
end end
describe '#personal_access_token_expiration_policy_available?' do
subject { group.personal_access_token_expiration_policy_available? }
let(:group) { build(:group) }
context 'when the group does not enforce managed accounts' do
it { is_expected.to be_falsey }
end
context 'when the group enforces managed accounts' do
before do
allow(group).to receive(:enforced_group_managed_accounts?).and_return(true)
end
context 'with `personal_access_token_expiration_policy` licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: true)
end
it { is_expected.to be_truthy }
end
context 'with `personal_access_token_expiration_policy` not licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: false)
end
it { is_expected.to be_falsey }
end
end
end
describe '#update_personal_access_tokens_lifetime' do
subject { group.update_personal_access_tokens_lifetime }
let(:limit) { 1 }
let(:group) { build(:group, max_personal_access_token_lifetime: limit) }
shared_examples_for 'it does not call the update lifetime service' do
it 'doesn not call the update lifetime service' do
expect(::PersonalAccessTokens::Groups::UpdateLifetimeService).not_to receive(:new)
subject
end
end
context 'when the group does not enforce managed accounts' do
it_behaves_like 'it does not call the update lifetime service'
end
context 'when the group enforces managed accounts' do
before do
allow(group).to receive(:enforced_group_managed_accounts?).and_return(true)
end
context 'with `personal_access_token_expiration_policy` not licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: false)
end
it_behaves_like 'it does not call the update lifetime service'
end
context 'with `personal_access_token_expiration_policy` licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: true)
end
context 'when the group does not enforce a PAT expiry policy' do
let(:limit) { nil }
it_behaves_like 'it does not call the update lifetime service'
end
context 'when the group enforces a PAT expiry policy' do
it 'executes the update lifetime service' do
expect_next_instance_of(::PersonalAccessTokens::Groups::UpdateLifetimeService, group) do |service|
expect(service).to receive(:execute)
end
subject
end
end
end
end
end
describe '#max_personal_access_token_lifetime_from_now' do
subject { group.max_personal_access_token_lifetime_from_now }
let(:days_from_now) { nil }
let(:group) { build(:group, max_personal_access_token_lifetime: days_from_now) }
context 'when max_personal_access_token_lifetime is defined' do
let(:days_from_now) { 30 }
it 'is a date time' do
expect(subject).to be_a Time
end
it 'is in the future' do
expect(subject).to be > Time.zone.now
end
it 'is in days_from_now' do
expect(subject.to_date - Date.today).to eq days_from_now
end
end
context 'when max_personal_access_token_lifetime is nil' do
it 'is nil' do
expect(subject).to be_nil
end
end
end
end end
...@@ -111,6 +111,15 @@ describe User do ...@@ -111,6 +111,15 @@ describe User do
expect(non_internal.all?(&:internal?)).to eq(false) expect(non_internal.all?(&:internal?)).to eq(false)
end end
end end
describe '.managed_by' do
let!(:group) { create(:group_with_managed_accounts) }
let!(:managed_users) { create_list(:user, 2, managing_group: group) }
it 'returns users managed by the specified group' do
expect(described_class.managed_by(group)).to match_array(managed_users)
end
end
end end
describe '.find_by_smartcard_identity' do describe '.find_by_smartcard_identity' do
......
...@@ -328,6 +328,69 @@ describe Groups::UpdateService, '#execute' do ...@@ -328,6 +328,69 @@ describe Groups::UpdateService, '#execute' do
end end
end end
context 'updating `max_personal_access_token_lifetime` param' do
subject { update_group(group, user, attrs) }
let!(:group) do
create(:group_with_managed_accounts, :public, max_personal_access_token_lifetime: 1)
end
let(:limit) { 10 }
let(:attrs) { { max_personal_access_token_lifetime: limit } }
shared_examples_for 'it does not call the update lifetime service' do
it 'doesn not call the update lifetime service' do
expect(::PersonalAccessTokens::Groups::UpdateLifetimeService).not_to receive(:new)
subject
end
end
it 'updates the attribute' do
expect { subject }.to change { group.reload.max_personal_access_token_lifetime }.from(1).to(10)
end
context 'when the group does not enforce managed accounts' do
it_behaves_like 'it does not call the update lifetime service'
end
context 'when the group enforces managed accounts' do
before do
allow(group).to receive(:enforced_group_managed_accounts?).and_return(true)
end
context 'without `personal_access_token_expiration_policy` licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: false)
end
it_behaves_like 'it does not call the update lifetime service'
end
context 'with personal_access_token_expiration_policy licensed' do
before do
stub_licensed_features(personal_access_token_expiration_policy: true)
end
context 'when `max_personal_access_token_lifetime` is updated to null value' do
let(:limit) { nil }
it_behaves_like 'it does not call the update lifetime service'
end
context 'when `max_personal_access_token_lifetime` is updated to a non-null value' do
it 'executes the update lifetime service' do
expect_next_instance_of(::PersonalAccessTokens::Groups::UpdateLifetimeService, group) do |service|
expect(service).to receive(:execute)
end
subject
end
end
end
end
end
def update_group(group, user, opts) def update_group(group, user, opts)
Groups::UpdateService.new(group, user, opts).execute Groups::UpdateService.new(group, user, opts).execute
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe PersonalAccessTokens::Groups::UpdateLifetimeService do
include ExclusiveLeaseHelpers
describe '#execute', :clean_gitlab_redis_shared_state do
let_it_be(:group) { create(:group_with_managed_accounts)}
subject { described_class.new(group) }
let(:lease_key) { "personal_access_tokens/groups/update_lifetime_service:group_id:#{group.id}" }
context 'when we can obtain the lease' do
it 'schedules the worker' do
stub_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
expect(::PersonalAccessTokens::Groups::PolicyWorker).to receive(:perform_in).once
subject.execute
end
end
context "when we can't obtain the lease" do
it 'does not schedule the worker' do
stub_exclusive_lease_taken(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
expect(::PersonalAccessTokens::Groups::PolicyWorker).not_to receive(:perform_in)
subject.execute
end
end
end
end
...@@ -2,17 +2,17 @@ ...@@ -2,17 +2,17 @@
require 'spec_helper' require 'spec_helper'
describe PersonalAccessTokens::UpdateLifetimeService do describe PersonalAccessTokens::Instance::UpdateLifetimeService do
describe '#execute', :clean_gitlab_redis_shared_state do describe '#execute', :clean_gitlab_redis_shared_state do
include ExclusiveLeaseHelpers include ExclusiveLeaseHelpers
let(:lease_key) { 'personal_access_tokens/update_lifetime_service' } let(:lease_key) { 'personal_access_tokens/instance/update_lifetime_service' }
context 'when we can obtain the lease' do context 'when we can obtain the lease' do
it 'schedules the worker' do it 'schedules the worker' do
stub_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT) stub_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
expect(::PersonalAccessTokens::PolicyWorker).to receive(:perform_in).once expect(::PersonalAccessTokens::Instance::PolicyWorker).to receive(:perform_in).once
subject.execute subject.execute
end end
...@@ -22,7 +22,7 @@ describe PersonalAccessTokens::UpdateLifetimeService do ...@@ -22,7 +22,7 @@ describe PersonalAccessTokens::UpdateLifetimeService do
it 'does not schedule the worker' do it 'does not schedule the worker' do
stub_exclusive_lease_taken(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT) stub_exclusive_lease_taken(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
expect(::PersonalAccessTokens::PolicyWorker).not_to receive(:perform_in) expect(::PersonalAccessTokens::Instance::PolicyWorker).not_to receive(:perform_in)
subject.execute subject.execute
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PersonalAccessTokens::Groups::PolicyWorker, type: :worker do
let(:group) do
create(:group_with_managed_accounts, max_personal_access_token_lifetime: limit)
end
let!(:pat) do
create(:personal_access_token, expires_at: expires_at, user: create(:user, managing_group: group))
end
let(:expires_at) { nil }
let(:limit) { 7 }
let(:expires_after_max_token_lifetime) { (limit + 1).days.from_now.to_date }
let(:expires_before_max_token_lifetime) { (limit - 1).days.from_now.to_date }
describe '#perform' do
subject do
described_class.new.perform(group.id)
end
it_behaves_like 'an idempotent worker' do
let(:job_args) { [group.id] }
context 'when the group has set a PAT expiry policy' do
context 'valid PATs' do
let(:expires_at) { expires_before_max_token_lifetime }
it 'does not revoke valid PATs' do
expect { subject }.not_to change { pat.reload.revoked }
end
end
context 'invalid PATs' do
let(:expires_at) { expires_after_max_token_lifetime }
it 'revokes invalid PATs' do
expect { subject }.to change { pat.reload.revoked }.from(false).to(true)
end
end
end
context 'when the group has not set a PAT expiry policy' do
let(:group_limit) { nil }
let(:expires_at) { 1.day.from_now.to_date }
it 'does not revoke any tokens' do
expect { subject }.not_to change { pat.reload.revoked }
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PersonalAccessTokens::Instance::PolicyWorker, type: :worker do
describe '#perform' do
let(:instance_limit) { 7 }
let!(:pat) { create(:personal_access_token, expires_at: expire_at) }
before do
stub_application_setting(max_personal_access_token_lifetime: instance_limit)
end
context 'when a token is valid' do
let(:expire_at) { (instance_limit - 1).days.from_now.to_date }
it "doesn't revoked valid tokens" do
expect { subject.perform }.not_to change { pat.reload.revoked }
end
end
context 'when limit is nil' do
let(:instance_limit) { nil }
let(:expire_at) { 1.day.from_now }
it "doesn't revoked valid tokens" do
expect { subject.perform }.not_to change { pat.reload.revoked }
end
it "doesn't call the revoke invalid service" do
expect(PersonalAccessTokens::RevokeInvalidTokens).not_to receive(:new)
subject.perform
end
end
context 'invalid tokens' do
context 'PATs of users that do not belong to a managed group' do
context "when a token doesn't have an expiration time" do
let(:expire_at) { nil }
it 'enforces the policy on tokens' do
expect { subject.perform }.to change { pat.reload.revoked }.from(false).to(true)
end
end
context 'when a token expires after the limit' do
let(:expire_at) { (instance_limit + 1).days.from_now.to_date }
it 'enforces the policy on tokens' do
expect { subject.perform }.to change { pat.reload.revoked }.from(false).to(true)
end
end
end
context 'PATs of users that belongs to a managed group' do
let(:group) do
create(:group_with_managed_accounts, max_personal_access_token_lifetime: group_limit)
end
let(:user) { create(:user, managing_group: group) }
let!(:pat) { create(:personal_access_token, expires_at: expires_at, user: user) }
context 'when the group has set a PAT expiry policy' do
let(:group_limit) { 10 }
context 'PAT invalid as per the instance PAT expiration policy' do
let(:expires_at) { (instance_limit + 1).days.from_now.to_date }
it 'does not revoke the PAT' do
expect { subject.perform }.not_to change { pat.reload.revoked }
end
end
context 'PAT invalid as per the group PAT expiration policy' do
let(:expires_at) { (group_limit + 1).days.from_now.to_date }
it 'does not revoke the PAT' do
expect { subject.perform }.not_to change { pat.reload.revoked }
end
end
end
context 'when the group has not set a PAT expiry policy' do
let(:group_limit) { nil }
context 'PAT invalid as per the instance PAT expiration policy' do
let(:expires_at) { (instance_limit + 1).days.from_now.to_date }
it 'revokes the PAT' do
expect { subject.perform }.to change { pat.reload.revoked }.from(false).to(true)
end
end
context 'PAT valid as per the instance PAT expiration policy' do
let(:expires_at) { (instance_limit - 1).days.from_now.to_date }
it 'does not revoke the PAT' do
expect { subject.perform }.not_to change { pat.reload.revoked }
end
end
end
end
end
end
end
...@@ -385,6 +385,11 @@ msgstr "" ...@@ -385,6 +385,11 @@ msgstr ""
msgid "%{name}'s avatar" msgid "%{name}'s avatar"
msgstr "" msgstr ""
msgid "%{no_of_days} day"
msgid_plural "%{no_of_days} days"
msgstr[0] ""
msgstr[1] ""
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "" msgstr ""
...@@ -10840,6 +10845,9 @@ msgstr "" ...@@ -10840,6 +10845,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds." msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr "" msgstr ""
msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
msgstr ""
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides" msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr "" msgstr ""
...@@ -25015,6 +25023,9 @@ msgstr "" ...@@ -25015,6 +25023,9 @@ msgstr ""
msgid "no contributions" msgid "no contributions"
msgstr "" msgstr ""
msgid "no expiration"
msgstr ""
msgid "no one can merge" msgid "no one can merge"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment