New upstream version 13.2.10

This commit is contained in:
Abraham Raji 2020-10-03 22:27:07 +00:00
parent d0904892e8
commit 529b513274
81 changed files with 1705 additions and 397 deletions

View file

@ -2,6 +2,34 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.2.10 (2020-10-01)
### Security (14 changes)
- Do not store session id in Redis.
- Fix permission checks when updating confidentiality and milestone on issues or merge requests.
- Purge unaccepted member invitations older than 90 days.
- Adds feature flags plan limits.
- Prevent SVG XSS via Web IDE.
- Ensure user has no solo owned groups before triggering account deletion.
- Security fix safe params helper.
- Do not bypass admin mode when authenticated with deploy token.
- Fixes release asset link filepath ReDoS.
- Ensure global ID is of Annotation type in GraphQL destroy mutation.
- Validate that membership expiry dates are not in the past.
- Rate limit adding new email and re-sending email confirmation.
- Fix redaction of confidential Todos.
- Update GitLab Runner Helm Chart to 0.19.4.
## 13.2.9 (2020-09-04)
### Fixed (2 changes)
- Fix ActiveRecord::IrreversibleOrderError during restore from backup. !40789
- Update the 2FA user update check to account for rounding errors. !41327
## 13.2.8 (2020-09-02) ## 13.2.8 (2020-09-02)
### Security (1 change) ### Security (1 change)

View file

@ -1 +1 @@
13.2.8 13.2.10

View file

@ -1 +1 @@
13.2.8 13.2.10

View file

@ -15,7 +15,7 @@ module ApplicationCable
private private
def find_user_from_session_store def find_user_from_session_store
session = ActiveSession.sessions_from_ids([session_id]).first session = ActiveSession.sessions_from_ids([session_id.private_id]).first
Warden::SessionSerializer.new('rack.session' => session).fetch(:user) Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
end end

View file

@ -5,6 +5,7 @@ class Admin::UsersController < Admin::ApplicationController
before_action :user, except: [:index, :new, :create] before_action :user, except: [:index, :new, :create]
before_action :check_impersonation_availability, only: :impersonate before_action :check_impersonation_availability, only: :impersonate
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
def index def index
@users = User.filter_items(params[:filter]).order_name_asc @users = User.filter_items(params[:filter]).order_name_asc
@ -168,7 +169,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
def destroy 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| respond_to do |format|
format.html { redirect_to admin_users_path, status: :found, notice: _("The user is being deleted.") } 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 user == current_user
end 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 def user
@user ||= find_routable!(User, params[:id]) @user ||= find_routable!(User, params[:id])
end end

View file

@ -125,6 +125,10 @@ module AuthenticatesWithTwoFactor
def user_changed?(user) def user_changed?(user)
return false unless session[:user_updated_at] return false unless session[:user_updated_at]
user.updated_at != session[:user_updated_at] # See: https://gitlab.com/gitlab-org/gitlab/-/issues/244638
# Rounding errors happen when the user is updated, as the Rails ActiveRecord
# object has higher precision than what is stored in the database, therefore
# using .to_i to force truncation to the timestamp
user.updated_at.to_i != session[:user_updated_at].to_i
end end
end end

View file

@ -6,7 +6,9 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
end end
def destroy def destroy
ActiveSession.destroy_with_public_id(current_user, params[:id]) # params[:id] can be either an Rack::Session::SessionId#private_id
# or an encrypted Rack::Session::SessionId#public_id
ActiveSession.destroy_with_deprecated_encryption(current_user, params[:id])
current_user.forget_me! current_user.forget_me!
respond_to do |format| respond_to do |format|

View file

@ -2,6 +2,8 @@
class Profiles::EmailsController < Profiles::ApplicationController class Profiles::EmailsController < Profiles::ApplicationController
before_action :find_email, only: [:destroy, :resend_confirmation_instructions] before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
before_action -> { rate_limit!(:profile_add_new_email) }, only: [:create]
before_action -> { rate_limit!(:profile_resend_email_confirmation) }, only: [:resend_confirmation_instructions]
def index def index
@primary_email = current_user.email @primary_email = current_user.email
@ -38,6 +40,16 @@ class Profiles::EmailsController < Profiles::ApplicationController
private private
def rate_limit!(action)
rate_limiter = ::Gitlab::ApplicationRateLimiter
if rate_limiter.throttled?(action, scope: current_user)
rate_limiter.log_request(request, action, current_user)
redirect_back_or_default(options: { alert: _('This action has been performed too many times. Try again later.') })
end
end
def email_params def email_params
params.require(:email).permit(:email) params.require(:email).permit(:email)
end end

View file

@ -12,6 +12,7 @@ class Projects::RawController < Projects::ApplicationController
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :show_rate_limit, only: [:show], unless: :external_storage_request? before_action :show_rate_limit, only: [:show], unless: :external_storage_request?
before_action :assign_ref_vars 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? before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled?
def show def show

View file

@ -10,7 +10,7 @@ class RegistrationsController < Devise::RegistrationsController
skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration] skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration]
prepend_before_action :check_captcha, only: :create 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, before_action :ensure_terms_accepted,
if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? } if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? }
before_action :load_recaptcha, only: :new before_action :load_recaptcha, only: :new
@ -125,6 +125,14 @@ class RegistrationsController < Devise::RegistrationsController
private 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) def user_created_message(confirmed: false)
"User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}" "User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}"
end end

View file

@ -9,7 +9,7 @@ module Mutations
# This method is defined here in order to be used by `authorized_find!` in the subclasses. # This method is defined here in order to be used by `authorized_find!` in the subclasses.
def find_object(id:) def find_object(id:)
GitlabSchema.object_from_id(id) GitlabSchema.object_from_id(id, expected_type: ::Metrics::Dashboard::Annotation)
end end
end end
end end

View file

@ -22,4 +22,13 @@ module ActiveSessionsHelper
sprite_icon(icon_name, size: 16, css_class: 'gl-mt-2') sprite_icon(icon_name, size: 16, css_class: 'gl-mt-2')
end end
def revoke_session_path(active_session)
if active_session.session_private_id
profile_active_session_path(active_session.session_private_id)
else
# TODO: remove in 13.7
profile_active_session_path(active_session.public_id)
end
end
end end

View file

@ -5,7 +5,7 @@ module SafeParamsHelper
# Use this helper when generating links with `params.merge(...)` # Use this helper when generating links with `params.merge(...)`
def safe_params def safe_params
if params.respond_to?(:permit!) if params.respond_to?(:permit!)
params.except(:host, :port, :protocol).permit! params.except(*ActionDispatch::Routing::RouteSet::RESERVED_OPTIONS).permit!
else else
params params
end end

View file

@ -9,14 +9,14 @@ class ActiveSession
attr_accessor :created_at, :updated_at, attr_accessor :created_at, :updated_at,
:ip_address, :browser, :os, :ip_address, :browser, :os,
:device_name, :device_type, :device_name, :device_type,
:is_impersonated, :session_id :is_impersonated, :session_id, :session_private_id
def current?(session) def current?(rack_session)
return false if session_id.nil? || session.id.nil? return false if session_private_id.nil? || rack_session.id.nil?
# Rack v2.0.8+ added private_id, which uses the hash of the # Rack v2.0.8+ added private_id, which uses the hash of the
# public_id to avoid timing attacks. # public_id to avoid timing attacks.
session_id.private_id == session.id.private_id session_private_id == rack_session.id.private_id
end end
def human_device_type def human_device_type
@ -25,13 +25,14 @@ class ActiveSession
# This is not the same as Rack::Session::SessionId#public_id, but we # This is not the same as Rack::Session::SessionId#public_id, but we
# need to preserve this for backwards compatibility. # need to preserve this for backwards compatibility.
# TODO: remove in 13.7
def public_id def public_id
Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id.public_id) Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id)
end end
def self.set(user, request) def self.set(user, request)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
session_id = request.session.id.public_id session_private_id = request.session.id.private_id
client = DeviceDetector.new(request.user_agent) client = DeviceDetector.new(request.user_agent)
timestamp = Time.current timestamp = Time.current
@ -43,25 +44,37 @@ class ActiveSession
device_type: client.device_type, device_type: client.device_type,
created_at: user.current_sign_in_at || timestamp, created_at: user.current_sign_in_at || timestamp,
updated_at: timestamp, updated_at: timestamp,
session_id: session_id, # TODO: remove in 13.7
session_id: request.session.id.public_id,
session_private_id: session_private_id,
is_impersonated: request.session[:impersonator_id].present? is_impersonated: request.session[:impersonator_id].present?
) )
redis.pipelined do redis.pipelined do
redis.setex( redis.setex(
key_name(user.id, session_id), key_name(user.id, session_private_id),
Settings.gitlab['session_expire_delay'] * 60, Settings.gitlab['session_expire_delay'] * 60,
Marshal.dump(active_user_session) Marshal.dump(active_user_session)
) )
redis.sadd( redis.sadd(
lookup_key_name(user.id), lookup_key_name(user.id),
session_id session_private_id
) )
# We remove the ActiveSession stored by using public_id to avoid
# duplicate entries
remove_deprecated_active_sessions_with_public_id(redis, user.id, request.session.id.public_id)
end end
end end
end end
# TODO: remove in 13.7
private_class_method def self.remove_deprecated_active_sessions_with_public_id(redis, user_id, rack_session_public_id)
redis.srem(lookup_key_name(user_id), rack_session_public_id)
redis.del(key_name(user_id, rack_session_public_id))
end
def self.list(user) def self.list(user)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user).map do |raw_session| cleaned_up_lookup_entries(redis, user).map do |raw_session|
@ -70,34 +83,6 @@ class ActiveSession
end end
end end
def self.destroy(user, session_id)
return unless session_id
Gitlab::Redis::SharedState.with do |redis|
destroy_sessions(redis, user, [session_id])
end
end
def self.destroy_with_public_id(user, public_id)
decrypted_id = decrypt_public_id(public_id)
return if decrypted_id.nil?
session_id = Rack::Session::SessionId.new(decrypted_id)
destroy(user, session_id)
end
def self.destroy_sessions(redis, user, session_ids)
key_names = session_ids.map { |session_id| key_name(user.id, session_id.public_id) }
redis.srem(lookup_key_name(user.id), session_ids.map(&:public_id))
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(key_names)
redis.del(rack_session_keys(session_ids))
end
end
def self.cleanup(user) def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
clean_up_old_sessions(redis, user) clean_up_old_sessions(redis, user)
@ -105,12 +90,52 @@ class ActiveSession
end end
end end
def self.destroy_all_but_current(user, current_session) # TODO: remove in 13.7
session_ids = not_impersonated(user) # After upgrade there might be a duplicate ActiveSessions:
session_ids.reject! { |session| session.current?(current_session) } if current_session # - one with the public_id stored in #session_id
# - another with private_id stored in #session_private_id
def self.destroy_with_rack_session_id(user, rack_session_id)
return unless rack_session_id
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
destroy_sessions(redis, user, session_ids.map(&:session_id)) if session_ids.any? destroy_sessions(redis, user, [rack_session_id.public_id, rack_session_id.private_id])
end
end
def self.destroy_sessions(redis, user, session_ids)
key_names = session_ids.map { |session_id| key_name(user.id, session_id) }
redis.srem(lookup_key_name(user.id), session_ids)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(key_names)
redis.del(rack_session_keys(session_ids))
end
end
# TODO: remove in 13.7
# After upgrade, .destroy might be called with the session id encrypted
# by .public_id.
def self.destroy_with_deprecated_encryption(user, session_id)
return unless session_id
decrypted_session_id = decrypt_public_id(session_id)
rack_session_private_id = if decrypted_session_id
Rack::Session::SessionId.new(decrypted_session_id).private_id
end
Gitlab::Redis::SharedState.with do |redis|
destroy_sessions(redis, user, [session_id, decrypted_session_id, rack_session_private_id].compact)
end
end
def self.destroy_all_but_current(user, current_rack_session)
sessions = not_impersonated(user)
sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session
Gitlab::Redis::SharedState.with do |redis|
session_ids = (sessions.map(&:session_id) | sessions.map(&:session_private_id)).compact
destroy_sessions(redis, user, session_ids) if session_ids.any?
end end
end end
@ -132,17 +157,16 @@ class ActiveSession
# Lists the relevant session IDs for the user. # Lists the relevant session IDs for the user.
# #
# Returns an array of Rack::Session::SessionId objects # Returns an array of strings
def self.session_ids_for_user(user_id) def self.session_ids_for_user(user_id)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
session_ids = redis.smembers(lookup_key_name(user_id)) redis.smembers(lookup_key_name(user_id))
session_ids.map { |id| Rack::Session::SessionId.new(id) }
end end
end end
# Lists the session Hash objects for the given session IDs. # Lists the session Hash objects for the given session IDs.
# #
# session_ids - An array of Rack::Session::SessionId objects # session_ids - An array of strings
# #
# Returns an array of ActiveSession objects # Returns an array of ActiveSession objects
def self.sessions_from_ids(session_ids) def self.sessions_from_ids(session_ids)
@ -168,27 +192,12 @@ class ActiveSession
# Returns an ActiveSession object # Returns an ActiveSession object
def self.load_raw_session(raw_session) def self.load_raw_session(raw_session)
# rubocop:disable Security/MarshalLoad # rubocop:disable Security/MarshalLoad
session = Marshal.load(raw_session) Marshal.load(raw_session)
# rubocop:enable Security/MarshalLoad # rubocop:enable Security/MarshalLoad
# Older ActiveSession models serialize `session_id` as strings, To
# avoid breaking older sessions, we keep backwards compatibility
# with older Redis keys and initiate Rack::Session::SessionId here.
session.session_id = Rack::Session::SessionId.new(session.session_id) if session.try(:session_id).is_a?(String)
session
end end
def self.rack_session_keys(session_ids) def self.rack_session_keys(rack_session_ids)
session_ids.each_with_object([]) do |session_id, arr| rack_session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
# This is a redis-rack implementation detail
# (https://github.com/redis-store/redis-rack/blob/master/lib/rack/session/redis.rb#L88)
#
# We need to delete session keys based on the legacy public key name
# and the newer private ID keys, but there's no well-defined interface
# so we have to do it directly.
arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.public_id}"
arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.private_id}"
end
end end
def self.raw_active_session_entries(redis, session_ids, user_id) def self.raw_active_session_entries(redis, session_ids, user_id)
@ -220,7 +229,7 @@ class ActiveSession
sessions = active_session_entries(session_ids, user.id, redis) sessions = active_session_entries(session_ids, user.id, redis)
sessions.sort_by! {|session| session.updated_at }.reverse! sessions.sort_by! {|session| session.updated_at }.reverse!
destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS) destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
destroyable_session_ids = destroyable_sessions.map { |session| session.session_id } destroyable_session_ids = destroyable_sessions.flat_map { |session| [session.session_id, session.session_private_id] }.compact
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any? destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
end end
@ -244,6 +253,7 @@ class ActiveSession
entries.compact entries.compact
end end
# TODO: remove in 13.7
private_class_method def self.decrypt_public_id(public_id) private_class_method def self.decrypt_public_id(public_id)
Gitlab::CryptoHelper.aes256_gcm_decrypt(public_id) Gitlab::CryptoHelper.aes256_gcm_decrypt(public_id)
rescue rescue

