Commit d39ef1a1 authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'merge-dev-to-master' into 'master'

Merge EE dev.gitlab.org master into GitLab.com EE master

See merge request gitlab-org/gitlab-ee!9836
parents 6ac0f752 6dca014d
...@@ -227,6 +227,17 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -227,6 +227,17 @@ Please view this file on the master branch, on stable branches it's out of date.
- Gather deepest epic relationship data. - Gather deepest epic relationship data.
## 11.6.10 (2019-02-28)
### Security (5 changes)
- Remove the possibility to share a project with a group that a user is not a member of.
- Prevent Group SAML authorizing sign in without prior user approval.
- Prevent SAML access when disabled by group admin on GitLab.com.
- Respect group membership lock when importing a member from another group.
- Ignore out of range epic IDs.
## 11.6.9 (2019-02-04) ## 11.6.9 (2019-02-04)
- No changes. - No changes.
......
...@@ -486,6 +486,33 @@ entry. ...@@ -486,6 +486,33 @@ entry.
- Update url placeholder for the sentry configuration page. !24338 - Update url placeholder for the sentry configuration page. !24338
## 11.6.10 (2019-02-28)
### Security (21 changes)
- Stop linking to unrecognized package sources. !55518
- Check snippet attached file to be moved is within designated directory.
- Fix potential Addressable::URI::InvalidURIError.
- Do not display impersonated sessions under active sessions and remove ability to revoke session.
- Display only information visible to current user on the Milestone page.
- Show only merge requests visible to user on milestone detail page.
- Disable issue boards API when issues are disabled.
- Don't show new issue link after move when a user does not have permissions.
- Fix git clone revealing private repo's presence.
- Fix blind SSRF in Prometheus integration by checking URL before querying.
- Check if desired milestone for an issue is available.
- Don't allow non-members to see private related MRs.
- Fix arbitrary file read via diffs during import.
- Display the correct number of MRs a user has access to.
- Forbid creating discussions for users with restricted access.
- Do not disclose milestone titles for unauthorized users.
- Validate session key when authorizing with GCP to create a cluster.
- Block local URLs for Kubernetes integration.
- Limit mermaid rendering to 5K characters.
- Remove the possibility to share a project with a group that a user is not a member of.
- Fix leaking private repository information in API.
## 11.6.9 (2019-02-04) ## 11.6.9 (2019-02-04)
### Security (1 change) ### Security (1 change)
......
...@@ -7,6 +7,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -7,6 +7,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
before_action :check_membership_lock!, only: [:create, :import, :apply_import]
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def index def index
...@@ -50,4 +51,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -50,4 +51,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# MembershipActions concern # MembershipActions concern
alias_method :membershipable, :project alias_method :membershipable, :project
def check_membership_lock!
access_denied!('Membership is locked by group settings') if membership_locked?
end
end end
...@@ -53,6 +53,18 @@ module Milestoneish ...@@ -53,6 +53,18 @@ module Milestoneish
end end
end end
def issue_participants_visible_by_user(user)
User.joins(:issue_assignees)
.where('issue_assignees.issue_id' => issues_visible_to_user(user).select(:id))
.distinct
end
def issue_labels_visible_by_user(user)
Label.joins(:label_links)
.where('label_links.target_id' => issues_visible_to_user(user).select(:id), 'label_links.target_type' => 'Issue')
.distinct
end
def sorted_issues(user) def sorted_issues(user)
issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority') issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority')
end end
......
...@@ -21,11 +21,11 @@ ...@@ -21,11 +21,11 @@
%li.nav-item %li.nav-item
= link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do = link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants Participants
%span.badge.badge-pill= milestone.participants.count %span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count
%li.nav-item %li.nav-item
= link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do = link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels Labels
%span.badge.badge-pill= milestone.labels.count %span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
- issues = milestone.sorted_issues(current_user) - issues = milestone.sorted_issues(current_user)
- show_project_name = local_assigns.fetch(:show_project_name, false) - show_project_name = local_assigns.fetch(:show_project_name, false)
......
---
title: Display only information visible to current user on the Milestone page
merge_request:
author:
type: security
...@@ -9,9 +9,11 @@ class Groups::OmniauthCallbacksController < OmniauthCallbacksController ...@@ -9,9 +9,11 @@ class Groups::OmniauthCallbacksController < OmniauthCallbacksController
@unauthenticated_group = Group.find_by_full_path(params[:group_id]) @unauthenticated_group = Group.find_by_full_path(params[:group_id])
@saml_provider = @unauthenticated_group.saml_provider @saml_provider = @unauthenticated_group.saml_provider
identity_linker = Gitlab::Auth::GroupSaml::IdentityLinker.new(current_user, oauth, @saml_provider) identity_linker = Gitlab::Auth::GroupSaml::IdentityLinker.new(current_user, oauth, @saml_provider, session)
omniauth_flow(Gitlab::Auth::GroupSaml, identity_linker: identity_linker) omniauth_flow(Gitlab::Auth::GroupSaml, identity_linker: identity_linker)
rescue Gitlab::Auth::GroupSaml::IdentityLinker::UnverifiedRequest
redirect_unverified_saml_initiation
end end
private private
...@@ -44,6 +46,12 @@ class Groups::OmniauthCallbacksController < OmniauthCallbacksController ...@@ -44,6 +46,12 @@ class Groups::OmniauthCallbacksController < OmniauthCallbacksController
super super
end end
def redirect_unverified_saml_initiation
flash[:notice] = "Request to link SAML account must be authorized"
redirect_to sso_group_saml_providers_path(@unauthenticated_group)
end
override :after_sign_in_path_for override :after_sign_in_path_for
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
saml_redirect_path || super saml_redirect_path || super
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module EE module EE
module MilestonesHelper module MilestonesHelper
def burndown_chart(milestone) def burndown_chart(milestone)
Burndown.new(milestone) if milestone.supports_burndown_charts? Burndown.new(milestone, current_user) if milestone.supports_burndown_charts?
end end
def can_generate_chart?(milestone, burndown) def can_generate_chart?(milestone, burndown)
......
...@@ -17,12 +17,13 @@ class Burndown ...@@ -17,12 +17,13 @@ class Burndown
end end
end end
attr_reader :start_date, :due_date, :end_date, :accurate, :legacy_data, :milestone attr_reader :start_date, :due_date, :end_date, :accurate, :legacy_data, :milestone, :current_user
alias_method :accurate?, :accurate alias_method :accurate?, :accurate
alias_method :empty?, :legacy_data alias_method :empty?, :legacy_data
def initialize(milestone) def initialize(milestone, current_user)
@milestone = milestone @milestone = milestone
@current_user = current_user
@start_date = @milestone.start_date @start_date = @milestone.start_date
@due_date = @milestone.due_date @due_date = @milestone.due_date
@end_date = @milestone.due_date @end_date = @milestone.due_date
...@@ -89,10 +90,10 @@ class Burndown ...@@ -89,10 +90,10 @@ class Burndown
strong_memoize(:opened_issues_grouped_by_date) do strong_memoize(:opened_issues_grouped_by_date) do
issues = issues =
@milestone @milestone
.issues .issues_visible_to_user(current_user)
.where('created_at <= ?', end_date) .where('issues.created_at <= ?', end_date)
.reorder(nil) .reorder(nil)
.order(:created_at).to_a .order('issues.created_at').to_a
issues.group_by do |issue| issues.group_by do |issue|
issue.created_at.to_date issue.created_at.to_date
...@@ -124,9 +125,8 @@ class Burndown ...@@ -124,9 +125,8 @@ class Burndown
# `issues.closed_at` can't be used once it's nullified if the issue is # `issues.closed_at` can't be used once it's nullified if the issue is
# reopened. # reopened.
internal_clause = internal_clause =
::Issue @milestone.issues_visible_to_user(current_user)
.joins("LEFT OUTER JOIN events e ON issues.id = e.target_id AND e.target_type = 'Issue' AND e.action = #{Event::CLOSED}") .joins("LEFT OUTER JOIN events e ON issues.id = e.target_id AND e.target_type = 'Issue' AND e.action = #{Event::CLOSED}")
.where(milestone: @milestone)
.where("state = 'closed' OR (state = 'opened' AND e.action = #{Event::CLOSED})") # rubocop:disable GitlabSecurity/SqlInjection .where("state = 'closed' OR (state = 'opened' AND e.action = #{Event::CLOSED})") # rubocop:disable GitlabSecurity/SqlInjection
rel = rel =
......
...@@ -8,7 +8,10 @@ module EE ...@@ -8,7 +8,10 @@ module EE
override :execute override :execute
def execute(group) def execute(group)
super.tap { |link| log_audit_event(link) if link && link&.persisted? } result = super
log_audit_event(result[:link]) if result[:status] == :success
result
end end
private private
......
---
title: Remove the possibility to share a project with a group that a user is not a member
of
merge_request:
author:
type: security
---
title: Prevent Group SAML authorizing sign in without prior user approval
merge_request:
author:
type: security
---
title: Respect group membership lock when importing a member from another group
merge_request:
author:
type: security
...@@ -4,15 +4,20 @@ module Gitlab ...@@ -4,15 +4,20 @@ module Gitlab
module Auth module Auth
module GroupSaml module GroupSaml
class IdentityLinker < Gitlab::Auth::Saml::IdentityLinker class IdentityLinker < Gitlab::Auth::Saml::IdentityLinker
attr_reader :saml_provider attr_reader :saml_provider, :session
def initialize(current_user, oauth, saml_provider) UnverifiedRequest = Class.new(StandardError)
def initialize(current_user, oauth, saml_provider, session)
super(current_user, oauth) super(current_user, oauth)
@saml_provider = saml_provider @saml_provider = saml_provider
@session = session
end end
def link def link
raise_unless_request_is_gitlab_initiated! if unlinked?
super super
update_group_membership unless failed? update_group_membership unless failed?
...@@ -32,6 +37,18 @@ module Gitlab ...@@ -32,6 +37,18 @@ module Gitlab
def update_group_membership def update_group_membership
MembershipUpdater.new(current_user, saml_provider).execute MembershipUpdater.new(current_user, saml_provider).execute
end end
def raise_unless_request_is_gitlab_initiated!
raise UnverifiedRequest unless valid_gitlab_initated_request?
end
def valid_gitlab_initated_request?
SamlOriginValidator.new(session).gitlab_initiated?(saml_response)
end
def saml_response
oauth.extra.response_object
end
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Auth
class SamlOriginValidator
attr_reader :session
AUTH_REQUEST_SESSION_KEY = "last_authn_request_id".freeze
def initialize(session)
@session = session
end
def store_origin(authn_request)
session[AUTH_REQUEST_SESSION_KEY] = authn_request.uuid
end
def gitlab_initiated?(saml_response)
return false if identity_provider_initiated?(saml_response)
matches?(saml_response)
end
private
def matches?(saml_response)
saml_response.in_response_to == expected_request_id
end
def identity_provider_initiated?(saml_response)
saml_response.in_response_to.blank?
end
def expected_request_id
session[AUTH_REQUEST_SESSION_KEY]
end
end
end
end
...@@ -36,6 +36,20 @@ module OmniAuth ...@@ -36,6 +36,20 @@ module OmniAuth
end end
end end
# NOTE: This method duplicates code from omniauth-saml
# so that we can access authn_request to store it
# See: https://github.com/omniauth/omniauth-saml/issues/172
override :request_phase
def request_phase
authn_request = OneLogin::RubySaml::Authrequest.new
store_authn_request_id(authn_request)
with_settings do |settings|
redirect(authn_request.create(settings, additional_params_for_authn_request))
end
end
def self.invalid_group!(path) def self.invalid_group!(path)
raise ActionController::RoutingError, path raise ActionController::RoutingError, path
end end
...@@ -54,6 +68,10 @@ module OmniAuth ...@@ -54,6 +68,10 @@ module OmniAuth
Feature.enabled?(:group_saml_metadata_available, group_lookup.group) Feature.enabled?(:group_saml_metadata_available, group_lookup.group)
end end
def store_authn_request_id(authn_request)
Gitlab::Auth::SamlOriginValidator.new(session).store_origin(authn_request)
end
def group_lookup def group_lookup
@group_lookup ||= Gitlab::Auth::GroupSaml::GroupLookup.new(env) @group_lookup ||= Gitlab::Auth::GroupSaml::GroupLookup.new(env)
end end
......
...@@ -9,6 +9,9 @@ describe Groups::OmniauthCallbacksController do ...@@ -9,6 +9,9 @@ describe Groups::OmniauthCallbacksController do
let(:provider) { :group_saml } let(:provider) { :group_saml }
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
let!(:saml_provider) { create(:saml_provider, group: group) } let!(:saml_provider) { create(:saml_provider, group: group) }
let(:in_response_to) { '12345' }
let(:last_request_id) { in_response_to }
let(:saml_response) { instance_double(OneLogin::RubySaml::Response, in_response_to: in_response_to) }
before do before do
stub_licensed_features(group_saml: true) stub_licensed_features(group_saml: true)
...@@ -22,6 +25,10 @@ describe Groups::OmniauthCallbacksController do ...@@ -22,6 +25,10 @@ describe Groups::OmniauthCallbacksController do
create(:omniauth_user, extern_uid: uid, provider: provider, saml_provider: saml_provider) create(:omniauth_user, extern_uid: uid, provider: provider, saml_provider: saml_provider)
end end
def stub_last_request_id(id)
session["last_authn_request_id"] = id
end
context "when request hasn't been validated by omniauth middleware" do context "when request hasn't been validated by omniauth middleware" do
it "prevents authentication" do it "prevents authentication" do
sign_in(user) sign_in(user)
...@@ -34,8 +41,9 @@ describe Groups::OmniauthCallbacksController do ...@@ -34,8 +41,9 @@ describe Groups::OmniauthCallbacksController do
context "valid credentials" do context "valid credentials" do
before do before do
mock_auth_hash(provider, uid, user.email) mock_auth_hash(provider, uid, user.email, response_object: saml_response)
stub_omniauth_provider(provider, context: request) stub_omniauth_provider(provider, context: request)
stub_last_request_id(last_request_id)
end end
shared_examples "and identity already linked" do shared_examples "and identity already linked" do
...@@ -104,6 +112,22 @@ describe Groups::OmniauthCallbacksController do ...@@ -104,6 +112,22 @@ describe Groups::OmniauthCallbacksController do
expect(flash[:notice]).to match(/SAML for .* was added/) expect(flash[:notice]).to match(/SAML for .* was added/)
end end
context 'with IdP initiated request' do
let(:last_request_id) { '99999' }
it 'redirects to account link page' do
post provider, params: { group_id: group }
expect(response).to redirect_to(sso_group_saml_providers_path(group))
end
it "lets the user know their account isn't linked yet" do
post provider, params: { group_id: group }
expect(flash[:notice]).to eq 'Request to link SAML account must be authorized'
end
end
end end
end end
......
...@@ -20,7 +20,9 @@ describe 'Profile > Account' do ...@@ -20,7 +20,9 @@ describe 'Profile > Account' do
def create_linked_identity def create_linked_identity
oauth = { 'provider' => 'group_saml', 'uid' => '1' } oauth = { 'provider' => 'group_saml', 'uid' => '1' }
Gitlab::Auth::GroupSaml::IdentityLinker.new(user, oauth, saml_provider).link identity_linker = Gitlab::Auth::GroupSaml::IdentityLinker.new(user, oauth, saml_provider, double(:session))
allow(identity_linker).to receive(:valid_gitlab_initated_request?).and_return(true)
identity_linker.link
end end
before do before do
......
...@@ -57,6 +57,7 @@ describe 'Project > Members > Invite group and members', :js do ...@@ -57,6 +57,7 @@ describe 'Project > Members > Invite group and members', :js do
before do before do
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
group_to_share_with.add_developer(maintainer)
sign_in(maintainer) sign_in(maintainer)
end end
......
...@@ -4,10 +4,13 @@ describe Gitlab::Auth::GroupSaml::IdentityLinker do ...@@ -4,10 +4,13 @@ describe Gitlab::Auth::GroupSaml::IdentityLinker do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:provider) { 'group_saml' } let(:provider) { 'group_saml' }
let(:uid) { user.email } let(:uid) { user.email }
let(:oauth) { { 'provider' => provider, 'uid' => uid } } let(:in_response_to) { '12345' }
let(:saml_response) { instance_double(OneLogin::RubySaml::Response, in_response_to: in_response_to) }
let(:oauth) { OmniAuth::AuthHash.new(provider: provider, uid: uid, extra: { response_object: saml_response }) }
let(:saml_provider) { create(:saml_provider) } let(:saml_provider) { create(:saml_provider) }
let(:session) { {} }
subject { described_class.new(user, oauth, saml_provider) } subject { described_class.new(user, oauth, saml_provider, session) }
context 'linked identity exists' do context 'linked identity exists' do
let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid, saml_provider: saml_provider) } let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid, saml_provider: saml_provider) }
...@@ -30,6 +33,15 @@ describe Gitlab::Auth::GroupSaml::IdentityLinker do ...@@ -30,6 +33,15 @@ describe Gitlab::Auth::GroupSaml::IdentityLinker do
end end
context 'identity needs to be created' do context 'identity needs to be created' do
context 'with identity provider initiated request' do
it 'attempting to link accounts raises an exception' do
expect { subject.link }.to raise_error(Gitlab::Auth::GroupSaml::IdentityLinker::UnverifiedRequest)
end
end
context 'with valid gitlab initiated request' do
let(:session) { { 'last_authn_request_id' => in_response_to } }
it 'creates linked identity' do it 'creates linked identity' do
expect { subject.link }.to change { user.identities.count } expect { subject.link }.to change { user.identities.count }
end end
...@@ -64,4 +76,5 @@ describe Gitlab::Auth::GroupSaml::IdentityLinker do ...@@ -64,4 +76,5 @@ describe Gitlab::Auth::GroupSaml::IdentityLinker do
expect(saml_provider.group.member?(user)).to eq(true) expect(saml_provider.group.member?(user)).to eq(true)
end end
end end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Auth::SamlOriginValidator do
let(:session) { instance_double(ActionDispatch::Request::Session) }
subject { described_class.new(session) }
describe '#store_origin' do
it 'stores the SAML request ID' do
request_id = double
authn_request = instance_double(OneLogin::RubySaml::Authrequest, uuid: request_id)
expect(session).to receive(:[]=).with('last_authn_request_id', request_id)
subject.store_origin(authn_request)
end
end
describe '#gitlab_initiated?' do
it 'returns false if InResponseTo is not present' do
saml_response = instance_double(OneLogin::RubySaml::Response, in_response_to: nil)
expect(subject.gitlab_initiated?(saml_response)).to eq(false)
end
it 'returns false if InResponseTo does not match stored value' do
saml_response = instance_double(OneLogin::RubySaml::Response, in_response_to: "abc")
allow(session).to receive(:[]).with('last_authn_request_id').and_return('123')
expect(subject.gitlab_initiated?(saml_response)).to eq(false)
end
it 'returns true if InResponseTo matches stored value' do
saml_response = instance_double(OneLogin::RubySaml::Response, in_response_to: "123")
allow(session).to receive(:[]).with('last_authn_request_id').and_return('123')
expect(subject.gitlab_initiated?(saml_response)).to eq(true)
end
end
end
...@@ -110,6 +110,15 @@ describe OmniAuth::Strategies::GroupSaml, type: :strategy do ...@@ -110,6 +110,15 @@ describe OmniAuth::Strategies::GroupSaml, type: :strategy do
post '/users/auth/group_saml' post '/users/auth/group_saml'
end.to raise_error(ActionController::RoutingError) end.to raise_error(ActionController::RoutingError)
end end
it "stores request ID during request phase" do
request_id = double
allow_any_instance_of(OneLogin::RubySaml::Authrequest).to receive(:uuid).and_return(request_id)
post '/users/auth/group_saml', group_path: 'my-group'
expect(session['last_authn_request_id']).to eq(request_id)
end
end end
describe 'POST /users/auth/group_saml/metadata' do describe 'POST /users/auth/group_saml/metadata' do
......
...@@ -2,6 +2,7 @@ require 'spec_helper' ...@@ -2,6 +2,7 @@ require 'spec_helper'
describe Burndown do describe Burndown do
set(:user) { create(:user) } set(:user) { create(:user) }
set(:non_member) { create(:user) }
let(:start_date) { "2017-03-01" } let(:start_date) { "2017-03-01" }
let(:due_date) { "2017-03-03" } let(:due_date) { "2017-03-03" }
...@@ -16,13 +17,13 @@ describe Burndown do ...@@ -16,13 +17,13 @@ describe Burndown do
end end
end end
subject { described_class.new(milestone).to_json } subject { described_class.new(milestone, user).to_json }
it "generates an array with date, issue count and weight" do it "generates an array with date, issue count and weight" do
expect(subject).to eq([ expect(subject).to eq([
["2017-03-01", 3, 6], ["2017-03-01", 4, 8],
["2017-03-02", 4, 8], ["2017-03-02", 5, 10],
["2017-03-03", 2, 4] ["2017-03-03", 3, 6]
].to_json) ].to_json)
end end
...@@ -45,7 +46,7 @@ describe Burndown do ...@@ -45,7 +46,7 @@ describe Burndown do
end end
it "sets attribute accurate to true" do it "sets attribute accurate to true" do
burndown = described_class.new(milestone) burndown = described_class.new(milestone, user)
expect(burndown).to be_accurate expect(burndown).to be_accurate
end end
...@@ -57,14 +58,14 @@ describe Burndown do ...@@ -57,14 +58,14 @@ describe Burndown do
it "considers closed_at as milestone start date" do it "considers closed_at as milestone start date" do
expect(subject).to eq([ expect(subject).to eq([
["2017-03-01", 3, 6], ["2017-03-01", 4, 8],
["2017-03-02", 3, 6], ["2017-03-02", 4, 8],
["2017-03-03", 3, 6] ["2017-03-03", 4, 8]
].to_json) ].to_json)
end end
it "sets attribute empty to true" do it "sets attribute empty to true" do
burndown = described_class.new(milestone) burndown = described_class.new(milestone, user)
expect(burndown).to be_empty expect(burndown).to be_empty
end end
...@@ -76,7 +77,7 @@ describe Burndown do ...@@ -76,7 +77,7 @@ describe Burndown do
end end
it "sets attribute accurate to false" do it "sets attribute accurate to false" do
burndown = described_class.new(milestone) burndown = described_class.new(milestone, user)
expect(burndown).not_to be_accurate expect(burndown).not_to be_accurate
end end
...@@ -92,12 +93,26 @@ describe Burndown do ...@@ -92,12 +93,26 @@ describe Burndown do
create(:issue, milestone: milestone, project: project, created_at: creation_date, weight: 3) create(:issue, milestone: milestone, project: project, created_at: creation_date, weight: 3)
expect(subject).to eq([ expect(subject).to eq([
['2017-03-01', 3, 6], ['2017-03-01', 4, 8],
['2017-03-02', 6, 13], ['2017-03-02', 7, 15],
['2017-03-03', 4, 9] ['2017-03-03', 5, 11]
].to_json) ].to_json)
end end
end end
context 'when issues belong to a public project' do
it 'does not include confidential issues for users who are not project members' do
project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
expected_result = [
["2017-03-01", 3, 6],
["2017-03-02", 4, 8],
["2017-03-03", 2, 4]
].to_json
expect(described_class.new(milestone, non_member).to_json).to eq(expected_result)
end
end
end end
describe 'project milestone burndown' do describe 'project milestone burndown' do
...@@ -126,6 +141,10 @@ describe Burndown do ...@@ -126,6 +141,10 @@ describe Burndown do
let(:nested_group_milestone) { create(:milestone, group: nested_group, start_date: start_date, due_date: due_date) } let(:nested_group_milestone) { create(:milestone, group: nested_group, start_date: start_date, due_date: due_date) }
context 'when nested group milestone', :nested_groups do context 'when nested group milestone', :nested_groups do
before do
group.add_developer(user)
end
it_behaves_like 'burndown for milestone' do it_behaves_like 'burndown for milestone' do
let(:milestone) { nested_group_milestone } let(:milestone) { nested_group_milestone }
let(:project) { nested_group_project } let(:project) { nested_group_project }
...@@ -194,6 +213,9 @@ describe Burndown do ...@@ -194,6 +213,9 @@ describe Burndown do
issue_closed_twice = reopened_issues.last issue_closed_twice = reopened_issues.last
close_issue(issue_closed_twice) close_issue(issue_closed_twice)
reopen_issue(issue_closed_twice) reopen_issue(issue_closed_twice)
# create one confidential issue
create(:issue, :confidential, issue_params) if Date.today == milestone.start_date
end end
end end
end end
......
...@@ -36,6 +36,7 @@ describe Projects::GroupLinks::CreateService, '#execute' do ...@@ -36,6 +36,7 @@ describe Projects::GroupLinks::CreateService, '#execute' do
end end
def create_group_link(user, project, group, opts) def create_group_link(user, project, group, opts)
group.add_developer(user)
described_class.new(project, user, opts).execute(group) described_class.new(project, user, opts).execute(group)
end end
end end
...@@ -2,7 +2,8 @@ require('spec_helper') ...@@ -2,7 +2,8 @@ require('spec_helper')
describe Projects::ProjectMembersController do describe Projects::ProjectMembersController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public, :access_requestable) } let(:project) { create(:project, :public, :access_requestable, namespace: namespace) }
let(:namespace) { create :group }
describe 'GET index' do describe 'GET index' do
it 'should have the project_members address with a 200 status code' do it 'should have the project_members address with a 200 status code' do
...@@ -313,6 +314,13 @@ describe Projects::ProjectMembersController do ...@@ -313,6 +314,13 @@ describe Projects::ProjectMembersController do
end end
describe 'POST apply_import' do describe 'POST apply_import' do
subject(:apply_import) do
post(:apply_import, params: {
namespace_id: project.namespace,
project_id: project,
source_project_id: another_project.id
})
end
let(:another_project) { create(:project, :private) } let(:another_project) { create(:project, :private) }
let(:member) { create(:user) } let(:member) { create(:user) }
...@@ -322,40 +330,44 @@ describe Projects::ProjectMembersController do ...@@ -322,40 +330,44 @@ describe Projects::ProjectMembersController do
sign_in(user) sign_in(user)
end end
shared_context 'import applied' do
before do
post(:apply_import, params: {
namespace_id: project.namespace,
project_id: project,
source_project_id: another_project.id
})
end
end
context 'when user can access source project members' do context 'when user can access source project members' do
before do before do
another_project.add_guest(user) another_project.add_guest(user)
end end
include_context 'import applied'
it 'imports source project members' do it 'imports source project members' do
apply_import
expect(project.team_members).to include member expect(project.team_members).to include member
expect(response).to set_flash.to 'Successfully imported' expect(response).to set_flash.to 'Successfully imported'
expect(response).to redirect_to( expect(response).to redirect_to(
project_project_members_path(project) project_project_members_path(project)
) )
end end
context 'and the project group has membership lock enabled' do
before do
project.namespace.update(membership_lock: true)
end end
context 'when user is not member of a source project' do it 'responds with 403' do
include_context 'import applied' apply_import
expect(response.status).to eq 403
end
end
end
context 'when user is not member of a source project' do
it 'does not import team members' do it 'does not import team members' do
apply_import
expect(project.team_members).not_to include member expect(project.team_members).not_to include member
end end
it 'responds with not found' do it 'responds with not found' do
apply_import
expect(response.status).to eq 404 expect(response.status).to eq 404
end end
end end
...@@ -363,40 +375,78 @@ describe Projects::ProjectMembersController do ...@@ -363,40 +375,78 @@ describe Projects::ProjectMembersController do
describe 'POST create' do describe 'POST create' do
let(:stranger) { create(:user) } let(:stranger) { create(:user) }
subject(:create_member) do
post :create, params: {
user_ids: stranger.id,
namespace_id: project.namespace,
access_level: access_level,
project_id: project
}
end
let(:access_level) { nil }
context 'when creating owner' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
end end
context 'when creating owner' do
let(:access_level) { Member::OWNER }
it 'does not create a member' do it 'does not create a member' do
expect do expect { create_member }.not_to change { project.members.count }
post :create, params: {
user_ids: stranger.id,
namespace_id: project.namespace,
access_level: Member::OWNER,
project_id: project
}
end.to change { project.members.count }.by(0)
end end
end end
context 'when create maintainer' do context 'when create maintainer' do
let(:access_level) { Member::MAINTAINER }
it 'creates a member' do
expect { create_member }.to change { project.members.count }.by(1)
end
end
context 'when project group has membership lock enabled' do
before do before do
project.add_maintainer(user) project.namespace.update(membership_lock: true)
sign_in(user)
end end
it 'creates a member' do it 'responds with 403' do
expect do create_member
post :create, params: {
user_ids: stranger.id, expect(response.status).to eq 403
end
end
end
describe 'GET import' do
subject(:import) do
get :import, params: {
namespace_id: project.namespace, namespace_id: project.namespace,
access_level: Member::MAINTAINER,
project_id: project project_id: project
} }
end.to change { project.members.count }.by(1) end
before do
project.add_maintainer(user)
sign_in(user)
end
it 'responds with 200' do
import
expect(response.status).to eq 200
end
context 'when project group has membership lock enabled' do
before do
project.namespace.update(membership_lock: true)
end
it 'responds with 403' do
import
expect(response.status).to eq 403
end end
end end
end end
......
...@@ -9,8 +9,10 @@ describe Milestone, 'Milestoneish' do ...@@ -9,8 +9,10 @@ describe Milestone, 'Milestoneish' do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) } let(:label1) { create(:label, project: project) }
let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) } let(:label2) { create(:label, project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone, assignees: [member], labels: [label1]) }
let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone, labels: [label2]) }
let!(:security_issue_2) { create(:issue, :confidential, project: project, assignees: [assignee], milestone: milestone) } let!(:security_issue_2) { create(:issue, :confidential, project: project, assignees: [assignee], milestone: milestone) }
let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) } let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) } let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
...@@ -42,6 +44,95 @@ describe Milestone, 'Milestoneish' do ...@@ -42,6 +44,95 @@ describe Milestone, 'Milestoneish' do
end end
end end
context 'attributes visibility' do
using RSpec::Parameterized::TableSyntax
let(:users) do
{
anonymous: nil,
non_member: non_member,
guest: guest,
member: member,
assignee: assignee
}
end
let(:project_visibility_levels) do
{
public: Gitlab::VisibilityLevel::PUBLIC,
internal: Gitlab::VisibilityLevel::INTERNAL,
private: Gitlab::VisibilityLevel::PRIVATE
}
end
describe '#issue_participants_visible_by_user' do
where(:visibility, :user_role, :result) do
:public | nil | [:member]
:public | :non_member | [:member]
:public | :guest | [:member]
:public | :member | [:member, :assignee]
:internal | nil | []
:internal | :non_member | [:member]
:internal | :guest | [:member]
:internal | :member | [:member, :assignee]
:private | nil | []
:private | :non_member | []
:private | :guest | [:member]
:private | :member | [:member, :assignee]
end
with_them do
before do
project.update(visibility_level: project_visibility_levels[visibility])
end
it 'returns the proper participants' do
user = users[user_role]
participants = result.map { |role| users[role] }
expect(milestone.issue_participants_visible_by_user(user)).to match_array(participants)
end
end
end
describe '#issue_labels_visible_by_user' do
let(:labels) do
{
label1: label1,
label2: label2
}
end
where(:visibility, :user_role, :result) do
:public | nil | [:label1]
:public | :non_member | [:label1]
:public | :guest | [:label1]
:public | :member | [:label1, :label2]
:internal | nil | []
:internal | :non_member | [:label1]
:internal | :guest | [:label1]
:internal | :member | [:label1, :label2]
:private | nil | []
:private | :non_member | []
:private | :guest | [:label1]
:private | :member | [:label1, :label2]
end
with_them do
before do
project.update(visibility_level: project_visibility_levels[visibility])
end
it 'returns the proper participants' do
user = users[user_role]
expected_labels = result.map { |label| labels[label] }
expect(milestone.issue_labels_visible_by_user(user)).to match_array(expected_labels)
end
end
end
end
describe '#sorted_merge_requests' do describe '#sorted_merge_requests' do
it 'sorts merge requests by label priority' do it 'sorts merge requests by label priority' do
merge_request_1 = create(:labeled_merge_request, labels: [label_2], source_project: project, source_branch: 'branch_1', milestone: milestone) merge_request_1 = create(:labeled_merge_request, labels: [label_2], source_project: project, source_branch: 'branch_1', milestone: milestone)
......
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