From d0aaab0fe909d9a7a921c1f8241fb6f320d1befe Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 30 Sep 2020 22:09:22 +0000 Subject: Add latest changes from gitlab-org/security/gitlab@13-2-stable-ee --- app/controllers/admin/users_controller.rb | 21 +++++- app/controllers/projects/raw_controller.rb | 1 + app/controllers/registrations_controller.rb | 10 ++- app/helpers/safe_params_helper.rb | 2 +- app/models/member.rb | 1 + app/workers/all_queues.yml | 8 +++ .../remove_unaccepted_member_invites_worker.rb | 33 ++++++++++ .../222349-purge_unaccepted_member_invitations.yml | 5 ++ ...security-44-stored-xss-via-svg-file-preview.yml | 5 ++ ...erequisites-are-met-before-account-deletion.yml | 5 ++ .../unreleased/security-fix-safe-params-helper.yml | 4 ++ config/initializers/1_settings.rb | 3 + ..._index_to_members_for_unaccepted_invitations.rb | 20 ++++++ db/schema_migrations/20200916120837 | 1 + db/structure.sql | 3 + doc/user/admin_area/custom_project_templates.md | 3 +- doc/user/group/custom_project_templates.md | 5 +- doc/user/project/members/index.md | 3 + locale/gitlab.pot | 6 ++ spec/controllers/admin/users_controller_spec.rb | 37 ++++++++++- spec/controllers/projects/raw_controller_spec.rb | 5 ++ spec/controllers/registrations_controller_spec.rb | 18 +++++ spec/factories/projects.rb | 8 ++- spec/requests/api/files_spec.rb | 15 ++--- .../cached_response_shared_examples.rb | 12 ++++ ...remove_unaccepted_member_invites_worker_spec.rb | 76 ++++++++++++++++++++++ vendor/gitignore/C++.gitignore | 0 vendor/gitignore/Java.gitignore | 0 28 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 app/workers/remove_unaccepted_member_invites_worker.rb create mode 100644 changelogs/unreleased/222349-purge_unaccepted_member_invitations.yml create mode 100644 changelogs/unreleased/security-44-stored-xss-via-svg-file-preview.yml create mode 100644 changelogs/unreleased/security-ensure-prerequisites-are-met-before-account-deletion.yml create mode 100644 changelogs/unreleased/security-fix-safe-params-helper.yml create mode 100644 db/migrate/20200916120837_add_index_to_members_for_unaccepted_invitations.rb create mode 100644 db/schema_migrations/20200916120837 create mode 100644 spec/support/shared_examples/cached_response_shared_examples.rb create mode 100644 spec/workers/remove_unaccepted_member_invites_worker_spec.rb mode change 100644 => 100755 vendor/gitignore/C++.gitignore mode change 100644 => 100755 vendor/gitignore/Java.gitignore diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index fc0acd8f99a..9cb1d16fbd8 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -5,6 +5,7 @@ class Admin::UsersController < Admin::ApplicationController before_action :user, except: [:index, :new, :create] before_action :check_impersonation_availability, only: :impersonate + before_action :ensure_destroy_prerequisites_met, only: [:destroy] def index @users = User.filter_items(params[:filter]).order_name_asc @@ -168,7 +169,7 @@ class Admin::UsersController < Admin::ApplicationController end def destroy - user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete)) + user.delete_async(deleted_by: current_user, params: destroy_params) respond_to do |format| format.html { redirect_to admin_users_path, status: :found, notice: _("The user is being deleted.") } @@ -197,6 +198,24 @@ class Admin::UsersController < Admin::ApplicationController user == current_user end + def destroy_params + params.permit(:hard_delete) + end + + def ensure_destroy_prerequisites_met + return if hard_delete? + + if user.solo_owned_groups.present? + message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account') + + redirect_to admin_user_path(user), status: :see_other, alert: message + end + end + + def hard_delete? + destroy_params[:hard_delete] + end + def user @user ||= find_routable!(User, params[:id]) end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 69a3898af55..29f1e4bfd44 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -12,6 +12,7 @@ class Projects::RawController < Projects::ApplicationController before_action :authorize_download_code! before_action :show_rate_limit, only: [:show], unless: :external_storage_request? before_action :assign_ref_vars + before_action :no_cache_headers, only: [:show] before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled? def show diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b1c1fe3ba74..f054be50e20 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -10,7 +10,7 @@ class RegistrationsController < Devise::RegistrationsController skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration] prepend_before_action :check_captcha, only: :create - before_action :whitelist_query_limiting, only: [:destroy] + before_action :whitelist_query_limiting, :ensure_destroy_prerequisites_met, only: [:destroy] before_action :ensure_terms_accepted, if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? } before_action :load_recaptcha, only: :new @@ -125,6 +125,14 @@ class RegistrationsController < Devise::RegistrationsController private + def ensure_destroy_prerequisites_met + if current_user.solo_owned_groups.present? + redirect_to profile_account_path, + status: :see_other, + alert: s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account') + end + end + def user_created_message(confirmed: false) "User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}" end diff --git a/app/helpers/safe_params_helper.rb b/app/helpers/safe_params_helper.rb index 72bf1377b02..e9f0d82bb27 100644 --- a/app/helpers/safe_params_helper.rb +++ b/app/helpers/safe_params_helper.rb @@ -5,7 +5,7 @@ module SafeParamsHelper # Use this helper when generating links with `params.merge(...)` def safe_params if params.respond_to?(:permit!) - params.except(:host, :port, :protocol).permit! + params.except(*ActionDispatch::Routing::RouteSet::RESERVED_OPTIONS).permit! else params end diff --git a/app/models/member.rb b/app/models/member.rb index b221c44c748..34360fc62f7 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -5,6 +5,7 @@ class Member < ApplicationRecord include AfterCommitQueue include Sortable include Importable + include CreatedAtFilterable include Expirable include Gitlab::Access include Presentable diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 5148772c881..59a949fadef 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -291,6 +291,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: cronjob:remove_unaccepted_member_invites + :feature_category: :authentication_and_authorization + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:remove_unreferenced_lfs_objects :feature_category: :git_lfs :has_external_dependencies: diff --git a/app/workers/remove_unaccepted_member_invites_worker.rb b/app/workers/remove_unaccepted_member_invites_worker.rb new file mode 100644 index 00000000000..4b75b43791e --- /dev/null +++ b/app/workers/remove_unaccepted_member_invites_worker.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class RemoveUnacceptedMemberInvitesWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + include CronjobQueue # rubocop:disable Scalability/CronWorkerContext + + feature_category :authentication_and_authorization + urgency :low + idempotent! + + EXPIRATION_THRESHOLD = 90.days + BATCH_SIZE = 10_000 + + def perform + # We need to check for user_id IS NULL because we have accepted invitations + # in the database where we did not clear the invite_token. We do not + # want to accidentally delete those members. + loop do + # rubocop: disable CodeReuse/ActiveRecord + inner_query = Member + .select(:id) + .invite + .created_before(EXPIRATION_THRESHOLD.ago) + .where(user_id: nil) + .limit(BATCH_SIZE) + + records_deleted = Member.where(id: inner_query).delete_all + # rubocop: enable CodeReuse/ActiveRecord + + break if records_deleted == 0 + end + end +end diff --git a/changelogs/unreleased/222349-purge_unaccepted_member_invitations.yml b/changelogs/unreleased/222349-purge_unaccepted_member_invitations.yml new file mode 100644 index 00000000000..988ebe9f0c8 --- /dev/null +++ b/changelogs/unreleased/222349-purge_unaccepted_member_invitations.yml @@ -0,0 +1,5 @@ +--- +title: Purge unaccepted member invitations older than 90 days +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-44-stored-xss-via-svg-file-preview.yml b/changelogs/unreleased/security-44-stored-xss-via-svg-file-preview.yml new file mode 100644 index 00000000000..89a1eedb753 --- /dev/null +++ b/changelogs/unreleased/security-44-stored-xss-via-svg-file-preview.yml @@ -0,0 +1,5 @@ +--- +title: Prevent SVG XSS via Web IDE +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-ensure-prerequisites-are-met-before-account-deletion.yml b/changelogs/unreleased/security-ensure-prerequisites-are-met-before-account-deletion.yml new file mode 100644 index 00000000000..4b8f1c64ec7 --- /dev/null +++ b/changelogs/unreleased/security-ensure-prerequisites-are-met-before-account-deletion.yml @@ -0,0 +1,5 @@ +--- +title: Ensure user has no solo owned groups before triggering account deletion +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-safe-params-helper.yml b/changelogs/unreleased/security-fix-safe-params-helper.yml new file mode 100644 index 00000000000..ac7d2b60ff2 --- /dev/null +++ b/changelogs/unreleased/security-fix-safe-params-helper.yml @@ -0,0 +1,4 @@ +--- +title: Security fix safe params helper +author: +type: security diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b7432c4cbe6..1bba665c918 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -440,6 +440,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker' +Settings.cron_jobs['remove_unaccepted_member_invites_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['remove_unaccepted_member_invites_worker']['cron'] ||= '10 15 * * *' +Settings.cron_jobs['remove_unaccepted_member_invites_worker']['job_class'] = 'RemoveUnacceptedMemberInvitesWorker' Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *' Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker' diff --git a/db/migrate/20200916120837_add_index_to_members_for_unaccepted_invitations.rb b/db/migrate/20200916120837_add_index_to_members_for_unaccepted_invitations.rb new file mode 100644 index 00000000000..3b0e4b99eb5 --- /dev/null +++ b/db/migrate/20200916120837_add_index_to_members_for_unaccepted_invitations.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddIndexToMembersForUnacceptedInvitations < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + INDEX_NAME = 'idx_members_created_at_user_id_invite_token' + INDEX_SCOPE = 'invite_token IS NOT NULL AND user_id IS NULL' + + disable_ddl_transaction! + + def up + add_concurrent_index(:members, :created_at, where: INDEX_SCOPE, name: INDEX_NAME) + end + + def down + remove_concurrent_index(:members, :created_at, where: INDEX_SCOPE, name: INDEX_NAME) + end +end diff --git a/db/schema_migrations/20200916120837 b/db/schema_migrations/20200916120837 new file mode 100644 index 00000000000..41ce56533b2 --- /dev/null +++ b/db/schema_migrations/20200916120837 @@ -0,0 +1 @@ +ff246eb2761c4504b67b7d7b197990a671626038e50f1b82d6b3e4739a1ec3d4 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 179be83d2cc..ecc4ec7b955 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18581,6 +18581,8 @@ CREATE INDEX idx_jira_connect_subscriptions_on_installation_id ON public.jira_co CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON public.jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id); +CREATE INDEX idx_members_created_at_user_id_invite_token ON public.members USING btree (created_at) WHERE ((invite_token IS NOT NULL) AND (user_id IS NULL)); + CREATE INDEX idx_merge_requests_on_id_and_merge_jid ON public.merge_requests USING btree (id, merge_jid) WHERE ((merge_jid IS NOT NULL) AND (state_id = 4)); CREATE INDEX idx_merge_requests_on_source_project_and_branch_state_opened ON public.merge_requests USING btree (source_project_id, source_branch) WHERE (state_id = 1); @@ -23872,5 +23874,6 @@ COPY "schema_migrations" (version) FROM STDIN; 20200728182311 20200831204646 20200831222347 +20200916120837 \. diff --git a/doc/user/admin_area/custom_project_templates.md b/doc/user/admin_area/custom_project_templates.md index d194ca42215..8d044b94fbd 100644 --- a/doc/user/admin_area/custom_project_templates.md +++ b/doc/user/admin_area/custom_project_templates.md @@ -15,7 +15,8 @@ templates are sourced. Every project directly under the group namespace will be available to the user if they have access to them. For example: -- Public project in the group will be available to every logged in user. +- Public projects, in the group will be available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions) + are set to **Everyone With Access**. - Private projects will be available only if the user is a member of the project. Repository and database information that are copied over to each new project are diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md index ebeacda24c6..7c02d80d08d 100644 --- a/doc/user/group/custom_project_templates.md +++ b/doc/user/group/custom_project_templates.md @@ -20,9 +20,8 @@ GitLab administrators can [set project templates for an entire GitLab instance](../admin_area/custom_project_templates.md). Within this section, you can configure the group where all the custom project -templates are sourced. Every project directly under the group namespace will be -available to the user if they have access to them. For example, every public -project in the group will be available to every logged in user. +templates are sourced. Every project _template_ directly under the group namespace is +available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions) are set to **Everyone With Access**. However, private projects will be available only if the user is a member of the project. diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index 3eb411aef18..d8579c4cd8e 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -93,6 +93,9 @@ invitation, change their access level, or even delete them. Once the user accepts the invitation, they will be prompted to create a new GitLab account using the same e-mail address the invitation was sent to. +Note: **Note:** +Unaccepted invites are automatically deleted after 90 days. + ## Project membership and requesting access Project owners can : diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 95a57c3d55a..60fd73edda4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1942,6 +1942,9 @@ msgstr "" msgid "AdminUsers|You cannot remove your own admin rights." msgstr "" +msgid "AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account" +msgstr "" + msgid "Administration" msgstr "" @@ -17980,6 +17983,9 @@ msgstr "" msgid "Profiles|You don't have access to delete this user." msgstr "" +msgid "Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account" +msgstr "" + msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account." msgstr "" diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 08a1d7c9fa9..9e7f24ff7e3 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Admin::UsersController do end end - describe 'DELETE #user with projects', :sidekiq_might_not_need_inline do + describe 'DELETE #destroy', :sidekiq_might_not_need_inline do let(:project) { create(:project, namespace: user.namespace) } let!(:issue) { create(:issue, author: user) } @@ -59,6 +59,41 @@ RSpec.describe Admin::UsersController do expect(User.exists?(user.id)).to be_falsy expect(Issue.exists?(issue.id)).to be_falsy end + + context 'prerequisites for account deletion' do + context 'solo-owned groups' do + let(:group) { create(:group) } + + context 'if the user is the sole owner of at least one group' do + before do + create(:group_member, :owner, group: group, user: user) + end + + context 'soft-delete' do + it 'fails' do + delete :destroy, params: { id: user.username } + + message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account') + + expect(flash[:alert]).to eq(message) + expect(response).to have_gitlab_http_status(:see_other) + expect(response).to redirect_to admin_user_path(user) + expect(User.exists?(user.id)).to be_truthy + end + end + + context 'hard-delete' do + it 'succeeds' do + delete :destroy, params: { id: user.username, hard_delete: true } + + expect(response).to redirect_to(admin_users_path) + expect(flash[:notice]).to eq(_('The user is being deleted.')) + expect(User.exists?(user.id)).to be_falsy + end + end + end + end + end end describe 'PUT #activate' do diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 5f10343eb76..b3921164c81 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -33,6 +33,11 @@ RSpec.describe Projects::RawController do it_behaves_like 'project cache control headers' it_behaves_like 'content disposition headers' + it_behaves_like 'uncached response' do + before do + subject + end + end end context 'image header' do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 66caa58666f..c1afc75a6fd 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -384,6 +384,24 @@ RSpec.describe RegistrationsController do expect_success end end + + context 'prerequisites for account deletion' do + context 'solo-owned groups' do + let(:group) { create(:group) } + + context 'if the user is the sole owner of at least one group' do + before do + create(:group_member, :owner, group: group, user: user) + end + + it 'fails' do + delete :destroy, params: { password: '12345678' } + + expect_failure(s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account')) + end + end + end + end end describe '#update_registration' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index e4b53186ea8..216991c56c5 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -31,6 +31,7 @@ FactoryBot.define do pages_access_level do visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE end + metrics_dashboard_access_level { ProjectFeature::PRIVATE } # we can't assign the delegated `#ci_cd_settings` attributes directly, as the # `#ci_cd_settings` relation needs to be created first @@ -54,7 +55,9 @@ FactoryBot.define do issues_access_level: evaluator.issues_access_level, forking_access_level: evaluator.forking_access_level, merge_requests_access_level: merge_requests_access_level, - repository_access_level: evaluator.repository_access_level + repository_access_level: evaluator.repository_access_level, + pages_access_level: evaluator.pages_access_level, + metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level } if ActiveRecord::Migrator.current_version >= PAGES_ACCESS_LEVEL_SCHEMA_VERSION @@ -300,6 +303,9 @@ FactoryBot.define do trait(:pages_enabled) { pages_access_level { ProjectFeature::ENABLED } } trait(:pages_disabled) { pages_access_level { ProjectFeature::DISABLED } } trait(:pages_private) { pages_access_level { ProjectFeature::PRIVATE } } + trait(:metrics_dashboard_enabled) { metrics_dashboard_access_level { ProjectFeature::ENABLED } } + trait(:metrics_dashboard_disabled) { metrics_dashboard_access_level { ProjectFeature::DISABLED } } + trait(:metrics_dashboard_private) { metrics_dashboard_access_level { ProjectFeature::PRIVATE } } trait :auto_devops do association :auto_devops, factory: :project_auto_devops diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index b50f63ed67c..91877776190 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -502,16 +502,13 @@ RSpec.describe API::Files do expect(response).to have_gitlab_http_status(:ok) end - it 'sets no-cache headers' do - url = route('.gitignore') + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - - get api(url, current_user), params: params + it_behaves_like 'uncached response' do + before do + url = route('.gitignore') + "/raw" + expect(Gitlab::Workhorse).to receive(:send_git_blob) - expect(response.headers["Cache-Control"]).to include("no-store") - expect(response.headers["Cache-Control"]).to include("no-cache") - expect(response.headers["Pragma"]).to eq("no-cache") - expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") + get api(url, current_user), params: params + end end context 'when mandatory params are not given' do diff --git a/spec/support/shared_examples/cached_response_shared_examples.rb b/spec/support/shared_examples/cached_response_shared_examples.rb new file mode 100644 index 00000000000..34e5f741b4e --- /dev/null +++ b/spec/support/shared_examples/cached_response_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +# +# Negates lib/gitlab/no_cache_headers.rb +# + +RSpec.shared_examples 'cached response' do + it 'defines a cached header response' do + expect(response.headers["Cache-Control"]).not_to include("no-store", "no-cache") + expect(response.headers["Pragma"]).not_to eq("no-cache") + expect(response.headers["Expires"]).not_to eq("Fri, 01 Jan 1990 00:00:00 GMT") + end +end diff --git a/spec/workers/remove_unaccepted_member_invites_worker_spec.rb b/spec/workers/remove_unaccepted_member_invites_worker_spec.rb new file mode 100644 index 00000000000..96d7cf535ed --- /dev/null +++ b/spec/workers/remove_unaccepted_member_invites_worker_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RemoveUnacceptedMemberInvitesWorker do + let(:worker) { described_class.new } + + describe '#perform' do + context 'unaccepted members' do + before do + stub_const("#{described_class}::EXPIRATION_THRESHOLD", 1.day) + end + + it 'removes unaccepted members', :aggregate_failures do + unaccepted_group_invitee = create( + :group_member, invite_token: 't0ken', + invite_email: 'group_invitee@example.com', + user: nil, + created_at: Time.current - 5.days) + unaccepted_project_invitee = create( + :project_member, invite_token: 't0ken', + invite_email: 'project_invitee@example.com', + user: nil, + created_at: Time.current - 5.days) + + expect { worker.perform }.to change { Member.count }.by(-2) + + expect(Member.where(id: unaccepted_project_invitee.id)).not_to exist + expect(Member.where(id: unaccepted_group_invitee.id)).not_to exist + end + end + + context 'invited members still within expiration threshold' do + it 'leaves invited members', :aggregate_failures do + group_invitee = create( + :group_member, invite_token: 't0ken', + invite_email: 'group_invitee@example.com', + user: nil) + project_invitee = create( + :project_member, invite_token: 't0ken', + invite_email: 'project_invitee@example.com', + user: nil) + + expect { worker.perform }.not_to change { Member.count } + + expect(Member.where(id: group_invitee.id)).to exist + expect(Member.where(id: project_invitee.id)).to exist + end + end + + context 'accepted members' do + before do + stub_const("#{described_class}::EXPIRATION_THRESHOLD", 1.day) + end + + it 'leaves accepted members', :aggregate_failures do + user = create(:user) + accepted_group_invitee = create( + :group_member, invite_token: 't0ken', + invite_email: 'group_invitee@example.com', + user: user, + created_at: Time.current - 5.days) + accepted_project_invitee = create( + :project_member, invite_token: nil, + invite_email: 'project_invitee@example.com', + user: user, + created_at: Time.current - 5.days) + + expect { worker.perform }.not_to change { Member.count } + + expect(Member.where(id: accepted_group_invitee.id)).to exist + expect(Member.where(id: accepted_project_invitee.id)).to exist + end + end + end +end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore old mode 100644 new mode 100755 diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore old mode 100644 new mode 100755 -- cgit v1.2.1