Commit 516fba52 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/security/gitlab@13-4-stable-ee

parent c90be62b
...@@ -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
......
...@@ -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|
......
...@@ -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
...@@ -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
......
...@@ -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
......
# 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
---
title: Do not store session id in Redis
merge_request:
author:
type: security
---
title: Fix permission checks when updating confidentiality and milestone on issues
or merge requests
merge_request:
author:
type: security
---
title: Purge unaccepted member invitations older than 90 days
merge_request:
author:
type: security
---
title: Adds feature flags plan limits
merge_request:
author:
type: security
---
title: Do not bypass admin mode when authenticated with deploy token
merge_request:
author:
type: security
---
title: Fixes release asset link filepath ReDoS
merge_request:
author:
type: security
---
title: Validate that membership expiry dates are not in the past
merge_request:
author:
type: security
...@@ -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);
......
...@@ -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))
......
...@@ -29646,6 +29646,9 @@ msgstr "" ...@@ -29646,6 +29646,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 ""
......
...@@ -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
......
...@@ -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
......
...@@ -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
...@@ -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
......
...@@ -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
......
...@@ -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
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
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
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