View file

@ -426,6 +426,8 @@ class ApplicationSetting < ApplicationRecord
end end
def self.create_from_defaults def self.create_from_defaults
check_schema!
transaction(requires_new: true) do transaction(requires_new: true) do
super super
end end
@ -434,6 +436,22 @@ class ApplicationSetting < ApplicationRecord
current_without_cache current_without_cache
end end
# Due to the frequency with which settings are accessed, it is
# likely that during a backup restore a running GitLab process
# will insert a new `application_settings` row before the
# constraints have been added to the table. This would add an
# extra row with ID 1 and prevent the primary key constraint from
# being added, which made ActiveRecord throw a
# IrreversibleOrderError anytime the settings were accessed
# (https://gitlab.com/gitlab-org/gitlab/-/issues/36405). To
# prevent this from happening, we do a sanity check that the
# primary key constraint is present before inserting a new entry.
def self.check_schema!
return if ActiveRecord::Base.connection.primary_key(self.table_name).present?
raise "The `#{self.table_name}` table is missing a primary key constraint in the database schema"
end
# By default, the backend is Rails.cache, which uses # By default, the backend is Rails.cache, which uses
# ActiveSupport::Cache::RedisStore. Since loading ApplicationSetting # ActiveSupport::Cache::RedisStore. Since loading ApplicationSetting
# can cause a significant amount of load on Redis, let's cache it in # can cause a significant amount of load on Redis, let's cache it in

View file

@ -3,7 +3,7 @@
module Clusters module Clusters
module Applications module Applications
class Runner < ApplicationRecord class Runner < ApplicationRecord
VERSION = '0.18.3' VERSION = '0.19.4'
self.table_name = 'clusters_applications_runners' self.table_name = 'clusters_applications_runners'

View file

@ -5,6 +5,7 @@ class Member < ApplicationRecord
include AfterCommitQueue include AfterCommitQueue
include Sortable include Sortable
include Importable include Importable
include CreatedAtFilterable
include Expirable include Expirable
include Gitlab::Access include Gitlab::Access
include Presentable include Presentable
@ -20,6 +21,7 @@ class Member < ApplicationRecord
delegate :name, :username, :email, to: :user, prefix: true delegate :name, :username, :email, to: :user, prefix: true
validates :expires_at, allow_blank: true, future_date: true
validates :user, presence: true, unless: :invite? validates :user, presence: true, unless: :invite?
validates :source, presence: true validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id], validates :user_id, uniqueness: { scope: [:source_type, :source_id],

View file

@ -6,7 +6,9 @@ module Releases
belongs_to :release belongs_to :release
FILEPATH_REGEX = /\A\/([\-\.\w]+\/?)*[\da-zA-Z]+\z/.freeze # See https://gitlab.com/gitlab-org/gitlab/-/issues/218753
# Regex modified to prevent catastrophic backtracking
FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z}.freeze
validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release } validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
validates :name, presence: true, uniqueness: { scope: :release } validates :name, presence: true, uniqueness: { scope: :release }

View file

@ -19,6 +19,7 @@ class IssuableBaseService < BaseService
def filter_params(issuable) def filter_params(issuable)
unless can_admin_issuable?(issuable) unless can_admin_issuable?(issuable)
params.delete(:milestone)
params.delete(:milestone_id) params.delete(:milestone_id)
params.delete(:labels) params.delete(:labels)
params.delete(:add_label_ids) params.delete(:add_label_ids)

View file

@ -3,6 +3,7 @@
module Issues module Issues
class UpdateService < Issues::BaseService class UpdateService < Issues::BaseService
include SpamCheckMethods include SpamCheckMethods
extend ::Gitlab::Utils::Override
def execute(issue) def execute(issue)
handle_move_between_ids(issue) handle_move_between_ids(issue)
@ -17,6 +18,17 @@ module Issues
super super
end end
override :filter_params
def filter_params(issue)
super
# filter confidential in `Issues::UpdateService` and not in `IssuableBaseService#filtr_params`
# because we do allow users that cannot admin issues to set confidential flag when creating an issue
unless can_admin_issuable?(issue)
params.delete(:confidential)
end
end
def before_update(issue, skip_spam_check: false) def before_update(issue, skip_spam_check: false)
spam_check(issue, current_user, action: :update) unless skip_spam_check spam_check(issue, current_user, action: :update) unless skip_spam_check
end end

View file

@ -7,6 +7,11 @@ module Members
def initialize(current_user = nil, params = {}) def initialize(current_user = nil, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = params
# could be a string, force to an integer, part of fix
# https://gitlab.com/gitlab-org/gitlab/-/issues/219496
# Allow the ArgumentError to be raised if it can't be converted to an integer.
@params[:access_level] = Integer(@params[:access_level]) if @params[:access_level]
end end
def after_execute(args) def after_execute(args)

View file

@ -52,7 +52,14 @@ module Todos
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def remove_project_todos def remove_project_todos
Todo.where(project_id: non_authorized_projects, user_id: user.id).delete_all # Issues are viewable by guests (even in private projects), so remove those todos
# from projects without guest access
Todo.where(project_id: non_authorized_guest_projects, user_id: user.id)
.delete_all
# MRs require reporter access, so remove those todos that are not authorized
Todo.where(project_id: non_authorized_reporter_projects, target_type: MergeRequest.name, user_id: user.id)
.delete_all
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
@ -68,7 +75,7 @@ module Todos
when Project when Project
{ id: entity.id } { id: entity.id }
when Namespace when Namespace
{ namespace_id: non_member_groups } { namespace_id: non_authorized_reporter_groups }
end end
Project.where(condition) Project.where(condition)
@ -76,8 +83,32 @@ module Todos
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def non_authorized_projects def authorized_reporter_projects
projects.where('id NOT IN (?)', user.authorized_projects.select(:id)) user.authorized_projects(Gitlab::Access::REPORTER).select(:id)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def authorized_guest_projects
user.authorized_projects(Gitlab::Access::GUEST).select(:id)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def non_authorized_reporter_projects
projects.where('id NOT IN (?)', authorized_reporter_projects)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def non_authorized_guest_projects
projects.where('id NOT IN (?)', authorized_guest_projects)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def authorized_reporter_groups
GroupsFinder.new(user, min_access_level: Gitlab::Access::REPORTER).execute.select(:id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
@ -91,9 +122,9 @@ module Todos
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def non_member_groups def non_authorized_reporter_groups
entity.self_and_descendants.select(:id) entity.self_and_descendants.select(:id)
.where('id NOT IN (?)', user.membership_groups.select(:id)) .where('id NOT IN (?)', authorized_reporter_groups)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
@ -106,8 +137,6 @@ module Todos
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def confidential_issues def confidential_issues
assigned_ids = IssueAssignee.select(:issue_id).where(user_id: user.id) assigned_ids = IssueAssignee.select(:issue_id).where(user_id: user.id)
authorized_reporter_projects = user
.authorized_projects(Gitlab::Access::REPORTER).select(:id)
Issue.where(project_id: projects, confidential: true) Issue.where(project_id: projects, confidential: true)
.where('project_id NOT IN(?)', authorized_reporter_projects) .where('project_id NOT IN(?)', authorized_reporter_projects)

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
# FutureDateValidator
# Validates that a date is in the future.
#
# Example:
#
# class Member < ActiveRecord::Base
# validates :expires_at, allow_blank: true, future_date: true
# end
class FutureDateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, _('cannot be a date in the past')) if value < Date.current
end
end

View file

@ -27,6 +27,9 @@
- unless is_current_session - unless is_current_session
.float-right .float-right
= link_to profile_active_session_path(active_session.public_id), data: { confirm: _('Are you sure? The device will be signed out of GitLab and all remember me tokens revoked.') }, method: :delete, class: "btn btn-danger gl-ml-3" do = link_to(revoke_session_path(active_session),
{ data: { confirm: _('Are you sure? The device will be signed out of GitLab and all remember me tokens revoked.') },
method: :delete,
class: "btn btn-danger gl-ml-3" }) do
%span.sr-only= _('Revoke') %span.sr-only= _('Revoke')
= _('Revoke') = _('Revoke')

View file

@ -291,6 +291,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :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 - :name: cronjob:remove_unreferenced_lfs_objects
:feature_category: :git_lfs :feature_category: :git_lfs
:has_external_dependencies: :has_external_dependencies:

View file

@ -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

View file

@ -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'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' 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_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'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *' Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker' Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'

View file

@ -40,7 +40,8 @@ Rails.application.configure do |config|
activity = Gitlab::Auth::Activity.new(opts) activity = Gitlab::Auth::Activity.new(opts)
tracker = Gitlab::Auth::BlockedUserTracker.new(user, auth) tracker = Gitlab::Auth::BlockedUserTracker.new(user, auth)
ActiveSession.destroy(user, auth.request.session.id) # TODO: switch to `auth.request.session.id.private_id` in 13.7
ActiveSession.destroy_with_rack_session_id(user, auth.request.session.id)
activity.user_session_destroyed! activity.user_session_destroyed!
## ##

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddProjectFeatureFlagsToPlanLimits < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column(:plan_limits, :project_feature_flags, :integer, default: 200, null: false)
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class InsertProjectFeatureFlagsPlanLimits < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
return unless Gitlab.com?
create_or_update_plan_limit('project_feature_flags', 'free', 50)
create_or_update_plan_limit('project_feature_flags', 'bronze', 100)
create_or_update_plan_limit('project_feature_flags', 'silver', 150)
create_or_update_plan_limit('project_feature_flags', 'gold', 200)
end
def down
return unless Gitlab.com?
create_or_update_plan_limit('project_feature_flags', 'free', 0)
create_or_update_plan_limit('project_feature_flags', 'bronze', 0)
create_or_update_plan_limit('project_feature_flags', 'silver', 0)
create_or_update_plan_limit('project_feature_flags', 'gold', 0)
end
end

View file

@ -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

View file

@ -0,0 +1 @@
ff246eb2761c4504b67b7d7b197990a671626038e50f1b82d6b3e4739a1ec3d4

View file

@ -13891,7 +13891,8 @@ CREATE TABLE public.plan_limits (
ci_max_artifact_size_requirements integer DEFAULT 0 NOT NULL, ci_max_artifact_size_requirements integer DEFAULT 0 NOT NULL,
ci_max_artifact_size_coverage_fuzzing integer DEFAULT 0 NOT NULL, ci_max_artifact_size_coverage_fuzzing integer DEFAULT 0 NOT NULL,
ci_max_artifact_size_browser_performance integer DEFAULT 0 NOT NULL, ci_max_artifact_size_browser_performance integer DEFAULT 0 NOT NULL,
ci_max_artifact_size_load_performance integer DEFAULT 0 NOT NULL ci_max_artifact_size_load_performance integer DEFAULT 0 NOT NULL,
project_feature_flags integer DEFAULT 200 NOT NULL
); );
CREATE SEQUENCE public.plan_limits_id_seq CREATE SEQUENCE public.plan_limits_id_seq
@ -18580,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 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_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); 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);
@ -23869,5 +23872,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200716120419 20200716120419
20200717040735 20200717040735
20200728182311 20200728182311
20200831204646
20200831222347
20200916120837
\. \.

View file

@ -61,6 +61,7 @@ exceptions:
- RSA - RSA
- RSS - RSS
- SAML - SAML
- SCIM
- SCP - SCP
- SCSS - SCSS
- SHA - SHA

View file

@ -409,7 +409,7 @@ pages:
### Using a custom Certificate Authority (CA) ### Using a custom Certificate Authority (CA)
NOTE: **Note:** NOTE: **Note:**
[Before 13.2](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4289), when using Omnibus, a [workaround was required](https://docs.gitlab.com/13.1/ee/administration/pages/index.html#using-a-custom-certificate-authority-ca). [Before 13.3](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4411), when using Omnibus, a [workaround was required](https://docs.gitlab.com/13.1/ee/administration/pages/index.html#using-a-custom-certificate-authority-ca).
When using certificates issued by a custom CA, [Access Control](../../user/project/pages/pages_access_control.md#gitlab-pages-access-control) and When using certificates issued by a custom CA, [Access Control](../../user/project/pages/pages_access_control.md#gitlab-pages-access-control) and
the [online view of HTML job artifacts](../../ci/pipelines/job_artifacts.md#browsing-artifacts) the [online view of HTML job artifacts](../../ci/pipelines/job_artifacts.md#browsing-artifacts)

View file

@ -248,7 +248,7 @@ Example response:
### Example with user / group level access **(STARTER)** ### Example with user / group level access **(STARTER)**
Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the
form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API in](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3516) GitLab 10.3 EE. form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3516) in GitLab 10.3 EE.
```shell ```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1" curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1"

View file

@ -2,7 +2,7 @@
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
The SCIM API implements [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644). The SCIM API implements the [RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
CAUTION: **Caution:** CAUTION: **Caution:**
This API is for internal system use for connecting with a SCIM provider. While it can be used directly, it is subject to change without notice. This API is for internal system use for connecting with a SCIM provider. While it can be used directly, it is subject to change without notice.

View file

@ -41,8 +41,8 @@ the `author` field. GitLab team members **should not**.
a changelog entry regardless of these guidelines if the contributor wants one. a changelog entry regardless of these guidelines if the contributor wants one.
Example: "Fixed a typo on the search results page." Example: "Fixed a typo on the search results page."
- Any docs-only changes **should not** have a changelog entry. - Any docs-only changes **should not** have a changelog entry.
- Any change behind a feature flag **should not** have a changelog entry - unless - Any change behind a disabled feature flag **should not** have a changelog entry.
the feature flag has been defaulted to true. - Any change behind an enabled feature flag **should** have a changelog entry.
- A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry - - A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry -
only if the feature flag did not default to true already. only if the feature flag did not default to true already.
- A fix for a regression introduced and then fixed in the same release (i.e., - A fix for a regression introduced and then fixed in the same release (i.e.,

View file

@ -638,6 +638,38 @@ unit tests.
Instead of `setImmediate`, use `jest.runAllTimers` or `jest.runOnlyPendingTimers` to run pending timers. Instead of `setImmediate`, use `jest.runAllTimers` or `jest.runOnlyPendingTimers` to run pending timers.
The latter is useful when you have `setInterval` in the code. **Remember:** our Jest configuration uses fake timers. The latter is useful when you have `setInterval` in the code. **Remember:** our Jest configuration uses fake timers.
## Avoid non-deterministic specs
Non-determinism is the breeding ground for flaky and brittle specs. Such specs end up breaking the CI pipeline, interrupting the work flow of other contributors.
1. Make sure your test subject's collaborators (e.g., axios, apollo, lodash helpers) and test environment (e.g., Date) behave consistently across systems and over time.
1. Make sure tests are focused and not doing "extra work" (e.g., needlessly creating the test subject more than once in an individual test)
### Faking `Date` for determinism
Consider using `useFakeDate` to ensure a consistent value is returned with every `new Date()` or `Date.now()`.
```javascript
import { useFakeDate } from 'helpers/fake_date';
describe('cool/component', () => {
useFakeDate();
// ...
});
```
### Faking `Math.random` for determinism
Consider replacing `Math.random` with a fake when the test subject depends on it.
```javascript
beforeEach(() => {
// https://xkcd.com/221/
jest.spyOn(Math, 'random').mockReturnValue(0.4);
});
```
## Factories ## Factories
TBU TBU

View file

@ -15,7 +15,8 @@ templates are sourced.
Every project directly under the group namespace will be Every project directly under the group namespace will be
available to the user if they have access to them. For example: 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. - 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 Repository and database information that are copied over to each new project are

View file

@ -73,10 +73,10 @@ configure cluster:
name: production name: production
``` ```
### Setting the environment scope **(PREMIUM)** ### Setting the environment scope
[Environment [Environment
scopes](../project/clusters/index.md#setting-the-environment-scope-premium) scopes](../project/clusters/index.md#setting-the-environment-scope)
are usable when associating multiple clusters to the same management are usable when associating multiple clusters to the same management
project. project.

View file

@ -20,9 +20,8 @@ GitLab administrators can
[set project templates for an entire GitLab instance](../admin_area/custom_project_templates.md). [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 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 templates are sourced. Every project _template_ directly under the group namespace is
available to the user if they have access to them. For example, every public available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions) are set to **Everyone With Access**.
project in the group will be available to every logged in user.
However, private projects will be available only if the user is a member of the project. However, private projects will be available only if the user is a member of the project.

View file

@ -135,7 +135,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
1. Click **Authenticate with AWS**. 1. Click **Authenticate with AWS**.
1. Choose your cluster's settings: 1. Choose your cluster's settings:
- **Kubernetes cluster name** - The name you wish to give the cluster. - **Kubernetes cluster name** - The name you wish to give the cluster.
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster. - **Environment scope** - The [associated environment](index.md#setting-the-environment-scope) to this cluster.
- **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14. - **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14.
- **Role name** - Select the [IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) - **Role name** - Select the [IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html)
to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. This IAM role is separate to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. This IAM role is separate

View file

@ -55,7 +55,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
**Sign in with Google** button. **Sign in with Google** button.
1. Choose your cluster's settings: 1. Choose your cluster's settings:
- **Kubernetes cluster name** - The name you wish to give the cluster. - **Kubernetes cluster name** - The name you wish to give the cluster.
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster. - **Environment scope** - The [associated environment](index.md#setting-the-environment-scope) to this cluster.
- **Google Cloud Platform project** - Choose the project you created in your GCP - **Google Cloud Platform project** - Choose the project you created in your GCP
console that will host the Kubernetes cluster. Learn more about console that will host the Kubernetes cluster. Learn more about
[Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).

View file

@ -169,7 +169,7 @@ To add a Kubernetes cluster to your project, group, or instance:
1. Click the **Add existing cluster** tab and fill in the details: 1. Click the **Add existing cluster** tab and fill in the details:
1. **Kubernetes cluster name** (required) - The name you wish to give the cluster. 1. **Kubernetes cluster name** (required) - The name you wish to give the cluster.
1. **Environment scope** (required) - The 1. **Environment scope** (required) - The
[associated environment](index.md#setting-the-environment-scope-premium) to this cluster. [associated environment](index.md#setting-the-environment-scope) to this cluster.
1. **API URL** (required) - 1. **API URL** (required) -
It's the URL that GitLab uses to access the Kubernetes API. Kubernetes It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
exposes several APIs, we want the "base" URL that is common to all of them. exposes several APIs, we want the "base" URL that is common to all of them.

View file

@ -74,10 +74,10 @@ project. That way you can have different clusters for different environments,
like dev, staging, production, and so on. like dev, staging, production, and so on.
Simply add another cluster, like you did the first time, and make sure to Simply add another cluster, like you did the first time, and make sure to
[set an environment scope](#setting-the-environment-scope-premium) that will [set an environment scope](#setting-the-environment-scope) that will
differentiate the new cluster with the rest. differentiate the new cluster with the rest.
#### Setting the environment scope **(PREMIUM)** #### Setting the environment scope
When adding more than one Kubernetes cluster to your project, you need to differentiate When adding more than one Kubernetes cluster to your project, you need to differentiate
them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments/index.md) similar to how the them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments/index.md) similar to how the

View file

@ -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 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. 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 membership and requesting access
Project owners can : Project owners can :

View file

@ -53,8 +53,10 @@ module API
user = find_user_from_sources user = find_user_from_sources
return unless user return unless user
if user.is_a?(User) && Feature.enabled?(:user_mode_in_session)
# Sessions are enforced to be unavailable for API calls, so ignore them for admin mode # Sessions are enforced to be unavailable for API calls, so ignore them for admin mode
Gitlab::Auth::CurrentUserMode.bypass_session!(user.id) if Feature.enabled?(:user_mode_in_session) Gitlab::Auth::CurrentUserMode.bypass_session!(user.id)
end
unless api_access_allowed?(user) unless api_access_allowed?(user)
forbidden!(api_access_denied_message(user)) forbidden!(api_access_denied_message(user))

View file

@ -31,7 +31,9 @@ module Gitlab
group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute }, group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute }, group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute }, group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
group_testing_hook: { threshold: 5, interval: 1.minute } group_testing_hook: { threshold: 5, interval: 1.minute },
profile_add_new_email: { threshold: 5, interval: 1.minute },
profile_resend_email_confirmation: { threshold: 5, interval: 1.minute }
}.freeze }.freeze
end end

View file

@ -1942,6 +1942,9 @@ msgstr ""
msgid "AdminUsers|You cannot remove your own admin rights." msgid "AdminUsers|You cannot remove your own admin rights."
msgstr "" msgstr ""
msgid "AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account"
msgstr ""
msgid "Administration" msgid "Administration"
msgstr "" msgstr ""
@ -17980,6 +17983,9 @@ msgstr ""
msgid "Profiles|You don't have access to delete this user." msgid "Profiles|You don't have access to delete this user."
msgstr "" 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." msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
msgstr "" msgstr ""
@ -23972,6 +23978,9 @@ msgstr ""
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention." msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr "" msgstr ""
msgid "This action has been performed too many times. Try again later."
msgstr ""
msgid "This also resolves all related threads" msgid "This also resolves all related threads"
msgstr "" msgstr ""
@ -27623,6 +27632,9 @@ msgstr ""
msgid "by %{user}" msgid "by %{user}"
msgstr "" msgstr ""
msgid "cannot be a date in the past"
msgstr ""
msgid "cannot be changed if a personal project has container registry tags." msgid "cannot be changed if a personal project has container registry tags."
msgstr "" msgstr ""

View file

@ -36,7 +36,7 @@ RSpec.describe Admin::UsersController do
end end
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(:project) { create(:project, namespace: user.namespace) }
let!(:issue) { create(:issue, author: user) } let!(:issue) { create(:issue, author: user) }
@ -59,6 +59,41 @@ RSpec.describe Admin::UsersController do
expect(User.exists?(user.id)).to be_falsy expect(User.exists?(user.id)).to be_falsy
expect(Issue.exists?(issue.id)).to be_falsy expect(Issue.exists?(issue.id)).to be_falsy
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
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 end
describe 'PUT #activate' do describe 'PUT #activate' do

View file

@ -139,6 +139,45 @@ RSpec.describe Groups::GroupMembersController do
expect(group.users).not_to include group_user expect(group.users).not_to include group_user
end end
end end
context 'access expiry date' do
before do
group.add_owner(user)
end
subject do
post :create, params: {
group_id: group,
user_ids: group_user.id,
access_level: Gitlab::Access::GUEST,
expires_at: expires_at
}
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago }
it 'does not add user to members' do
subject
expect(flash[:alert]).to include('Expires at cannot be a date in the past')
expect(response).to redirect_to(group_group_members_path(group))
expect(group.users).not_to include group_user
end
end
context 'when set to a date in the future' do
let(:expires_at) { 5.days.from_now }
it 'adds user to members' do
subject
expect(response).to set_flash.to 'Users were successfully added.'
expect(response).to redirect_to(group_group_members_path(group))
expect(group.users).to include group_user
end
end
end
end end
describe 'PUT update' do describe 'PUT update' do
@ -149,6 +188,7 @@ RSpec.describe Groups::GroupMembersController do
sign_in(user) sign_in(user)
end end
context 'access level' do
Gitlab::Access.options.each do |label, value| Gitlab::Access.options.each do |label, value|
it "can change the access level to #{label}" do it "can change the access level to #{label}" do
put :update, params: { put :update, params: {
@ -162,6 +202,39 @@ RSpec.describe Groups::GroupMembersController do
end end
end end
context 'access expiry date' do
subject do
put :update, xhr: true, params: {
group_member: {
expires_at: expires_at
},
group_id: group,
id: requester
}
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago }
it 'does not update the member' do
subject
expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
end
end
context 'when set to a date in the future' do
let(:expires_at) { 5.days.from_now }
it 'updates the member' do
subject
expect(requester.reload.expires_at).to eq(expires_at.to_date)
end
end
end
end
describe 'DELETE destroy' do describe 'DELETE destroy' do
let(:member) { create(:group_member, :developer, group: group) } let(:member) { create(:group_member, :developer, group: group) }

View file

@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Profiles::EmailsController do RSpec.describe Profiles::EmailsController do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
sign_in(user) sign_in(user)
@ -15,37 +15,75 @@ RSpec.describe Profiles::EmailsController do
end end
end end
shared_examples_for 'respects the rate limit' do
context 'after the rate limit is exceeded' do
before do
allowed_threshold = Gitlab::ApplicationRateLimiter.rate_limits[action][:threshold]
allow(Gitlab::ApplicationRateLimiter)
.to receive(:increment)
.and_return(allowed_threshold + 1)
end
it 'does not send any email' do
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
end
it 'displays an alert' do
subject
expect(response).to have_gitlab_http_status(:redirect)
expect(flash[:alert]).to eq(_('This action has been performed too many times. Try again later.'))
end
end
end
describe '#create' do describe '#create' do
context 'when email address is valid' do let(:email) { 'add_email@example.com' }
let(:email_params) { { email: "add_email@example.com" } } let(:params) { { email: { email: email } } }
subject { post(:create, params: params) }
it 'sends an email confirmation' do it 'sends an email confirmation' do
expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size } expect { subject }.to change { ActionMailer::Base.deliveries.size }
end
end end
context 'when email address is invalid' do context 'when email address is invalid' do
let(:email_params) { { email: "test.@example.com" } } let(:email) { 'invalid.@example.com' }
it 'does not send an email confirmation' do it 'does not send an email confirmation' do
expect { post(:create, params: { email: email_params }) }.not_to change { ActionMailer::Base.deliveries.size } expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
end end
end end
it_behaves_like 'respects the rate limit' do
let(:action) { :profile_add_new_email }
end
end end
describe '#resend_confirmation_instructions' do describe '#resend_confirmation_instructions' do
let(:email_params) { { email: "add_email@example.com" } } let_it_be(:email) { create(:email, user: user) }
let(:params) { { id: email.id } }
subject { put(:resend_confirmation_instructions, params: params) }
it 'resends an email confirmation' do it 'resends an email confirmation' do
email = user.emails.create(email: 'add_email@example.com') expect { subject }.to change { ActionMailer::Base.deliveries.size }
expect { put(:resend_confirmation_instructions, params: { id: email }) }.to change { ActionMailer::Base.deliveries.size } expect(ActionMailer::Base.deliveries.last.to).to eq [email.email]
expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]] expect(ActionMailer::Base.deliveries.last.subject).to match 'Confirmation instructions'
expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
end end
it 'unable to resend an email confirmation' do context 'email does not exist' do
expect { put(:resend_confirmation_instructions, params: { id: 1 }) }.not_to change { ActionMailer::Base.deliveries.size } let(:params) { { id: non_existing_record_id } }
it 'does not send an email confirmation' do
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
end
end
it_behaves_like 'respects the rate limit' do
let(:action) { :profile_resend_email_confirmation }
end end
end end
end end

