Commit d5cc1255 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge remote-tracking branch 'dev/13-4-stable' into 13-4-stable

parents c90be62b b08b36dc
...@@ -2,6 +2,26 @@ ...@@ -2,6 +2,26 @@
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.4.2 (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.20.2.
## 13.4.1 (2020-09-24) ## 13.4.1 (2020-09-24)
### Fixed (2 changes) ### Fixed (2 changes)
......
...@@ -15,7 +15,7 @@ module ApplicationCable ...@@ -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
......
...@@ -5,6 +5,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -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
...@@ -173,7 +174,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -173,7 +174,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.") }
...@@ -202,6 +203,24 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -202,6 +203,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
......
...@@ -6,7 +6,9 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController ...@@ -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|
......
...@@ -2,6 +2,8 @@ ...@@ -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 ...@@ -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
......
...@@ -12,6 +12,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -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
......
...@@ -10,7 +10,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -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
...@@ -124,6 +124,14 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -124,6 +124,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
......
...@@ -9,7 +9,7 @@ module Mutations ...@@ -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
......
...@@ -22,4 +22,13 @@ module ActiveSessionsHelper ...@@ -22,4 +22,13 @@ module ActiveSessionsHelper
sprite_icon(icon_name, css_class: 'gl-mt-2') sprite_icon(icon_name, 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
...@@ -5,7 +5,7 @@ module SafeParamsHelper ...@@ -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
......
...@@ -9,14 +9,14 @@ class ActiveSession ...@@ -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 ...@@ -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 ...@@ -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,27 +83,29 @@ class ActiveSession ...@@ -70,27 +83,29 @@ class ActiveSession
end end
end end
def self.destroy(user, session_id) def self.cleanup(user)
return unless session_id
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
destroy_sessions(redis, user, [session_id]) clean_up_old_sessions(redis, user)
cleaned_up_lookup_entries(redis, user)
end end
end end
def self.destroy_with_public_id(user, public_id) # TODO: remove in 13.7
decrypted_id = decrypt_public_id(public_id) # After upgrade there might be a duplicate ActiveSessions:
# - 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
return if decrypted_id.nil? Gitlab::Redis::SharedState.with do |redis|
destroy_sessions(redis, user, [rack_session_id.public_id, rack_session_id.private_id])
session_id = Rack::Session::SessionId.new(decrypted_id) end
destroy(user, session_id)
end end
def self.destroy_sessions(redis, user, session_ids) def self.destroy_sessions(redis, user, session_ids)
key_names = session_ids.map { |session_id| key_name(user.id, session_id.public_id) } key_names = session_ids.map { |session_id| key_name(user.id, session_id) }
redis.srem(lookup_key_name(user.id), session_ids.map(&:public_id)) redis.srem(lookup_key_name(user.id), session_ids)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(key_names) redis.del(key_names)
...@@ -98,19 +113,29 @@ class ActiveSession ...@@ -98,19 +113,29 @@ class ActiveSession
end end
end end
def self.cleanup(user) # 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| Gitlab::Redis::SharedState.with do |redis|
clean_up_old_sessions(redis, user) destroy_sessions(redis, user, [session_id, decrypted_session_id, rack_session_private_id].compact)
cleaned_up_lookup_entries(redis, user)
end end
end end
def self.destroy_all_but_current(user, current_session) def self.destroy_all_but_current(user, current_rack_session)
session_ids = not_impersonated(user) sessions = not_impersonated(user)
session_ids.reject! { |session| session.current?(current_session) } if current_session sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session
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? 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 ...@@ -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 ...@@ -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
end
# Older ActiveSession models serialize `session_id` as strings, To def self.rack_session_keys(rack_session_ids)
# avoid breaking older sessions, we keep backwards compatibility rack_session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
# 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
def self.rack_session_keys(session_ids)
session_ids.each_with_object([]) do |session_id, arr|
# 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 ...@@ -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 ...@@ -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
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Clusters module Clusters
module Applications module Applications
class Runner < ApplicationRecord class Runner < ApplicationRecord
VERSION = '0.20.1' VERSION = '0.20.2'
self.table_name = 'clusters_applications_runners' self.table_name = 'clusters_applications_runners'
......
...@@ -5,6 +5,7 @@ class Member < ApplicationRecord ...@@ -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 ...@@ -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],
......
...@@ -4,8 +4,11 @@ module Operations ...@@ -4,8 +4,11 @@ module Operations
class FeatureFlag < ApplicationRecord class FeatureFlag < ApplicationRecord
include AtomicInternalId include AtomicInternalId
include IidRoutes include IidRoutes
include Limitable
self.table_name = 'operations_feature_flags' self.table_name = 'operations_feature_flags'
self.limit_scope = :project
self.limit_name = 'project_feature_flags'
belongs_to :project belongs_to :project
......
...@@ -6,7 +6,9 @@ module Releases ...@@ -6,7 +6,9 @@ module Releases
belongs_to :release belongs_to :release
FILEPATH_REGEX = %r{\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 }
......
...@@ -19,6 +19,7 @@ class IssuableBaseService < BaseService ...@@ -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)
......
...@@ -3,6 +3,7 @@ ...@@ -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 ...@@ -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
......
...@@ -7,6 +7,11 @@ module Members ...@@ -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)
......
...@@ -52,7 +52,14 @@ module Todos ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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)
......
# 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
...@@ -27,6 +27,9 @@ ...@@ -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')
...@@ -315,6 +315,14 @@ ...@@ -315,6 +315,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:
......
# 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
...@@ -445,6 +445,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire ...@@ -445,6 +445,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'
......
...@@ -40,7 +40,8 @@ Rails.application.configure do |config| ...@@ -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!
## ##
......
# 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
# 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
# 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
06e87d83d5520e6ffbfb839d15a6dd02ad2caf5737136441d496e29e03a07e64
\ No newline at end of file
00af22b19af29b453f0022ded835bd9246c602c63a04a51ef93cbedd47047753
\ No newline at end of file
ff246eb2761c4504b67b7d7b197990a671626038e50f1b82d6b3e4739a1ec3d4
\ No newline at end of file
...@@ -14362,7 +14362,8 @@ CREATE TABLE plan_limits ( ...@@ -14362,7 +14362,8 @@ CREATE TABLE plan_limits (
npm_max_file_size bigint DEFAULT 524288000 NOT NULL, npm_max_file_size bigint DEFAULT 524288000 NOT NULL,
nuget_max_file_size bigint DEFAULT 524288000 NOT NULL, nuget_max_file_size bigint DEFAULT 524288000 NOT NULL,
pypi_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL, pypi_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL,
generic_packages_max_file_size bigint DEFAULT '5368709120'::bigint NOT NULL generic_packages_max_file_size bigint DEFAULT '5368709120'::bigint NOT NULL,
project_feature_flags integer DEFAULT 200 NOT NULL
); );
CREATE SEQUENCE plan_limits_id_seq CREATE SEQUENCE plan_limits_id_seq
...@@ -19312,6 +19313,8 @@ CREATE INDEX idx_jira_connect_subscriptions_on_installation_id ON jira_connect_s ...@@ -19312,6 +19313,8 @@ CREATE INDEX idx_jira_connect_subscriptions_on_installation_id ON jira_connect_s
CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id); CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id);
CREATE INDEX idx_members_created_at_user_id_invite_token ON 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 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 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 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 merge_requests USING btree (source_project_id, source_branch) WHERE (state_id = 1);
......
...@@ -15,7 +15,8 @@ templates are sourced. ...@@ -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
......
...@@ -61,9 +61,8 @@ GitLab administrators can ...@@ -61,9 +61,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.
......
...@@ -93,6 +93,9 @@ invitation, change their access level, or even delete them. ...@@ -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 :
......
...@@ -54,8 +54,10 @@ module API ...@@ -54,8 +54,10 @@ module API
user = find_user_from_sources user = find_user_from_sources
return unless user return unless user
# Sessions are enforced to be unavailable for API calls, so ignore them for admin mode if user.is_a?(User) && Feature.enabled?(:user_mode_in_session)
Gitlab::Auth::CurrentUserMode.bypass_session!(user.id) if Feature.enabled?(:user_mode_in_session) # Sessions are enforced to be unavailable for API calls, so ignore them for admin mode
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))
......
...@@ -31,7 +31,9 @@ module Gitlab ...@@ -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
......
...@@ -2132,6 +2132,9 @@ msgstr "" ...@@ -2132,6 +2132,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 ""
...@@ -19391,6 +19394,9 @@ msgstr "" ...@@ -19391,6 +19394,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 ""
...@@ -25734,6 +25740,9 @@ msgstr "" ...@@ -25734,6 +25740,9 @@ msgstr ""
msgid "This action cannot be undone. You will lose the project's repository and all content: issues, merge requests, etc." msgid "This action cannot be undone. You will lose the project's repository and all content: issues, merge requests, etc."
msgstr "" msgstr ""
msgid "This action has been performed too many times. Try again later."
msgstr ""
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc." msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc."
msgstr "" msgstr ""
...@@ -29646,6 +29655,9 @@ msgstr "" ...@@ -29646,6 +29655,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 ""
......
...@@ -36,7 +36,7 @@ RSpec.describe Admin::UsersController do ...@@ -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 ...@@ -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
......
...@@ -139,6 +139,45 @@ RSpec.describe Groups::GroupMembersController do ...@@ -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,15 +188,49 @@ RSpec.describe Groups::GroupMembersController do ...@@ -149,15 +188,49 @@ RSpec.describe Groups::GroupMembersController do
sign_in(user) sign_in(user)
end end
Gitlab::Access.options.each do |label, value| context 'access level' do
it "can change the access level to #{label}" do Gitlab::Access.options.each do |label, value|
put :update, params: { it "can change the access level to #{label}" do
group_member: { access_level: value }, put :update, params: {
group_id: group, group_member: { access_level: value },
id: requester group_id: group,
}, xhr: true id: requester
}, xhr: true
expect(requester.reload.human_access).to eq(label) expect(requester.reload.human_access).to eq(label)
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 end
end end
......
...@@ -15,6 +15,29 @@ RSpec.describe Profiles::EmailsController do ...@@ -15,6 +15,29 @@ 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
let(:email) { 'add_email@example.com' } let(:email) { 'add_email@example.com' }
let(:params) { { email: { email: email } } } let(:params) { { email: { email: email } } }
...@@ -32,6 +55,10 @@ RSpec.describe Profiles::EmailsController do ...@@ -32,6 +55,10 @@ RSpec.describe Profiles::EmailsController do
expect { subject }.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
...@@ -54,5 +81,9 @@ RSpec.describe Profiles::EmailsController do ...@@ -54,5 +81,9 @@ RSpec.describe Profiles::EmailsController do
expect { subject }.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_resend_email_confirmation }
end
end end
end end
...@@ -129,6 +129,46 @@ RSpec.describe Projects::ProjectMembersController do ...@@ -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,16 +179,53 @@ RSpec.describe Projects::ProjectMembersController do ...@@ -139,16 +179,53 @@ RSpec.describe Projects::ProjectMembersController do
sign_in(user) sign_in(user)
end end
Gitlab::Access.options.each do |label, value| context 'access level' do
it "can change the access level to #{label}" do Gitlab::Access.options.each do |label, value|
put :update, params: { it "can change the access level to #{label}" do
project_member: { access_level: value }, params = {
namespace_id: project.namespace, project_member: { access_level: value },
project_id: project, namespace_id: project.namespace,
id: requester project_id: project,
}, xhr: true id: requester
}
put :update, params: params, xhr: true
expect(requester.reload.human_access).to eq(label)
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
expect(requester.reload.human_access).to eq(label) 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 end
end end
......
...@@ -33,6 +33,11 @@ RSpec.describe Projects::RawController do ...@@ -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
......
...@@ -459,6 +459,24 @@ RSpec.describe RegistrationsController do ...@@ -459,6 +459,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 '#welcome' do describe '#welcome' do
......
...@@ -29,6 +29,7 @@ FactoryBot.define do ...@@ -29,6 +29,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
...@@ -53,7 +54,8 @@ FactoryBot.define do ...@@ -53,7 +54,8 @@ FactoryBot.define do
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 pages_access_level: evaluator.pages_access_level,
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level
} }
project.build_project_feature(hash) project.build_project_feature(hash)
...@@ -309,6 +311,9 @@ FactoryBot.define do ...@@ -309,6 +311,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
......
...@@ -3,7 +3,8 @@ ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
...@@ -3,31 +3,29 @@ ...@@ -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 ...@@ -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
......
# 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
This diff is collapsed.
...@@ -4,9 +4,13 @@ require 'spec_helper' ...@@ -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]) }
......
...@@ -17,6 +17,13 @@ RSpec.describe Member do ...@@ -17,6 +17,13 @@ RSpec.describe Member do
it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:source) }
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
......
...@@ -44,8 +44,9 @@ RSpec.describe ProjectMember do ...@@ -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
......
...@@ -7,6 +7,10 @@ RSpec.describe Operations::FeatureFlag do ...@@ -7,6 +7,10 @@ RSpec.describe Operations::FeatureFlag do
subject { create(:operations_feature_flag) } subject { create(:operations_feature_flag) }
it_behaves_like 'includes Limitable concern' do
subject { build(:operations_feature_flag, project: create(:project)) }
end
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:scopes) } it { is_expected.to have_many(:scopes) }
......
...@@ -67,4 +67,29 @@ RSpec.describe API::API do ...@@ -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
...@@ -532,16 +532,13 @@ RSpec.describe API::Files do ...@@ -532,16 +532,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
url = route('.gitignore') + "/raw" before do
expect(Gitlab::Workhorse).to receive(:send_git_blob) url = route('.gitignore') + "/raw"
expect(Gitlab::Workhorse).to receive(:send_git_blob)
get api(url, current_user), params: params
expect(response.headers["Cache-Control"]).to include("no-store") get api(url, current_user), params: params
expect(response.headers["Cache-Control"]).to include("no-cache") end
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
......
...@@ -9,13 +9,9 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do ...@@ -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 ...@@ -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
......
...@@ -244,13 +244,12 @@ RSpec.describe API::Members do ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -39,7 +39,9 @@ RSpec.describe 'Projects::MetricsDashboardController' do ...@@ -39,7 +39,9 @@ RSpec.describe 'Projects::MetricsDashboardController' do
context 'with anonymous user and public dashboard visibility' do context 'with anonymous user and public dashboard visibility' do
let(:anonymous_user) { create(:user) } let(:anonymous_user) { create(:user) }
let(:project) { create(:project, :public) } let(:project) do
create(:project, :public, :metrics_dashboard_enabled)
end
before do before do
project.update!(metrics_dashboard_access_level: 'enabled') project.update!(metrics_dashboard_access_level: 'enabled')
......
...@@ -25,6 +25,7 @@ RSpec.describe Issues::CreateService do ...@@ -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
...@@ -102,6 +103,12 @@ RSpec.describe Issues::CreateService do ...@@ -102,6 +103,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
......
...@@ -10,6 +10,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -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',
...@@ -53,7 +54,8 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -53,7 +54,8 @@ RSpec.describe Issues::UpdateService, :mailer do
label_ids: [label.id], label_ids: [label.id],
due_date: Date.tomorrow, due_date: Date.tomorrow,
discussion_locked: true, discussion_locked: true,
severity: 'low' severity: 'low',
milestone_id: milestone.id
} }
end end
...@@ -70,6 +72,14 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -70,6 +72,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
context 'when issue type is not incident' do context 'when issue type is not incident' do
...@@ -128,6 +138,8 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -128,6 +138,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
...@@ -137,6 +149,8 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -137,6 +149,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
context 'issue in incident type' do context 'issue in incident type' do
...@@ -297,7 +311,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -297,7 +311,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'
...@@ -307,6 +321,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -307,6 +321,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
......
...@@ -31,17 +31,35 @@ RSpec.describe Members::UpdateService do ...@@ -31,17 +31,35 @@ 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
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)
expect(updated_member).to be_valid
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
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 end
it 'schedules to delete confidential todos' do context 'with Gitlab::Access::GUEST level as an integer' do
expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once let(:params) { { access_level: Gitlab::Access::GUEST } }
it_behaves_like 'schedules to delete confidential todos'
end
end
updated_member = described_class.new(current_user, params).execute(member, permission: permission) context 'when access_level is invalid' do
let(:params) { { access_level: 'invalid' } }
expect(updated_member).to be_valid it 'raises an error' do
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST) expect { described_class.new(current_user, params) }.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
end end
end end
end end
......
...@@ -6,12 +6,13 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -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',
...@@ -61,7 +62,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -61,7 +62,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)
...@@ -85,6 +87,26 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -85,6 +87,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(
...@@ -152,6 +174,46 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -152,6 +174,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)
......
# 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
# 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
...@@ -7,9 +7,13 @@ RSpec.describe RemoveExpiredMembersWorker do ...@@ -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 ...@@ -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 ...@@ -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
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment