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 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
### Fixed (2 changes)
......
......@@ -15,7 +15,7 @@ module ApplicationCable
private
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)
end
......
......@@ -5,6 +5,7 @@ class Admin::UsersController < Admin::ApplicationController
before_action :user, except: [:index, :new, :create]
before_action :check_impersonation_availability, only: :impersonate
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
def index
@users = User.filter_items(params[:filter]).order_name_asc
......@@ -173,7 +174,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete))
user.delete_async(deleted_by: current_user, params: destroy_params)
respond_to do |format|
format.html { redirect_to admin_users_path, status: :found, notice: _("The user is being deleted.") }
......@@ -202,6 +203,24 @@ class Admin::UsersController < Admin::ApplicationController
user != current_user
end
def destroy_params
params.permit(:hard_delete)
end
def ensure_destroy_prerequisites_met
return if hard_delete?
if user.solo_owned_groups.present?
message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
redirect_to admin_user_path(user), status: :see_other, alert: message
end
end
def hard_delete?
destroy_params[:hard_delete]
end
def user
@user ||= find_routable!(User, params[:id])
end
......
......@@ -6,7 +6,9 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
end
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!
respond_to do |format|
......
......@@ -2,6 +2,8 @@
class Profiles::EmailsController < Profiles::ApplicationController
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
@primary_email = current_user.email
......@@ -38,6 +40,16 @@ class Profiles::EmailsController < Profiles::ApplicationController
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
params.require(:email).permit(:email)
end
......
......@@ -12,6 +12,7 @@ class Projects::RawController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :show_rate_limit, only: [:show], unless: :external_storage_request?
before_action :assign_ref_vars
before_action :no_cache_headers, only: [:show]
before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled?
def show
......
......@@ -10,7 +10,7 @@ class RegistrationsController < Devise::RegistrationsController
skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration]
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
before_action :whitelist_query_limiting, :ensure_destroy_prerequisites_met, only: [:destroy]
before_action :ensure_terms_accepted,
if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? }
before_action :load_recaptcha, only: :new
......@@ -124,6 +124,14 @@ class RegistrationsController < Devise::RegistrationsController
private
def ensure_destroy_prerequisites_met
if current_user.solo_owned_groups.present?
redirect_to profile_account_path,
status: :see_other,
alert: s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account')
end
end
def user_created_message(confirmed: false)
"User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}"
end
......
......@@ -9,7 +9,7 @@ module Mutations
# This method is defined here in order to be used by `authorized_find!` in the subclasses.
def find_object(id:)
GitlabSchema.object_from_id(id)
GitlabSchema.object_from_id(id, expected_type: ::Metrics::Dashboard::Annotation)
end
end
end
......
......@@ -22,4 +22,13 @@ module ActiveSessionsHelper
sprite_icon(icon_name, css_class: 'gl-mt-2')
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
......@@ -5,7 +5,7 @@ module SafeParamsHelper
# Use this helper when generating links with `params.merge(...)`
def safe_params
if params.respond_to?(:permit!)
params.except(:host, :port, :protocol).permit!
params.except(*ActionDispatch::Routing::RouteSet::RESERVED_OPTIONS).permit!
else
params
end
......
......@@ -9,14 +9,14 @@ class ActiveSession
attr_accessor :created_at, :updated_at,
:ip_address, :browser, :os,
:device_name, :device_type,
:is_impersonated, :session_id
:is_impersonated, :session_id, :session_private_id
def current?(session)
return false if session_id.nil? || session.id.nil?
def current?(rack_session)
return false if session_private_id.nil? || rack_session.id.nil?
# Rack v2.0.8+ added private_id, which uses the hash of the
# public_id to avoid timing attacks.
session_id.private_id == session.id.private_id
session_private_id == rack_session.id.private_id
end
def human_device_type
......@@ -25,13 +25,14 @@ class ActiveSession
# This is not the same as Rack::Session::SessionId#public_id, but we
# need to preserve this for backwards compatibility.
# TODO: remove in 13.7
def public_id
Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id.public_id)
Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id)
end
def self.set(user, request)
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)
timestamp = Time.current
......@@ -43,25 +44,37 @@ class ActiveSession
device_type: client.device_type,
created_at: user.current_sign_in_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?
)
redis.pipelined do
redis.setex(
key_name(user.id, session_id),
key_name(user.id, session_private_id),
Settings.gitlab['session_expire_delay'] * 60,
Marshal.dump(active_user_session)
)
redis.sadd(
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
# 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)
Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user).map do |raw_session|
......@@ -70,27 +83,29 @@ class ActiveSession
end
end
def self.destroy(user, session_id)
return unless session_id
def self.cleanup(user)
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
def self.destroy_with_public_id(user, public_id)
decrypted_id = decrypt_public_id(public_id)
# TODO: remove in 13.7
# 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?
session_id = Rack::Session::SessionId.new(decrypted_id)
destroy(user, session_id)
Gitlab::Redis::SharedState.with do |redis|
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.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
redis.del(key_names)
......@@ -98,19 +113,29 @@ class ActiveSession
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|
clean_up_old_sessions(redis, user)
cleaned_up_lookup_entries(redis, user)
destroy_sessions(redis, user, [session_id, decrypted_session_id, rack_session_private_id].compact)
end
end
def self.destroy_all_but_current(user, current_session)
session_ids = not_impersonated(user)
session_ids.reject! { |session| session.current?(current_session) } if current_session
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|
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
......@@ -132,17 +157,16 @@ class ActiveSession
# 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)
Gitlab::Redis::SharedState.with do |redis|
session_ids = redis.smembers(lookup_key_name(user_id))
session_ids.map { |id| Rack::Session::SessionId.new(id) }
redis.smembers(lookup_key_name(user_id))
end
end
# 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
def self.sessions_from_ids(session_ids)
......@@ -168,27 +192,12 @@ class ActiveSession
# Returns an ActiveSession object
def self.load_raw_session(raw_session)
# rubocop:disable Security/MarshalLoad
session = Marshal.load(raw_session)
Marshal.load(raw_session)
# rubocop:enable Security/MarshalLoad
end
# 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
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
def self.rack_session_keys(rack_session_ids)
rack_session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
end
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.sort_by! {|session| session.updated_at }.reverse!
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?
end
......@@ -244,6 +253,7 @@ class ActiveSession
entries.compact
end
# TODO: remove in 13.7
private_class_method def self.decrypt_public_id(public_id)
Gitlab::CryptoHelper.aes256_gcm_decrypt(public_id)
rescue
......
......@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
VERSION = '0.20.1'
VERSION = '0.20.2'
self.table_name = 'clusters_applications_runners'
......
......@@ -5,6 +5,7 @@ class Member < ApplicationRecord
include AfterCommitQueue
include Sortable
include Importable
include CreatedAtFilterable
include Expirable
include Gitlab::Access
include Presentable
......@@ -20,6 +21,7 @@ class Member < ApplicationRecord
delegate :name, :username, :email, to: :user, prefix: true
validates :expires_at, allow_blank: true, future_date: true
validates :user, presence: true, unless: :invite?
validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
......
......@@ -4,8 +4,11 @@ module Operations
class FeatureFlag < ApplicationRecord
include AtomicInternalId
include IidRoutes
include Limitable
self.table_name = 'operations_feature_flags'
self.limit_scope = :project
self.limit_name = 'project_feature_flags'
belongs_to :project
......
......@@ -6,7 +6,9 @@ module Releases
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 :name, presence: true, uniqueness: { scope: :release }
......
......@@ -19,6 +19,7 @@ class IssuableBaseService < BaseService
def filter_params(issuable)
unless can_admin_issuable?(issuable)
params.delete(:milestone)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
......
......@@ -3,6 +3,7 @@
module Issues
class UpdateService < Issues::BaseService
include SpamCheckMethods
extend ::Gitlab::Utils::Override
def execute(issue)
handle_move_between_ids(issue)
......@@ -17,6 +18,17 @@ module Issues
super
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)
spam_check(issue, current_user, action: :update) unless skip_spam_check
end
......
......@@ -7,6 +7,11 @@ module Members
def initialize(current_user = nil, params = {})
@current_user = current_user
@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
def after_execute(args)
......
......@@ -52,7 +52,14 @@ module Todos
# rubocop: disable CodeReuse/ActiveRecord
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
# rubocop: enable CodeReuse/ActiveRecord
......@@ -68,7 +75,7 @@ module Todos
when Project
{ id: entity.id }
when Namespace
{ namespace_id: non_member_groups }
{ namespace_id: non_authorized_reporter_groups }
end
Project.where(condition)
......@@ -76,8 +83,32 @@ module Todos
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def non_authorized_projects
projects.where('id NOT IN (?)', user.authorized_projects.select(:id))
def authorized_reporter_projects
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
# rubocop: enable CodeReuse/ActiveRecord
......@@ -91,9 +122,9 @@ module Todos
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def non_member_groups
def non_authorized_reporter_groups
entity.self_and_descendants.select(:id)
.where('id NOT IN (?)', user.membership_groups.select(:id))
.where('id NOT IN (?)', authorized_reporter_groups)
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -106,8 +137,6 @@ module Todos
# rubocop: disable CodeReuse/ActiveRecord
def confidential_issues
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)
.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 @@
- unless is_current_session
.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')
= _('Revoke')
......@@ -315,6 +315,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:remove_unaccepted_member_invites
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:remove_unreferenced_lfs_objects
:feature_category: :git_lfs
:has_external_dependencies:
......
# 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
Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
Settings.cron_jobs['remove_unaccepted_member_invites_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_unaccepted_member_invites_worker']['cron'] ||= '10 15 * * *'
Settings.cron_jobs['remove_unaccepted_member_invites_worker']['job_class'] = 'RemoveUnacceptedMemberInvitesWorker'
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
......
......@@ -40,7 +40,8 @@ Rails.application.configure do |config|
activity = Gitlab::Auth::Activity.new(opts)
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!
##
......
# 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 (
npm_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,
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
......@@ -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 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_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.
Every project directly under the group namespace will be
available to the user if they have access to them. For example:
- Public project in the group will be available to every logged in user.
- Public projects, in the group will be available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions)
are set to **Everyone With Access**.
- Private projects will be available only if the user is a member of the project.
Repository and database information that are copied over to each new project are
......
......@@ -61,9 +61,8 @@ GitLab administrators can
[set project templates for an entire GitLab instance](../admin_area/custom_project_templates.md).
Within this section, you can configure the group where all the custom project
templates are sourced. Every project directly under the group namespace will be
available to the user if they have access to them. For example, every public
project in the group will be available to every logged in user.
templates are sourced. Every project _template_ directly under the group namespace is
available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions) are set to **Everyone With Access**.
However, private projects will be available only if the user is a member of the project.
......
......@@ -93,6 +93,9 @@ invitation, change their access level, or even delete them.
Once the user accepts the invitation, they will be prompted to create a new
GitLab account using the same e-mail address the invitation was sent to.
Note: **Note:**
Unaccepted invites are automatically deleted after 90 days.
## Project membership and requesting access
Project owners can :
......
......@@ -54,8 +54,10 @@ module API
user = find_user_from_sources
return unless user
# 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)
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
Gitlab::Auth::CurrentUserMode.bypass_session!(user.id)
end
unless api_access_allowed?(user)
forbidden!(api_access_denied_message(user))
......
......@@ -31,7 +31,9 @@ module Gitlab
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_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
end
......
......@@ -2132,6 +2132,9 @@ msgstr ""
msgid "AdminUsers|You cannot remove your own admin rights."
msgstr ""
msgid "AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account"
msgstr ""
msgid "Administration"
msgstr ""
......@@ -19391,6 +19394,9 @@ msgstr ""
msgid "Profiles|You don't have access to delete this user."
msgstr ""
msgid "Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account"
msgstr ""
msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
msgstr ""
......@@ -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."
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."
msgstr ""
......@@ -29646,6 +29655,9 @@ msgstr ""
msgid "by %{user}"
msgstr ""
msgid "cannot be a date in the past"
msgstr ""
msgid "cannot be changed if a personal project has container registry tags."
msgstr ""
......
......@@ -36,7 +36,7 @@ RSpec.describe Admin::UsersController do
end
end
describe 'DELETE #user with projects', :sidekiq_might_not_need_inline do
describe 'DELETE #destroy', :sidekiq_might_not_need_inline do
let(:project) { create(:project, namespace: user.namespace) }
let!(:issue) { create(:issue, author: user) }
......@@ -59,6 +59,41 @@ RSpec.describe Admin::UsersController do
expect(User.exists?(user.id)).to be_falsy
expect(Issue.exists?(issue.id)).to be_falsy
end
context 'prerequisites for account deletion' do
context 'solo-owned groups' do
let(:group) { create(:group) }
context 'if the user is the sole owner of at least one group' do
before do
create(:group_member, :owner, group: group, user: user)
end
context 'soft-delete' do
it 'fails' do
delete :destroy, params: { id: user.username }
message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
expect(flash[:alert]).to eq(message)
expect(response).to have_gitlab_http_status(:see_other)
expect(response).to redirect_to admin_user_path(user)
expect(User.exists?(user.id)).to be_truthy
end
end
context 'hard-delete' do
it 'succeeds' do
delete :destroy, params: { id: user.username, hard_delete: true }
expect(response).to redirect_to(admin_users_path)
expect(flash[:notice]).to eq(_('The user is being deleted.'))
expect(User.exists?(user.id)).to be_falsy
end
end
end
end
end
end
describe 'PUT #activate' do
......
......@@ -139,6 +139,45 @@ RSpec.describe Groups::GroupMembersController do
expect(group.users).not_to include group_user
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
describe 'PUT update' do
......@@ -149,15 +188,49 @@ RSpec.describe Groups::GroupMembersController do
sign_in(user)
end
Gitlab::Access.options.each do |label, value|
it "can change the access level to #{label}" do
put :update, params: {
group_member: { access_level: value },
group_id: group,
id: requester
}, xhr: true
context 'access level' do
Gitlab::Access.options.each do |label, value|
it "can change the access level to #{label}" do
put :update, params: {
group_member: { access_level: value },
group_id: group,
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
......
......@@ -15,6 +15,29 @@ RSpec.describe Profiles::EmailsController do
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
let(:email) { 'add_email@example.com' }
let(:params) { { email: { email: email } } }
......@@ -32,6 +55,10 @@ RSpec.describe Profiles::EmailsController do
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
end
end
it_behaves_like 'respects the rate limit' do
let(:action) { :profile_add_new_email }
end
end
describe '#resend_confirmation_instructions' do
......@@ -54,5 +81,9 @@ RSpec.describe Profiles::EmailsController 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
......@@ -129,6 +129,46 @@ RSpec.describe Projects::ProjectMembersController do
expect(response).to redirect_to(project_project_members_path(project))
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
describe 'PUT update' do
......@@ -139,16 +179,53 @@ RSpec.describe Projects::ProjectMembersController do
sign_in(user)
end
Gitlab::Access.options.each do |label, value|
it "can change the access level to #{label}" do
put :update, params: {
project_member: { access_level: value },
namespace_id: project.namespace,
project_id: project,
id: requester
}, xhr: true
context 'access level' do
Gitlab::Access.options.each do |label, value|
it "can change the access level to #{label}" do
params = {
project_member: { access_level: value },
namespace_id: project.namespace,
project_id: project,
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
......
......@@ -33,6 +33,11 @@ RSpec.describe Projects::RawController do
it_behaves_like 'project cache control headers'
it_behaves_like 'content disposition headers'
it_behaves_like 'uncached response' do
before do
subject
end
end
end
context 'image header' do
......
......@@ -459,6 +459,24 @@ RSpec.describe RegistrationsController do
expect_success
end
end
context 'prerequisites for account deletion' do
context 'solo-owned groups' do
let(:group) { create(:group) }
context 'if the user is the sole owner of at least one group' do
before do
create(:group_member, :owner, group: group, user: user)
end
it 'fails' do
delete :destroy, params: { password: '12345678' }
expect_failure(s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account'))
end
end
end
end
end
describe '#welcome' do
......
......@@ -29,6 +29,7 @@ FactoryBot.define do
pages_access_level do
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
end
metrics_dashboard_access_level { ProjectFeature::PRIVATE }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
......@@ -53,7 +54,8 @@ FactoryBot.define do
forking_access_level: evaluator.forking_access_level,
merge_requests_access_level: merge_requests_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)
......@@ -309,6 +311,9 @@ FactoryBot.define do
trait(:pages_enabled) { pages_access_level { ProjectFeature::ENABLED } }
trait(:pages_disabled) { pages_access_level { ProjectFeature::DISABLED } }
trait(:pages_private) { pages_access_level { ProjectFeature::PRIVATE } }
trait(:metrics_dashboard_enabled) { metrics_dashboard_access_level { ProjectFeature::ENABLED } }
trait(:metrics_dashboard_disabled) { metrics_dashboard_access_level { ProjectFeature::DISABLED } }
trait(:metrics_dashboard_private) { metrics_dashboard_access_level { ProjectFeature::PRIVATE } }
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
......
......@@ -3,7 +3,8 @@
require 'spec_helper'
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) }
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(: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
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
before do
issue.project.add_developer(user)
project.add_developer(user)
end
it 'returns the issue as confidential' do
......@@ -39,5 +40,19 @@ RSpec.describe Mutations::Issues::SetConfidential do
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
......@@ -3,31 +3,29 @@
require 'spec_helper'
RSpec.describe Mutations::MergeRequests::SetMilestone do
let(:merge_request) { create(:merge_request) }
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) }
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
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
project.add_developer(user)
end
it 'returns the merge request with the milestone' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.milestone).to eq(milestone)
expect(subject[:merge_request]).to eq(merge_request)
expect(subject[:merge_request].milestone).to eq(milestone)
expect(subject[:errors]).to be_empty
end
......@@ -43,13 +41,37 @@ RSpec.describe Mutations::MergeRequests::SetMilestone do
let(:milestone) { nil }
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
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
......
# 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'
RSpec.describe Expirable do
describe 'ProjectMember' do
let(:no_expire) { create(:project_member) }
let(:expire_later) { create(:project_member, expires_at: Time.current + 6.days) }
let(:expired) { create(:project_member, expires_at: Time.current - 6.days) }
let_it_be(:no_expire) { create(:project_member) }
let_it_be(:expire_later) { create(:project_member, expires_at: 8.days.from_now) }
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
it { expect(ProjectMember.expired).to match_array([expired]) }
......
......@@ -17,6 +17,13 @@ RSpec.describe Member do
it { is_expected.to validate_presence_of(:user) }
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
subject { build(:project_member) }
end
......
......@@ -44,8 +44,9 @@ RSpec.describe ProjectMember do
let(:maintainer) { create(:project_member, project: project) }
it "creates an expired event when left due to expiry" do
expired = create(:project_member, project: project, expires_at: Time.current - 6.days)
expired.destroy
expired = create(:project_member, project: project, expires_at: 1.day.from_now)
travel_to(2.days.from_now) { expired.destroy }
expect(Event.recent.first).to be_expired_action
end
......
......@@ -7,6 +7,10 @@ RSpec.describe Operations::FeatureFlag do
subject { create(:operations_feature_flag) }
it_behaves_like 'includes Limitable concern' do
subject { build(:operations_feature_flag, project: create(:project)) }
end
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:scopes) }
......
......@@ -67,4 +67,29 @@ RSpec.describe API::API do
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
......@@ -532,16 +532,13 @@ RSpec.describe API::Files do
expect(response).to have_gitlab_http_status(:ok)
end
it 'sets no-cache headers' do
url = route('.gitignore') + "/raw"
expect(Gitlab::Workhorse).to receive(:send_git_blob)
get api(url, current_user), params: params
it_behaves_like 'uncached response' do
before do
url = route('.gitignore') + "/raw"
expect(Gitlab::Workhorse).to receive(:send_git_blob)
expect(response.headers["Cache-Control"]).to include("no-store")
expect(response.headers["Cache-Control"]).to include("no-cache")
expect(response.headers["Pragma"]).to eq("no-cache")
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
get api(url, current_user), params: params
end
end
context 'when mandatory params are not given' do
......
......@@ -9,13 +9,9 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
let_it_be(:project) { create(:project, :private, :repository) }
let_it_be(:environment) { create(:environment, project: project) }
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)
end
let(:variables) { { id: GitlabSchema.id_from_object(annotation).to_s } }
let(:mutation) { graphql_mutation(:delete_annotation, variables) }
def mutation_response
graphql_mutation_response(:delete_annotation)
......@@ -37,15 +33,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
end
context 'with invalid params' do
let(:mutation) do
variables = {
id: 'invalid_id'
}
let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } }
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
it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.']
end
context 'when the delete fails' do
......
......@@ -244,13 +244,12 @@ RSpec.describe API::Members do
it 'creates a new member' do
expect do
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)
end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
expect(json_response['expires_at']).to eq('2016-08-05')
end
end
......@@ -285,6 +284,40 @@ RSpec.describe API::Members do
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
post api("/#{source_type.pluralize}/#{source.id}/members", 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
it 'updates the member' do
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(json_response['id']).to eq(developer.id)
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
......
......@@ -39,7 +39,9 @@ RSpec.describe 'Projects::MetricsDashboardController' do
context 'with anonymous user and public dashboard visibility' do
let(:anonymous_user) { create(:user) }
let(:project) { create(:project, :public) }
let(:project) do
create(:project, :public, :metrics_dashboard_enabled)
end
before do
project.update!(metrics_dashboard_access_level: 'enabled')
......
......@@ -25,6 +25,7 @@ RSpec.describe Issues::CreateService do
assignee_ids: [assignee.id],
label_ids: labels.map(&:id),
milestone_id: milestone.id,
milestone: milestone,
due_date: Date.tomorrow }
end
......@@ -102,6 +103,12 @@ RSpec.describe Issues::CreateService do
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
end
it 'creates confidential issues' do
issue = described_class.new(project, guest, confidential: true).execute
expect(issue.confidential).to be_truthy
end
end
it 'creates a pending todo for new assignee' 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(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:issue) do
create(:issue, title: 'Old title',
......@@ -53,7 +54,8 @@ RSpec.describe Issues::UpdateService, :mailer do
label_ids: [label.id],
due_date: Date.tomorrow,
discussion_locked: true,
severity: 'low'
severity: 'low',
milestone_id: milestone.id
}
end
......@@ -70,6 +72,14 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.labels).to match_array [label]
expect(issue.due_date).to eq Date.tomorrow
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
context 'when issue type is not incident' 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)
update_issue(confidential: true)
expect(issue.confidential).to be_truthy
end
it 'does not enqueue ConfidentialIssueWorker when an issue is made non confidential' do
......@@ -137,6 +149,8 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(TodosDestroyer::ConfidentialIssueWorker).not_to receive(:perform_in)
update_issue(confidential: false)
expect(issue.confidential).to be_falsey
end
context 'issue in incident type' do
......@@ -297,7 +311,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
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.title).to eq 'New title'
......@@ -307,6 +321,7 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
expect(issue.discussion_locked).to be_falsey
expect(issue.confidential).to be_falsey
end
end
......
......@@ -31,17 +31,35 @@ RSpec.describe Members::UpdateService do
end
context 'when member is downgraded to guest' do
let(:params) do
{ access_level: Gitlab::Access::GUEST }
shared_examples 'schedules to delete confidential todos' do
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
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
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
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
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
it 'raises an error' do
expect { described_class.new(current_user, params) }.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
end
end
end
......
......@@ -6,12 +6,13 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
include ProjectForksHelper
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(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:label) { create(:label, project: project) }
let(:label2) { create(:label) }
let(:milestone) { create(:milestone, project: project) }
let(:merge_request) do
create(:merge_request, :simple, title: 'Old title',
......@@ -61,7 +62,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
}
end
let(:service) { described_class.new(project, user, opts) }
let(:service) { described_class.new(project, current_user, opts) }
let(:current_user) { user }
before do
allow(service).to receive(:execute_hooks)
......@@ -85,6 +87,26 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
expect(@merge_request.discussion_locked).to be_truthy
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
expect(service).to have_received(:execute_hooks)
.with(
......@@ -152,6 +174,46 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
expect(note.note).to eq 'locked this merge request'
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
before do
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
describe '#perform' do
context 'project members' do
let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
let!(: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(:expired_project_member) { create(:project_member, expires_at: 1.day.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_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
expect { worker.perform }.to change { Member.count }.by(-1)
......@@ -28,9 +32,13 @@ RSpec.describe RemoveExpiredMembersWorker do
end
context 'group members' do
let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
let!(: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(:expired_group_member) { create(:group_member, expires_at: 1.day.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_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
expect { worker.perform }.to change { Member.count }.by(-1)
......@@ -49,7 +57,11 @@ RSpec.describe RemoveExpiredMembersWorker do
end
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
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