View file

@ -129,6 +129,46 @@ RSpec.describe Projects::ProjectMembersController do
expect(response).to redirect_to(project_project_members_path(project)) expect(response).to redirect_to(project_project_members_path(project))
end end
end end
context 'access expiry date' do
before do
project.add_maintainer(user)
end
subject do
post :create, params: {
namespace_id: project.namespace,
project_id: project,
user_ids: project_user.id,
access_level: Gitlab::Access::GUEST,
expires_at: expires_at
}
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago }
it 'does not add user to members' do
subject
expect(flash[:alert]).to include('Expires at cannot be a date in the past')
expect(response).to redirect_to(project_project_members_path(project))
expect(project.users).not_to include project_user
end
end
context 'when set to a date in the future' do
let(:expires_at) { 5.days.from_now }
it 'adds user to members' do
subject
expect(response).to set_flash.to 'Users were successfully added.'
expect(response).to redirect_to(project_project_members_path(project))
expect(project.users).to include project_user
end
end
end
end end
describe 'PUT update' do describe 'PUT update' do
@ -139,20 +179,57 @@ RSpec.describe Projects::ProjectMembersController do
sign_in(user) sign_in(user)
end end
context 'access level' do
Gitlab::Access.options.each do |label, value| Gitlab::Access.options.each do |label, value|
it "can change the access level to #{label}" do it "can change the access level to #{label}" do
put :update, params: { params = {
project_member: { access_level: value }, project_member: { access_level: value },
namespace_id: project.namespace, namespace_id: project.namespace,
project_id: project, project_id: project,
id: requester id: requester
}, xhr: true }
put :update, params: params, xhr: true
expect(requester.reload.human_access).to eq(label) expect(requester.reload.human_access).to eq(label)
end end
end end
end end
context 'access expiry date' do
subject do
put :update, xhr: true, params: {
project_member: {
expires_at: expires_at
},
namespace_id: project.namespace,
project_id: project,
id: requester
}
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago }
it 'does not update the member' do
subject
expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
end
end
context 'when set to a date in the future' do
let(:expires_at) { 5.days.from_now }
it 'updates the member' do
subject
expect(requester.reload.expires_at).to eq(expires_at.to_date)
end
end
end
end
describe 'DELETE destroy' do describe 'DELETE destroy' do
let(:member) { create(:project_member, :developer, project: project) } let(:member) { create(:project_member, :developer, project: project) }

View file

@ -33,6 +33,11 @@ RSpec.describe Projects::RawController do
it_behaves_like 'project cache control headers' it_behaves_like 'project cache control headers'
it_behaves_like 'content disposition headers' it_behaves_like 'content disposition headers'
it_behaves_like 'uncached response' do
before do
subject
end
end
end end
context 'image header' do context 'image header' do

View file

@ -384,6 +384,24 @@ RSpec.describe RegistrationsController do
expect_success expect_success
end end
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 end
describe '#update_registration' do describe '#update_registration' do

View file

@ -31,6 +31,7 @@ FactoryBot.define do
pages_access_level do pages_access_level do
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
end end
metrics_dashboard_access_level { ProjectFeature::PRIVATE }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the # we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first # `#ci_cd_settings` relation needs to be created first
@ -54,7 +55,9 @@ FactoryBot.define do
issues_access_level: evaluator.issues_access_level, issues_access_level: evaluator.issues_access_level,
forking_access_level: evaluator.forking_access_level, forking_access_level: evaluator.forking_access_level,
merge_requests_access_level: merge_requests_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 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_enabled) { pages_access_level { ProjectFeature::ENABLED } }
trait(:pages_disabled) { pages_access_level { ProjectFeature::DISABLED } } trait(:pages_disabled) { pages_access_level { ProjectFeature::DISABLED } }
trait(:pages_private) { pages_access_level { ProjectFeature::PRIVATE } } 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 trait :auto_devops do
association :auto_devops, factory: :project_auto_devops association :auto_devops, factory: :project_auto_devops

View file

@ -0,0 +1,49 @@
// Frida Kahlo's birthday (6 = July)
export const DEFAULT_ARGS = [2020, 6, 6];
const RealDate = Date;
const isMocked = val => Boolean(val.mock);
export const createFakeDateClass = ctorDefault => {
const FakeDate = new Proxy(RealDate, {
construct: (target, argArray) => {
const ctorArgs = argArray.length ? argArray : ctorDefault;
return new RealDate(...ctorArgs);
},
apply: (target, thisArg, argArray) => {
const ctorArgs = argArray.length ? argArray : ctorDefault;
return RealDate(...ctorArgs);
},
// We want to overwrite the default 'now', but only if it's not already mocked
get: (target, prop) => {
if (prop === 'now' && !isMocked(target[prop])) {
return () => new RealDate(...ctorDefault).getTime();
}
return target[prop];
},
getPrototypeOf: target => {
return target.prototype;
},
// We need to be able to set props so that `jest.spyOn` will work.
set: (target, prop, value) => {
// eslint-disable-next-line no-param-reassign
target[prop] = value;
return true;
},
});
return FakeDate;
};
export const useFakeDate = (...args) => {
const FakeDate = createFakeDateClass(args.length ? args : DEFAULT_ARGS);
global.Date = FakeDate;
};
export const useRealDate = () => {
global.Date = RealDate;
};

View file

@ -0,0 +1,33 @@
import { createFakeDateClass, DEFAULT_ARGS, useRealDate } from './fake_date';
describe('spec/helpers/fake_date', () => {
describe('createFakeDateClass', () => {
let FakeDate;
beforeAll(() => {
useRealDate();
});
beforeEach(() => {
FakeDate = createFakeDateClass(DEFAULT_ARGS);
});
it('should use default args', () => {
expect(new FakeDate()).toEqual(new Date(...DEFAULT_ARGS));
expect(FakeDate()).toEqual(Date(...DEFAULT_ARGS));
});
it('should have deterministic now()', () => {
expect(FakeDate.now()).not.toBe(Date.now());
expect(FakeDate.now()).toBe(new Date(...DEFAULT_ARGS).getTime());
});
it('should be instanceof Date', () => {
expect(new FakeDate()).toBeInstanceOf(Date);
});
it('should be instanceof self', () => {
expect(new FakeDate()).toBeInstanceOf(FakeDate);
});
});
});

View file

@ -3,7 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Mutations::Issues::SetConfidential do RSpec.describe Mutations::Issues::SetConfidential do
let(:issue) { create(:issue) } let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [user]) }
let(:user) { create(:user) } let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
@ -14,7 +15,7 @@ RSpec.describe Mutations::Issues::SetConfidential do
let(:confidential) { true } let(:confidential) { true }
let(:mutated_issue) { subject[:issue] } let(:mutated_issue) { subject[:issue] }
subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, confidential: confidential) } subject { mutation.resolve(project_path: project.full_path, iid: issue.iid, confidential: confidential) }
it 'raises an error if the resource is not accessible to the user' do it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
@ -22,7 +23,7 @@ RSpec.describe Mutations::Issues::SetConfidential do
context 'when the user can update the issue' do context 'when the user can update the issue' do
before do before do
issue.project.add_developer(user) project.add_developer(user)
end end
it 'returns the issue as confidential' do it 'returns the issue as confidential' do
@ -39,5 +40,19 @@ RSpec.describe Mutations::Issues::SetConfidential do
end end
end end
end end
context 'when guest user is an assignee' do
let(:project) { create(:project, :public) }
before do
project.add_guest(user)
end
it 'does not change issue confidentiality' do
expect(mutated_issue).to eq(issue)
expect(mutated_issue.confidential).to be_falsey
expect(subject[:errors]).to be_empty
end
end
end end
end end

View file

@ -3,31 +3,29 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Mutations::MergeRequests::SetMilestone do RSpec.describe Mutations::MergeRequests::SetMilestone do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :private) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, assignees: [user]) }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:milestone) { create(:milestone, project: project) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } subject { mutation.resolve(project_path: project.full_path, iid: merge_request.iid, milestone: milestone) }
specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) } specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
describe '#resolve' do describe '#resolve' do
let(:milestone) { create(:milestone, project: merge_request.project) }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, milestone: milestone) }
it 'raises an error if the resource is not accessible to the user' do it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end end
context 'when the user can update the merge request' do context 'when the user can update the merge request' do
before do before do
merge_request.project.add_developer(user) project.add_developer(user)
end end
it 'returns the merge request with the milestone' do it 'returns the merge request with the milestone' do
expect(mutated_merge_request).to eq(merge_request) expect(subject[:merge_request]).to eq(merge_request)
expect(mutated_merge_request.milestone).to eq(milestone) expect(subject[:merge_request].milestone).to eq(milestone)
expect(subject[:errors]).to be_empty expect(subject[:errors]).to be_empty
end end
@ -43,13 +41,37 @@ RSpec.describe Mutations::MergeRequests::SetMilestone do
let(:milestone) { nil } let(:milestone) { nil }
it 'removes the milestone' do it 'removes the milestone' do
merge_request.update!(milestone: create(:milestone, project: merge_request.project)) merge_request.update!(milestone: create(:milestone, project: project))
expect(mutated_merge_request.milestone).to eq(nil) expect(subject[:merge_request].milestone).to be_nil
end end
it 'does not do anything if the MR already does not have a milestone' do it 'does not do anything if the MR already does not have a milestone' do
expect(mutated_merge_request.milestone).to eq(nil) expect(subject[:merge_request].milestone).to be_nil
end
end
end
context 'when issue assignee is a guest' do
let(:project) { create(:project, :public) }
before do
project.add_guest(user)
end
it 'does not update the milestone' do
expect(subject[:merge_request]).to eq(merge_request)
expect(subject[:merge_request].milestone).to be_nil
expect(subject[:errors]).to be_empty
end
context 'when passing milestone_id as nil' do
let(:milestone) { nil }
it 'does not remove the milestone' do
merge_request.update!(milestone: create(:milestone, project: project))
expect(subject[:merge_request].milestone).not_to be_nil
end end
end end
end end

View file

@ -115,6 +115,16 @@ RSpec.describe Gitlab::CurrentSettings do
expect(settings).to have_attributes(settings_from_defaults) expect(settings).to have_attributes(settings_from_defaults)
end end
context 'when ApplicationSettings does not have a primary key' do
before do
allow(ActiveRecord::Base.connection).to receive(:primary_key).with('application_settings').and_return(nil)
end
it 'raises an exception if ApplicationSettings does not have a primary key' do
expect { described_class.current_application_settings }.to raise_error(/table is missing a primary key constraint/)
end
end
context 'with pending migrations' do context 'with pending migrations' do
let(:current_settings) { described_class.current_application_settings } let(:current_settings) { described_class.current_application_settings }

View file

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join(
'db',
'migrate',
'20200831222347_insert_project_feature_flags_plan_limits.rb'
)
RSpec.describe InsertProjectFeatureFlagsPlanLimits do
let(:migration) { described_class.new }
let(:plans) { table(:plans) }
let(:plan_limits) { table(:plan_limits) }
let!(:default_plan) { plans.create!(name: 'default') }
let!(:free_plan) { plans.create!(name: 'free') }
let!(:bronze_plan) { plans.create!(name: 'bronze') }
let!(:silver_plan) { plans.create!(name: 'silver') }
let!(:gold_plan) { plans.create!(name: 'gold') }
let!(:default_plan_limits) do
plan_limits.create!(plan_id: default_plan.id, project_feature_flags: 200)
end
context 'when on Gitlab.com' do
before do
expect(Gitlab).to receive(:com?).at_most(:twice).and_return(true)
end
describe '#up' do
it 'updates the project_feature_flags plan limits' do
migration.up
expect(plan_limits.pluck(:plan_id, :project_feature_flags)).to contain_exactly(
[default_plan.id, 200],
[free_plan.id, 50],
[bronze_plan.id, 100],
[silver_plan.id, 150],
[gold_plan.id, 200]
)
end
end
describe '#down' do
it 'removes the project_feature_flags plan limits' do
migration.up
migration.down
expect(plan_limits.pluck(:plan_id, :project_feature_flags)).to contain_exactly(
[default_plan.id, 200],
[free_plan.id, 0],
[bronze_plan.id, 0],
[silver_plan.id, 0],
[gold_plan.id, 0]
)
end
end
end
context 'when on self-hosted' do
before do
expect(Gitlab).to receive(:com?).at_most(:twice).and_return(false)
end
describe '#up' do
it 'does not change the plan limits' do
migration.up
expect(plan_limits.pluck(:project_feature_flags)).to contain_exactly(200)
end
end
describe '#down' do
it 'does not change the plan limits' do
migration.up
migration.down
expect(plan_limits.pluck(:project_feature_flags)).to contain_exactly(200)
end
end
end
end

View file

@ -23,7 +23,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
describe '#current?' do describe '#current?' do
it 'returns true if the active session matches the current session' do it 'returns true if the active session matches the current session' do
active_session = ActiveSession.new(session_id: rack_session) active_session = ActiveSession.new(session_private_id: rack_session.private_id)
expect(active_session.current?(session)).to be true expect(active_session.current?(session)).to be true
end end
@ -45,7 +45,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
describe '#public_id' do describe '#public_id' do
it 'returns an encrypted, url-encoded session id' do it 'returns an encrypted, url-encoded session id' do
original_session_id = Rack::Session::SessionId.new("!*'();:@&\n=+$,/?%abcd#123[4567]8") original_session_id = Rack::Session::SessionId.new("!*'();:@&\n=+$,/?%abcd#123[4567]8")
active_session = ActiveSession.new(session_id: original_session_id) active_session = ActiveSession.new(session_id: original_session_id.public_id)
encrypted_id = active_session.public_id encrypted_id = active_session.public_id
derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id) derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
@ -106,8 +106,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
redis.sadd( redis.sadd(
"session:lookup:user:gitlab:#{user.id}", "session:lookup:user:gitlab:#{user.id}",
%w[ %w[
6919a6f1bb119dd7396fadc38fd18d0d 2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae
59822c7d9fcdfa03725eff41782ad97d 2::d2ee6f70d6ef0e8701efa3f6b281cbe8e6bf3d109ef052a8b5ce88bfc7e71c26
] ]
) )
end end
@ -135,7 +135,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
redis.set("session:gitlab:#{rack_session.private_id}", Marshal.dump({ _csrf_token: 'abcd' })) redis.set("session:gitlab:#{rack_session.private_id}", Marshal.dump({ _csrf_token: 'abcd' }))
end end
expect(ActiveSession.sessions_from_ids([rack_session])).to eq [{ _csrf_token: 'abcd' }] expect(ActiveSession.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }]
end end
it 'avoids a redis lookup for an empty array' do it 'avoids a redis lookup for an empty array' do
@ -150,12 +150,11 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
redis = double(:redis) redis = double(:redis)
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
sessions = %w[session-a session-b session-c session-d] sessions = %w[session-a session-b]
mget_responses = sessions.map { |session| [Marshal.dump(session)]} mget_responses = sessions.map { |session| [Marshal.dump(session)]}
expect(redis).to receive(:mget).exactly(4).times.and_return(*mget_responses) expect(redis).to receive(:mget).twice.times.and_return(*mget_responses)
session_ids = [1, 2].map { |id| Rack::Session::SessionId.new(id.to_s) } expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions)
expect(ActiveSession.sessions_from_ids(session_ids).map(&:to_s)).to eql(sessions)
end end
end end
@ -165,7 +164,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each.to_a).to include( expect(redis.scan_each.to_a).to include(
"session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", "session:user:gitlab:#{user.id}:2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae",
"session:lookup:user:gitlab:#{user.id}" "session:lookup:user:gitlab:#{user.id}"
) )
end end
@ -208,13 +207,41 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end end
end end
end end
context 'ActiveSession stored by deprecated rack_session_public_key' do
let(:active_session) { ActiveSession.new(session_id: rack_session.public_id) }
let(:deprecated_active_session_lookup_key) { rack_session.public_id }
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{deprecated_active_session_lookup_key}",
'')
redis.sadd(described_class.lookup_key_name(user.id),
deprecated_active_session_lookup_key)
end
end end
describe '.destroy' do it 'removes deprecated key and stores only new one' do
expected_session_keys = ["session:user:gitlab:#{user.id}:#{rack_session.private_id}",
"session:lookup:user:gitlab:#{user.id}"]
ActiveSession.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
actual_session_keys = redis.scan_each(match: 'session:*').to_a
expect(actual_session_keys).to(match_array(expected_session_keys))
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq [rack_session.private_id]
end
end
end
end
describe '.destroy_with_rack_session_id' do
it 'gracefully handles a nil session ID' do it 'gracefully handles a nil session ID' do
expect(described_class).not_to receive(:destroy_sessions) expect(described_class).not_to receive(:destroy_sessions)
ActiveSession.destroy(user, nil) ActiveSession.destroy_with_rack_session_id(user, nil)
end end
it 'removes the entry associated with the currently killed user session' do it 'removes the entry associated with the currently killed user session' do
@ -224,7 +251,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '') redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
end end
ActiveSession.destroy(user, request.session.id) ActiveSession.destroy_with_rack_session_id(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [ expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
@ -240,7 +267,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d') redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
end end
ActiveSession.destroy(user, request.session.id) ActiveSession.destroy_with_rack_session_id(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
@ -249,12 +276,12 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
it 'removes the devise session' do it 'removes the devise session' do
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{rack_session.public_id}", '') redis.set("session:user:gitlab:#{user.id}:#{rack_session.private_id}", '')
# Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88 # Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88
redis.set("session:gitlab:#{rack_session.private_id}", '') redis.set("session:gitlab:#{rack_session.private_id}", '')
end end
ActiveSession.destroy(user, request.session.id) ActiveSession.destroy_with_rack_session_id(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
@ -262,37 +289,83 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end end
end end
describe '.destroy_with_public_id' do describe '.destroy_with_deprecated_encryption' do
it 'receives a user and public id and destroys the associated session' do shared_examples 'removes all session data' do
ActiveSession.set(user, request) before do
session = ActiveSession.list(user).first Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{active_session_lookup_key}", '')
# Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88
redis.set("session:gitlab:#{rack_session.private_id}", '')
ActiveSession.destroy_with_public_id(user, session.public_id) redis.set(described_class.key_name(user.id, active_session_lookup_key),
Marshal.dump(active_session))
total_sessions = ActiveSession.list(user).count redis.sadd(described_class.lookup_key_name(user.id),
expect(total_sessions).to eq 0 active_session_lookup_key)
end
end end
it 'handles invalid input for public id' do it 'removes the devise session' do
expect do subject
ActiveSession.destroy_with_public_id(user, nil)
end.not_to raise_error
expect do Gitlab::Redis::SharedState.with do |redis|
ActiveSession.destroy_with_public_id(user, "") expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
end.not_to raise_error end
expect do
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
end.not_to raise_error
end end
it 'does not attempt to destroy session when given invalid input for public id' do it 'removes the lookup entry' do
expect(ActiveSession).not_to receive(:destroy) subject
ActiveSession.destroy_with_public_id(user, nil) Gitlab::Redis::SharedState.with do |redis|
ActiveSession.destroy_with_public_id(user, "") expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
ActiveSession.destroy_with_public_id(user, "aaaaaaaa") end
end
it 'removes the ActiveSession' do
subject
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:user:gitlab:*").to_a).to be_empty
end
end
end
context 'destroy called with Rack::Session::SessionId#private_id' do
subject { ActiveSession.destroy_with_deprecated_encryption(user, rack_session.private_id) }
it 'calls .destroy_sessions' do
expect(ActiveSession).to(
receive(:destroy_sessions)
.with(anything, user, [rack_session.private_id]))
subject
end
context 'ActiveSession with session_private_id' do
let(:active_session) { ActiveSession.new(session_private_id: rack_session.private_id) }
let(:active_session_lookup_key) { rack_session.private_id }
include_examples 'removes all session data'
end
end
context 'destroy called with ActiveSession#public_id (deprecated)' do
let(:active_session) { ActiveSession.new(session_id: rack_session.public_id) }
let(:encrypted_active_session_id) { active_session.public_id }
let(:active_session_lookup_key) { rack_session.public_id }
subject { ActiveSession.destroy_with_deprecated_encryption(user, encrypted_active_session_id) }
it 'calls .destroy_sessions' do
expect(ActiveSession).to(
receive(:destroy_sessions)
.with(anything, user, [active_session.public_id, rack_session.public_id, rack_session.private_id]))
subject
end
context 'ActiveSession with session_id (deprecated)' do
include_examples 'removes all session data'
end
end end
end end
@ -308,29 +381,43 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
before do before do
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
redis.set(described_class.key_name(user.id, current_session_id), # setup for current user
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new(current_session_id)))) [current_session_id, '59822c7d9fcdfa03725eff41782ad97d'].each do |session_public_id|
redis.set(described_class.key_name(user.id, '59822c7d9fcdfa03725eff41782ad97d'), session_private_id = Rack::Session::SessionId.new(session_public_id).private_id
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new('59822c7d9fcdfa03725eff41782ad97d')))) active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(described_class.key_name(9999, '5c8611e4f9c69645ad1a1492f4131358'), redis.set(described_class.key_name(user.id, session_private_id),
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358')))) Marshal.dump(active_session))
redis.sadd(described_class.lookup_key_name(user.id), '59822c7d9fcdfa03725eff41782ad97d') redis.sadd(described_class.lookup_key_name(user.id),
redis.sadd(described_class.lookup_key_name(user.id), current_session_id) session_private_id)
redis.sadd(described_class.lookup_key_name(9999), '5c8611e4f9c69645ad1a1492f4131358') end
# setup for unrelated user
unrelated_user_id = 9999
session_private_id = Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358').private_id
active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(described_class.key_name(unrelated_user_id, session_private_id),
Marshal.dump(active_session))
redis.sadd(described_class.lookup_key_name(unrelated_user_id),
session_private_id)
end end
end end
it 'removes the entry associated with the all user sessions but current' do it 'removes the entry associated with the all user sessions but current' do
expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(2).to(1) expect { ActiveSession.destroy_all_but_current(user, request.session) }
.to(change { ActiveSession.session_ids_for_user(user.id).size }.from(2).to(1))
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1) expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
end end
it 'removes the lookup entry of deleted sessions' do it 'removes the lookup entry of deleted sessions' do
session_private_id = Rack::Session::SessionId.new(current_session_id).private_id
ActiveSession.destroy_all_but_current(user, request.session) ActiveSession.destroy_all_but_current(user, request.session)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers(described_class.lookup_key_name(user.id))).to eq [current_session_id] expect(
redis.smembers(described_class.lookup_key_name(user.id))
).to eq([session_private_id])
end end
end end
@ -464,5 +551,38 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end end
end end
end end
context 'cleaning up old sessions stored by Rack::Session::SessionId#private_id' do
let(:max_number_of_sessions_plus_one) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 1 }
let(:max_number_of_sessions_plus_two) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 2 }
before do
Gitlab::Redis::SharedState.with do |redis|
(1..max_number_of_sessions_plus_two).each do |number|
redis.set(
"session:user:gitlab:#{user.id}:#{number}",
Marshal.dump(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago))
)
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
"#{number}"
)
end
end
end
it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
expect(sessions.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
expect(sessions).not_to(
include("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}",
"session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}"))
end
end
end
end end
end end

View file

@ -654,6 +654,16 @@ RSpec.describe ApplicationSetting do
end end
end end
context 'when ApplicationSettings does not have a primary key' do
before do
allow(ActiveRecord::Base.connection).to receive(:primary_key).with(described_class.table_name).and_return(nil)
end
it 'raises an exception' do
expect { described_class.create_from_defaults }.to raise_error(/table is missing a primary key constraint/)
end
end
describe '#disabled_oauth_sign_in_sources=' do describe '#disabled_oauth_sign_in_sources=' do
before do before do
allow(Devise).to receive(:omniauth_providers).and_return([:github]) allow(Devise).to receive(:omniauth_providers).and_return([:github])

View file

@ -4,9 +4,13 @@ require 'spec_helper'
RSpec.describe Expirable do RSpec.describe Expirable do
describe 'ProjectMember' do describe 'ProjectMember' do
let(:no_expire) { create(:project_member) } let_it_be(:no_expire) { create(:project_member) }
let(:expire_later) { create(:project_member, expires_at: Time.current + 6.days) } let_it_be(:expire_later) { create(:project_member, expires_at: 8.days.from_now) }
let(:expired) { create(:project_member, expires_at: Time.current - 6.days) } let_it_be(:expired) { create(:project_member, expires_at: 1.day.from_now) }
before do
travel_to(3.days.from_now)
end
describe '.expired' do describe '.expired' do
it { expect(ProjectMember.expired).to match_array([expired]) } it { expect(ProjectMember.expired).to match_array([expired]) }

View file

@ -18,6 +18,13 @@ RSpec.describe Member do
it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) } it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
context 'expires_at' do
it { is_expected.not_to allow_value(Date.yesterday).for(:expires_at) }
it { is_expected.to allow_value(Date.tomorrow).for(:expires_at) }
it { is_expected.to allow_value(Date.today).for(:expires_at) }
it { is_expected.to allow_value(nil).for(:expires_at) }
end
it_behaves_like 'an object with email-formated attributes', :invite_email do it_behaves_like 'an object with email-formated attributes', :invite_email do
subject { build(:project_member) } subject { build(:project_member) }
end end

View file

@ -44,8 +44,9 @@ RSpec.describe ProjectMember do
let(:maintainer) { create(:project_member, project: project) } let(:maintainer) { create(:project_member, project: project) }
it "creates an expired event when left due to expiry" do it "creates an expired event when left due to expiry" do
expired = create(:project_member, project: project, expires_at: Time.current - 6.days) expired = create(:project_member, project: project, expires_at: 1.day.from_now)
expired.destroy travel_to(2.days.from_now) { expired.destroy }
expect(Event.recent.first).to be_expired_action expect(Event.recent.first).to be_expired_action
end end

View file

@ -67,4 +67,29 @@ RSpec.describe API::API do
end end
end end
end end
describe 'authentication with deploy token' do
context 'admin mode' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:package) { create(:maven_package, project: project, name: project.full_path) }
let_it_be(:maven_metadatum) { package.maven_metadatum }
let_it_be(:package_file) { package.package_files.first }
let_it_be(:deploy_token) { create(:deploy_token) }
let(:headers_with_deploy_token) do
{
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
}
end
it 'does not bypass the session' do
expect(Gitlab::Auth::CurrentUserMode).not_to receive(:bypass_session!)
get(api("/packages/maven/#{maven_metadatum.path}/#{package_file.file_name}"),
headers: headers_with_deploy_token)
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
end
end
end end

View file

@ -502,16 +502,13 @@ RSpec.describe API::Files do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it 'sets no-cache headers' do it_behaves_like 'uncached response' do
before do
url = route('.gitignore') + "/raw" url = route('.gitignore') + "/raw"
expect(Gitlab::Workhorse).to receive(:send_git_blob) expect(Gitlab::Workhorse).to receive(:send_git_blob)
get api(url, current_user), params: params get api(url, current_user), params: params
end
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")
end end
context 'when mandatory params are not given' do context 'when mandatory params are not given' do

View file

@ -9,13 +9,9 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
let_it_be(:project) { create(:project, :private, :repository) } let_it_be(:project) { create(:project, :private, :repository) }
let_it_be(:environment) { create(:environment, project: project) } let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment) } let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment) }
let(:mutation) do
variables = {
id: GitlabSchema.id_from_object(annotation).to_s
}
graphql_mutation(:delete_annotation, variables) let(:variables) { { id: GitlabSchema.id_from_object(annotation).to_s } }
end let(:mutation) { graphql_mutation(:delete_annotation, variables) }
def mutation_response def mutation_response
graphql_mutation_response(:delete_annotation) graphql_mutation_response(:delete_annotation)
@ -37,15 +33,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
end end
context 'with invalid params' do context 'with invalid params' do
let(:mutation) do let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } }
variables = {
id: 'invalid_id'
}
graphql_mutation(:delete_annotation, variables) it_behaves_like 'a mutation that returns top-level errors' do
let(:match_errors) { eq(["#{variables[:id]} is not a valid id for #{annotation.class}."]) }
end end
it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
end end
context 'when the delete fails' do context 'when the delete fails' do

View file

@ -244,13 +244,12 @@ RSpec.describe API::Members do
it 'creates a new member' do it 'creates a new member' do
expect do expect do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' } params: { user_id: stranger.id, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end.to change { source.members.count }.by(1) end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id) expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER) expect(json_response['access_level']).to eq(Member::DEVELOPER)
expect(json_response['expires_at']).to eq('2016-08-05')
end end
end end
@ -285,6 +284,40 @@ RSpec.describe API::Members do
end end
end end
context 'access expiry date' do
subject do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: expires_at }
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago.to_date }
it 'does not create a member' do
expect do
subject
end.not_to change { source.members.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq({ 'expires_at' => ['cannot be a date in the past'] })
end
end
context 'when set to a date in the future' do
let(:expires_at) { 2.days.from_now.to_date }
it 'creates a member' do
expect do
subject
end.to change { source.members.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['expires_at']).to eq(expires_at.to_s)
end
end
end
it "returns 409 if member already exists" do it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: maintainer.id, access_level: Member::MAINTAINER } params: { user_id: maintainer.id, access_level: Member::MAINTAINER }
@ -369,12 +402,40 @@ RSpec.describe API::Members do
context 'when authenticated as a maintainer/owner' do context 'when authenticated as a maintainer/owner' do
it 'updates the member' do it 'updates the member' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer), put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
params: { access_level: Member::MAINTAINER, expires_at: '2016-08-05' } params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(developer.id) expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::MAINTAINER) expect(json_response['access_level']).to eq(Member::MAINTAINER)
expect(json_response['expires_at']).to eq('2016-08-05') end
end
context 'access expiry date' do
subject do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
params: { expires_at: expires_at, access_level: Member::MAINTAINER }
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago.to_date }
it 'does not update the member' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq({ 'expires_at' => ['cannot be a date in the past'] })
end
end
context 'when set to a date in the future' do
let(:expires_at) { 2.days.from_now.to_date }
it 'updates the member' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['expires_at']).to eq(expires_at.to_s)
end
end end
end end

View file

@ -25,6 +25,7 @@ RSpec.describe Issues::CreateService do
assignee_ids: [assignee.id], assignee_ids: [assignee.id],
label_ids: labels.map(&:id), label_ids: labels.map(&:id),
milestone_id: milestone.id, milestone_id: milestone.id,
milestone: milestone,
due_date: Date.tomorrow } due_date: Date.tomorrow }
end end
@ -59,6 +60,12 @@ RSpec.describe Issues::CreateService do
expect(issue.milestone).to be_nil expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil expect(issue.due_date).to be_nil
end end
it 'creates confidential issues' do
issue = described_class.new(project, guest, confidential: true).execute
expect(issue.confidential).to be_truthy
end
end end
it 'creates a pending todo for new assignee' do it 'creates a pending todo for new assignee' do

View file

@ -10,6 +10,7 @@ RSpec.describe Issues::UpdateService, :mailer do
let_it_be(:project, reload: true) { create(:project, :repository, group: group) } let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
let_it_be(:label) { create(:label, project: project) } let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:issue) do let(:issue) do
create(:issue, title: 'Old title', create(:issue, title: 'Old title',
@ -52,7 +53,8 @@ RSpec.describe Issues::UpdateService, :mailer do
state_event: 'close', state_event: 'close',
label_ids: [label.id], label_ids: [label.id],
due_date: Date.tomorrow, due_date: Date.tomorrow,
discussion_locked: true discussion_locked: true,
milestone_id: milestone.id
} }
end end
@ -69,6 +71,14 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.labels).to match_array [label] expect(issue.labels).to match_array [label]
expect(issue.due_date).to eq Date.tomorrow expect(issue.due_date).to eq Date.tomorrow
expect(issue.discussion_locked).to be_truthy expect(issue.discussion_locked).to be_truthy
expect(issue.confidential).to be_falsey
expect(issue.milestone).to eq milestone
end
it 'updates issue milestone when passing `milestone` param' do
update_issue(milestone: milestone)
expect(issue.milestone).to eq milestone
end end
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
@ -82,6 +92,8 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, issue.id) expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, issue.id)
update_issue(confidential: true) update_issue(confidential: true)
expect(issue.confidential).to be_truthy
end end
it 'does not enqueue ConfidentialIssueWorker when an issue is made non confidential' do it 'does not enqueue ConfidentialIssueWorker when an issue is made non confidential' do
@ -91,6 +103,8 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(TodosDestroyer::ConfidentialIssueWorker).not_to receive(:perform_in) expect(TodosDestroyer::ConfidentialIssueWorker).not_to receive(:perform_in)
update_issue(confidential: false) update_issue(confidential: false)
expect(issue.confidential).to be_falsey
end end
it 'updates open issue counter for assignees when issue is reassigned' do it 'updates open issue counter for assignees when issue is reassigned' do
@ -157,7 +171,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end end
it 'filters out params that cannot be set without the :admin_issue permission' do it 'filters out params that cannot be set without the :admin_issue permission' do
described_class.new(project, guest, opts).execute(issue) described_class.new(project, guest, opts.merge(confidential: true)).execute(issue)
expect(issue).to be_valid expect(issue).to be_valid
expect(issue.title).to eq 'New title' expect(issue.title).to eq 'New title'
@ -167,6 +181,7 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.milestone).to be_nil expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil expect(issue.due_date).to be_nil
expect(issue.discussion_locked).to be_falsey expect(issue.discussion_locked).to be_falsey
expect(issue.confidential).to be_falsey
end end
end end

View file

@ -31,11 +31,8 @@ RSpec.describe Members::UpdateService do
end end
context 'when member is downgraded to guest' do context 'when member is downgraded to guest' do
let(:params) do shared_examples 'schedules to delete confidential todos' do
{ access_level: Gitlab::Access::GUEST } it do
end
it 'schedules to delete confidential todos' do
expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
updated_member = described_class.new(current_user, params).execute(member, permission: permission) updated_member = described_class.new(current_user, params).execute(member, permission: permission)
@ -44,6 +41,27 @@ RSpec.describe Members::UpdateService do
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST) expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
end end
end end
context 'with Gitlab::Access::GUEST level as a string' do
let(:params) { { access_level: Gitlab::Access::GUEST.to_s } }
it_behaves_like 'schedules to delete confidential todos'
end
context 'with Gitlab::Access::GUEST level as an integer' do
let(:params) { { access_level: Gitlab::Access::GUEST } }
it_behaves_like 'schedules to delete confidential todos'
end
end
context 'when access_level is invalid' do
let(:params) { { access_level: 'invalid' } }
it 'raises an error' do
expect { described_class.new(current_user, params) }.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
end
end
end end
before do before do

View file

@ -6,12 +6,13 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
include ProjectForksHelper include ProjectForksHelper
let(:group) { create(:group, :public) } let(:group) { create(:group, :public) }
let(:project) { create(:project, :repository, group: group) } let(:project) { create(:project, :private, :repository, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
let(:label2) { create(:label) } let(:label2) { create(:label) }
let(:milestone) { create(:milestone, project: project) }
let(:merge_request) do let(:merge_request) do
create(:merge_request, :simple, title: 'Old title', create(:merge_request, :simple, title: 'Old title',
@ -60,7 +61,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
} }
end end
let(:service) { described_class.new(project, user, opts) } let(:service) { described_class.new(project, current_user, opts) }
let(:current_user) { user }
before do before do
allow(service).to receive(:execute_hooks) allow(service).to receive(:execute_hooks)
@ -83,6 +85,26 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
expect(@merge_request.discussion_locked).to be_truthy expect(@merge_request.discussion_locked).to be_truthy
end end
context 'updating milestone' do
RSpec.shared_examples 'updates milestone' do
it 'sets milestone' do
expect(@merge_request.milestone).to eq milestone
end
end
context 'when milestone_id param' do
let(:opts) { { milestone_id: milestone.id } }
it_behaves_like 'updates milestone'
end
context 'when milestone param' do
let(:opts) { { milestone: milestone } }
it_behaves_like 'updates milestone'
end
end
it 'executes hooks with update action' do it 'executes hooks with update action' do
expect(service).to have_received(:execute_hooks) expect(service).to have_received(:execute_hooks)
.with( .with(
@ -150,6 +172,46 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
expect(note.note).to eq 'locked this merge request' expect(note.note).to eq 'locked this merge request'
end end
context 'when current user cannot admin issues in the project' do
let(:guest) { create(:user) }
let(:current_user) { guest }
before do
project.add_guest(guest)
end
it 'filters out params that cannot be set without the :admin_merge_request permission' do
expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('New title')
expect(@merge_request.assignees).to match_array([user3])
expect(@merge_request).to be_opened
expect(@merge_request.labels.count).to eq(0)
expect(@merge_request.target_branch).to eq('target')
expect(@merge_request.discussion_locked).to be_falsey
expect(@merge_request.milestone).to be_nil
end
context 'updating milestone' do
RSpec.shared_examples 'does not update milestone' do
it 'sets milestone' do
expect(@merge_request.milestone).to be_nil
end
end
context 'when milestone_id param' do
let(:opts) { { milestone_id: milestone.id } }
it_behaves_like 'does not update milestone'
end
context 'when milestone param' do
let(:opts) { { milestone: milestone } }
it_behaves_like 'does not update milestone'
end
end
end
context 'when not including source branch removal options' do context 'when not including source branch removal options' do
before do before do
opts.delete(:force_remove_source_branch) opts.delete(:force_remove_source_branch)

View file

@ -3,77 +3,145 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Todos::Destroy::EntityLeaveService do RSpec.describe Todos::Destroy::EntityLeaveService do
let(:group) { create(:group, :private) } let_it_be(:user, reload: true) { create(:user) }
let(:project) { create(:project, group: group) } let_it_be(:user2, reload: true) { create(:user) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:issue) { create(:issue, project: project, confidential: true) }
let(:mr) { create(:merge_request, source_project: project) }
let!(:todo_mr_user) { create(:todo, user: user, target: mr, project: project) } let(:group) { create(:group, :private) }
let!(:todo_issue_user) { create(:todo, user: user, target: issue, project: project) } let(:project) { create(:project, :private, group: group) }
let(:issue) { create(:issue, project: project) }
let(:issue_c) { create(:issue, project: project, confidential: true) }
let!(:todo_group_user) { create(:todo, user: user, group: group) } let!(:todo_group_user) { create(:todo, user: user, group: group) }
let!(:todo_issue_user2) { create(:todo, user: user2, target: issue, project: project) }
let!(:todo_group_user2) { create(:todo, user: user2, group: group) } let!(:todo_group_user2) { create(:todo, user: user2, group: group) }
let(:mr) { create(:merge_request, source_project: project) }
let!(:todo_mr_user) { create(:todo, user: user, target: mr, project: project) }
let!(:todo_issue_user) { create(:todo, user: user, target: issue, project: project) }
let!(:todo_issue_c_user) { create(:todo, user: user, target: issue_c, project: project) }
let!(:todo_issue_c_user2) { create(:todo, user: user2, target: issue_c, project: project) }
shared_examples 'using different access permissions' do |access_table|
using RSpec::Parameterized::TableSyntax
where(:group_access, :project_access, :c_todos, :mr_todos, :method, &access_table)
with_them do
before do
set_access(project, user, project_access) if project_access
set_access(group, user, group_access) if group_access
end
it "#{params[:method].to_s.humanize(capitalize: false)}" do
send(method)
end
end
end
shared_examples 'does not remove any todos' do
it { does_not_remove_any_todos }
end
shared_examples 'removes only confidential issues todos' do
it { removes_only_confidential_issues_todos }
end
def does_not_remove_any_todos
expect { subject }.not_to change { Todo.count }
end
def removes_only_confidential_issues_todos
expect { subject }.to change { Todo.count }.from(6).to(5)
end
def removes_confidential_issues_and_merge_request_todos
expect { subject }.to change { Todo.count }.from(6).to(4)
expect(user.todos).to match_array([todo_issue_user, todo_group_user])
end
def set_access(object, user, access_name)
case access_name
when :developer
object.add_developer(user)
when :reporter
object.add_reporter(user)
when :guest
object.add_guest(user)
end
end
describe '#execute' do describe '#execute' do
context 'when a user leaves a project' do describe 'updating a Project' do
subject { described_class.new(user.id, project.id, 'Project').execute } subject { described_class.new(user.id, project.id, 'Project').execute }
# a private project in a private group is valid
context 'when project is private' do context 'when project is private' do
context 'when user is not a member of the project' do
it 'removes project todos for the provided user' do it 'removes project todos for the provided user' do
expect { subject }.to change { Todo.count }.from(5).to(3) expect { subject }.to change { Todo.count }.from(6).to(3)
expect(user.todos).to match_array([todo_group_user]) expect(user.todos).to match_array([todo_group_user])
expect(user2.todos).to match_array([todo_issue_user2, todo_group_user2]) expect(user2.todos).to match_array([todo_issue_c_user2, todo_group_user2])
end
context 'when the user is member of the project' do
before do
project.add_developer(user)
end
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end end
end end
context 'when the user is a project guest' do context 'access permissions' do
before do # rubocop:disable RSpec/LeakyConstantDeclaration
project.add_guest(user) PRIVATE_PROJECT_PRIVATE_GROUP_ACCESS_TABLE =
lambda do |_|
[
# :group_access, :project_access, :c_todos, :mr_todos, :method
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
[nil, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
[:guest, nil, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
[:guest, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos]
]
end end
# rubocop:enable RSpec/LeakyConstantDeclaration
it 'removes only confidential issues todos' do it_behaves_like 'using different access permissions', PRIVATE_PROJECT_PRIVATE_GROUP_ACCESS_TABLE
expect { subject }.to change { Todo.count }.from(5).to(4)
end end
end end
context 'when the user is member of a parent group' do # a private project in an internal/public group is valid
before do context 'when project is private in an internal/public group' do
group.add_developer(user) let(:group) { create(:group, :internal) }
end
it 'does not remove any todos' do context 'when user is not a member of the project' do
expect { subject }.not_to change { Todo.count } it 'removes project todos for the provided user' do
expect { subject }.to change { Todo.count }.from(6).to(3)
expect(user.todos).to match_array([todo_group_user])
expect(user2.todos).to match_array([todo_issue_c_user2, todo_group_user2])
end end
end end
context 'when the user is guest of a parent group' do context 'access permissions' do
before do # rubocop:disable RSpec/LeakyConstantDeclaration
project.add_guest(user) PRIVATE_PROJECT_INTERNAL_GROUP_ACCESS_TABLE =
lambda do |_|
[
# :group_access, :project_access, :c_todos, :mr_todos, :method
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
[nil, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
[:guest, nil, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
[:guest, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos]
]
end end
# rubocop:enable RSpec/LeakyConstantDeclaration
it 'removes only confidential issues todos' do it_behaves_like 'using different access permissions', PRIVATE_PROJECT_INTERNAL_GROUP_ACCESS_TABLE
expect { subject }.to change { Todo.count }.from(5).to(4)
end
end end
end end
# an internal project in an internal/public group is valid
context 'when project is not private' do context 'when project is not private' do
before do let(:group) { create(:group, :internal) }
group.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) let(:project) { create(:project, :internal, group: group) }
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) let(:issue) { create(:issue, project: project) }
end let(:issue_c) { create(:issue, project: project, confidential: true) }
it 'enqueues the PrivateFeaturesWorker' do it 'enqueues the PrivateFeaturesWorker' do
expect(TodosDestroyer::PrivateFeaturesWorker) expect(TodosDestroyer::PrivateFeaturesWorker)
@ -84,50 +152,42 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
context 'confidential issues' do context 'confidential issues' do
context 'when a user is not an author of confidential issue' do context 'when a user is not an author of confidential issue' do
it 'removes only confidential issues todos' do it_behaves_like 'removes only confidential issues todos'
expect { subject }.to change { Todo.count }.from(5).to(4)
end
end end
context 'when a user is an author of confidential issue' do context 'when a user is an author of confidential issue' do
before do before do
issue.update!(author: user) issue_c.update!(author: user)
end end
it 'does not remove any todos' do it_behaves_like 'does not remove any todos'
expect { subject }.not_to change { Todo.count }
end
end end
context 'when a user is an assignee of confidential issue' do context 'when a user is an assignee of confidential issue' do
before do before do
issue.assignees << user issue_c.assignees << user
end end
it 'does not remove any todos' do it_behaves_like 'does not remove any todos'
expect { subject }.not_to change { Todo.count }
end
end end
context 'when a user is a project guest' do context 'access permissions' do
before do # rubocop:disable RSpec/LeakyConstantDeclaration
project.add_guest(user) INTERNAL_PROJECT_INTERNAL_GROUP_ACCESS_TABLE =
lambda do |_|
[
# :group_access, :project_access, :c_todos, :mr_todos, :method
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
[nil, :guest, :delete, :keep, :removes_only_confidential_issues_todos],
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
[:guest, nil, :delete, :keep, :removes_only_confidential_issues_todos],
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
[:guest, :guest, :delete, :keep, :removes_only_confidential_issues_todos]
]
end end
# rubocop:enable RSpec/LeakyConstantDeclaration
it 'removes only confidential issues todos' do it_behaves_like 'using different access permissions', INTERNAL_PROJECT_INTERNAL_GROUP_ACCESS_TABLE
expect { subject }.to change { Todo.count }.from(5).to(4)
end
end
context 'when a user is a project guest but group developer' do
before do
project.add_guest(user)
group.add_developer(user)
end
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end
end end
end end
@ -138,42 +198,43 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
end end
it 'removes only users issue todos' do it 'removes only users issue todos' do
expect { subject }.to change { Todo.count }.from(5).to(4) expect { subject }.to change { Todo.count }.from(6).to(5)
end end
end end
end end
end end
end end
context 'when a user leaves a group' do describe 'updating a Group' do
subject { described_class.new(user.id, group.id, 'Group').execute } subject { described_class.new(user.id, group.id, 'Group').execute }
context 'when group is private' do context 'when group is private' do
context 'when a user leaves a group' do
it 'removes group and subproject todos for the user' do it 'removes group and subproject todos for the user' do
expect { subject }.to change { Todo.count }.from(5).to(2) expect { subject }.to change { Todo.count }.from(6).to(2)
expect(user.todos).to be_empty expect(user.todos).to be_empty
expect(user2.todos).to match_array([todo_issue_user2, todo_group_user2]) expect(user2.todos).to match_array([todo_issue_c_user2, todo_group_user2])
end
context 'when the user is member of the group' do
before do
group.add_developer(user)
end
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end end
end end
context 'when the user is member of the group project but not the group' do context 'access permissions' do
before do # rubocop:disable RSpec/LeakyConstantDeclaration
project.add_developer(user) PRIVATE_GROUP_PRIVATE_PROJECT_ACCESS_TABLE =
lambda do |_|
[
# :group_access, :project_access, :c_todos, :mr_todos, :method
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
[nil, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
[:guest, nil, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
[:guest, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos]
]
end end
# rubocop:enable RSpec/LeakyConstantDeclaration
it 'does not remove any todos' do it_behaves_like 'using different access permissions', PRIVATE_GROUP_PRIVATE_PROJECT_ACCESS_TABLE
expect { subject }.not_to change { Todo.count }
end
end end
context 'with nested groups' do context 'with nested groups' do
@ -191,12 +252,12 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
context 'when the user is not a member of any groups/projects' do context 'when the user is not a member of any groups/projects' do
it 'removes todos for the user including subprojects todos' do it 'removes todos for the user including subprojects todos' do
expect { subject }.to change { Todo.count }.from(11).to(4) expect { subject }.to change { Todo.count }.from(12).to(4)
expect(user.todos).to be_empty expect(user.todos).to be_empty
expect(user2.todos) expect(user2.todos)
.to match_array( .to match_array(
[todo_issue_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2] [todo_issue_c_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
) )
end end
end end
@ -208,9 +269,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
parent_group.add_developer(user) parent_group.add_developer(user)
end end
it 'does not remove any todos' do it_behaves_like 'does not remove any todos'
expect { subject }.not_to change { Todo.count }
end
end end
context 'when the user is member of a subgroup' do context 'when the user is member of a subgroup' do
@ -219,12 +278,12 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
end end
it 'does not remove group and subproject todos' do it 'does not remove group and subproject todos' do
expect { subject }.to change { Todo.count }.from(11).to(7) expect { subject }.to change { Todo.count }.from(12).to(7)
expect(user.todos).to match_array([todo_group_user, todo_subgroup_user, todo_subproject_user]) expect(user.todos).to match_array([todo_group_user, todo_subgroup_user, todo_subproject_user])
expect(user2.todos) expect(user2.todos)
.to match_array( .to match_array(
[todo_issue_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2] [todo_issue_c_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
) )
end end
end end
@ -235,12 +294,12 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
end end
it 'does not remove subproject and group todos' do it 'does not remove subproject and group todos' do
expect { subject }.to change { Todo.count }.from(11).to(7) expect { subject }.to change { Todo.count }.from(12).to(7)
expect(user.todos).to match_array([todo_subgroup_user, todo_group_user, todo_subproject_user]) expect(user.todos).to match_array([todo_subgroup_user, todo_group_user, todo_subproject_user])
expect(user2.todos) expect(user2.todos)
.to match_array( .to match_array(
[todo_issue_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2] [todo_issue_c_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
) )
end end
end end
@ -248,10 +307,10 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
end end
context 'when group is not private' do context 'when group is not private' do
before do let(:group) { create(:group, :internal) }
group.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) let(:project) { create(:project, :internal, group: group) }
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) let(:issue) { create(:issue, project: project) }
end let(:issue_c) { create(:issue, project: project, confidential: true) }
it 'enqueues the PrivateFeaturesWorker' do it 'enqueues the PrivateFeaturesWorker' do
expect(TodosDestroyer::PrivateFeaturesWorker) expect(TodosDestroyer::PrivateFeaturesWorker)
@ -260,31 +319,24 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
subject subject
end end
context 'when user is not member' do context 'access permissions' do
it 'removes only confidential issues todos' do # rubocop:disable RSpec/LeakyConstantDeclaration
expect { subject }.to change { Todo.count }.from(5).to(4) INTERNAL_GROUP_INTERNAL_PROJECT_ACCESS_TABLE =
end lambda do |_|
[
# :group_access, :project_access, :c_todos, :mr_todos, :method
[nil, nil, :delete, :keep, :removes_only_confidential_issues_todos],
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
[nil, :guest, :delete, :keep, :removes_only_confidential_issues_todos],
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
[:guest, nil, :delete, :keep, :removes_only_confidential_issues_todos],
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
[:guest, :guest, :delete, :keep, :removes_only_confidential_issues_todos]
]
end end
# rubocop:enable RSpec/LeakyConstantDeclaration
context 'when user is a project guest' do it_behaves_like 'using different access permissions', INTERNAL_GROUP_INTERNAL_PROJECT_ACCESS_TABLE
before do
project.add_guest(user)
end
it 'removes only confidential issues todos' do
expect { subject }.to change { Todo.count }.from(5).to(4)
end
end
context 'when user is a project guest & group developer' do
before do
project.add_guest(user)
group.add_developer(user)
end
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end
end end
end end
end end

View file

@ -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

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe FutureDateValidator do
subject do
Class.new do
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :expires_at
validates :expires_at, future_date: true
end.new
end
before do
subject.expires_at = date
end
context 'past date' do
let(:date) { Date.yesterday }
it { is_expected.not_to be_valid }
end
context 'current date' do
let(:date) { Date.today }
it { is_expected.to be_valid }
end
context 'future date' do
let(:date) { Date.tomorrow }
it { is_expected.to be_valid }
end
end

View file

@ -7,9 +7,13 @@ RSpec.describe RemoveExpiredMembersWorker do
describe '#perform' do describe '#perform' do
context 'project members' do context 'project members' do
let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) } let_it_be(:expired_project_member) { create(:project_member, expires_at: 1.day.from_now, access_level: GroupMember::DEVELOPER) }
let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) } let_it_be(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) } let_it_be(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
before do
travel_to(3.days.from_now)
end
it 'removes expired members' do it 'removes expired members' do
expect { worker.perform }.to change { Member.count }.by(-1) expect { worker.perform }.to change { Member.count }.by(-1)
@ -28,9 +32,13 @@ RSpec.describe RemoveExpiredMembersWorker do
end end
context 'group members' do context 'group members' do
let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) } let_it_be(:expired_group_member) { create(:group_member, expires_at: 1.day.from_now, access_level: GroupMember::DEVELOPER) }
let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) } let_it_be(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) } let_it_be(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
before do
travel_to(3.days.from_now)
end
it 'removes expired members' do it 'removes expired members' do
expect { worker.perform }.to change { Member.count }.by(-1) expect { worker.perform }.to change { Member.count }.by(-1)
@ -49,7 +57,11 @@ RSpec.describe RemoveExpiredMembersWorker do
end end
context 'when the last group owner expires' do context 'when the last group owner expires' do
let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) } let_it_be(:expired_group_owner) { create(:group_member, expires_at: 1.day.from_now, access_level: GroupMember::OWNER) }
before do
travel_to(3.days.from_now)
end
it 'does not delete the owner' do it 'does not delete the owner' do
worker.perform worker.perform

View file

@ -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