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

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents d41d1b0f 17169a02
......@@ -82,6 +82,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
......@@ -724,8 +725,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-parameterized (0.4.0)
binding_of_caller
rspec-parameterized (0.4.1)
binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
......@@ -895,7 +896,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
unparser (0.2.7)
unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
......
......@@ -79,6 +79,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
......@@ -715,8 +716,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-parameterized (0.4.0)
binding_of_caller
rspec-parameterized (0.4.1)
binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
......@@ -889,7 +890,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
unparser (0.2.7)
unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
......
......@@ -14,6 +14,7 @@ const Api = {
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners',
mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
......@@ -124,6 +125,15 @@ const Api = {
return axios.get(url);
},
projectRunners(projectPath, config = {}) {
const url = Api.buildUrl(Api.projectRunnersPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.get(url, config);
},
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
......
......@@ -92,20 +92,7 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
<span>Projects</span>
<button
aria-label="Close"
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<icon
name="merge-request-close-m"
data-hidden="true"
class="dropdown-menu-close-icon"
/>
</button>
</div>
<div class="dropdown-title">Projects</div>
<div class="dropdown-input">
<input class="dropdown-input-field" type="search" placeholder="Search projects" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
......
......@@ -296,7 +296,6 @@ export default {
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
:disabled="!helmInstalled"
class="hide-bottom-border rounded-bottom"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
<div slot="description" v-html="certManagerDescription"></div>
......@@ -396,6 +395,7 @@ export default {
</div>
</application-row>
<application-row
v-if="isProjectCluster"
id="knative"
:logo-url="knativeLogo"
:title="applications.knative.title"
......@@ -405,7 +405,6 @@ export default {
:request-reason="applications.knative.requestReason"
:install-application-request-params="{ hostname: applications.knative.hostname }"
:disabled="!helmInstalled"
class="hide-bottom-border rounded-bottom"
title-link="https://github.com/knative/docs"
>
<div slot="description">
......@@ -432,7 +431,7 @@ export default {
/>
</div>
</template>
<template v-else>
<template v-else-if="helmInstalled">
<div class="form-group">
<label for="knative-domainname">
{{ s__('ClusterIntegration|Knative Domain Name:') }}
......
......@@ -70,7 +70,7 @@ export default {
<template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
<gl-loading-icon :size="2" />
<gl-loading-icon :size="2" class="prepend-top-16" />
</div>
<template v-else>
......
import Vue from 'vue';
import { mapActions } from 'vuex';
import _ from 'underscore';
import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
import store from './stores';
......@@ -13,19 +14,19 @@ Vue.use(Translate);
*
* @param {Element} el - The element that will contain the IDE.
* @param {Object} options - Extra options for the IDE (Used by EE).
* @param {(e:Element) => Object} options.extraInitialData -
* Function that returns extra properties to seed initial data.
* @param {Component} options.rootComponent -
* Component that overrides the root component.
* @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore -
* Function that receives the default store and returns an extended one.
*/
export function initIde(el, options = {}) {
if (!el) return null;
const { extraInitialData = () => ({}), rootComponent = ide } = options;
const { rootComponent = ide, extendStore = _.identity } = options;
return new Vue({
el,
store,
store: extendStore(store, el),
router,
created() {
this.setEmptyStateSvgs({
......@@ -41,7 +42,6 @@ export function initIde(el, options = {}) {
});
this.setInitialData({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
...extraInitialData(el),
});
},
methods: {
......
......@@ -16,7 +16,9 @@ const httpStatusCodes = {
IM_USED: 226,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
FORBIDDEN: 403,
NOT_FOUND: 404,
UNPROCESSABLE_ENTITY: 422,
};
export const successCodes = [
......
......@@ -65,3 +65,4 @@
@import 'framework/feature_highlight';
@import 'framework/terms';
@import 'framework/read_more';
@import 'framework/flex_grid';
.flex-grid {
.grid-row {
border-bottom: 1px solid $border-color;
padding: 0;
&:last-child {
border-bottom: 0;
}
@include media-breakpoint-down(md) {
border-bottom: 0;
border-right: 1px solid $border-color;
&:last-child {
border-right: 0;
}
}
@include media-breakpoint-down(xs) {
border-right: 0;
border-bottom: 1px solid $border-color;
&:last-child {
border-bottom: 0;
}
}
}
.grid-cell {
padding: 10px $gl-padding;
border-right: 1px solid $border-color;
&:last-child {
border-right: 0;
}
@include media-breakpoint-up(md) {
flex: 1;
}
@include media-breakpoint-down(md) {
border-right: 0;
flex: none;
}
}
}
.card {
.card-body.flex-grid {
padding: 0;
}
}
# frozen_string_literal: true
module IdeHelper
def ide_data
{
"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s
}
end
end
......@@ -120,7 +120,7 @@ module Ci
acts_as_taggable
add_authentication_token_field :token
add_authentication_token_field :token, encrypted: true, fallback: true
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
......
......@@ -7,6 +7,7 @@ class Member < ActiveRecord::Base
include Expirable
include Gitlab::Access
include Presentable
include Gitlab::Utils::StrongMemoize
attr_accessor :raw_invite_token
......@@ -22,6 +23,7 @@ class Member < ActiveRecord::Base
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validate :higher_access_level_than_group, unless: :importing?
validates :invite_email,
presence: {
if: :invite?
......@@ -364,6 +366,15 @@ class Member < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
# Find the user's group member with a highest access level
def highest_group_member
strong_memoize(:highest_group_member) do
next unless user_id && source&.ancestors&.any?
GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
end
end
private
def send_invite
......@@ -430,4 +441,12 @@ class Member < ActiveRecord::Base
def notifiable_options
{}
end
def higher_access_level_than_group
if highest_group_member && highest_group_member.access_level >= access_level
error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
end
end
end
......@@ -570,6 +570,8 @@ class Project < ActiveRecord::Base
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
alias_method :ancestors, :ancestors_upto
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
......
......@@ -31,6 +31,6 @@ class GroupClusterablePresenter < ClusterablePresenter
override :learn_more_link
def learn_more_link
link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/group/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
end
......@@ -7,6 +7,14 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
member.class.access_level_roles
end
def valid_level_roles
return access_level_roles unless member.highest_group_member
access_level_roles.reject do |_name, level|
member.highest_group_member.access_level > level
end
end
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
......
- @body_class = 'ide-layout'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: ide_data() }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
- @body_class = 'ide-layout'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s } }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
= render 'ide/show'
......@@ -75,7 +75,7 @@
= dropdown_title(_("Change permissions"))
.dropdown-content
%ul
- member.access_level_roles.each do |role, role_id|
- member.valid_level_roles.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
......
---
title: Restrict member access level to be higher than that of any parent group
merge_request: 23226
author:
type: fixed
---
title: Support RSA and ECDSA algorithms in Omniauth JWT provider
merge_request: 23411
author: Michael Tsyganov
type: fixed
---
title: Add partial index for ci_builds on project_id and status
merge_request: 23268
author:
type: performance
---
title: 'Fix deprecation: You are passing an instance of ActiveRecord::Base to'
merge_request: 23369
author: Jasper Maes
type: other
---
title: Encrypt CI/CD builds authentication tokens
merge_request: 23436
author:
type: security
---
title: Add top padding for nested environment items loading icon
merge_request: 23580
author: George Tsiolis
type: fixed
---
title: Hide Knative from group cluster applications until supported
merge_request: 23577
author:
type: fixed
---
title: Gracefully handle unknown/invalid GPG keys
merge_request: 23492
author:
type: fixed
---
title: Remove close icon from projects dropdown in issue boards
merge_request: 23567
author:
type: changed
......@@ -548,15 +548,15 @@ production: &base
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
# - { name: 'jwt',
# app_secret: 'YOUR_APP_SECRET',
# args: {
# algorithm: 'HS256',
# uid_claim: 'email',
# required_claims: ["name", "email"],
# info_map: { name: "name", email: "email" },
# auth_url: 'https://example.com/',
# valid_within: null,
# }
# secret: 'YOUR_APP_SECRET',
# algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
# uid_claim: 'email',
# required_claims: ['name', 'email'],
# info_map: { name: 'name', email: 'email' },
# auth_url: 'https://example.com/',
# valid_within: 3600 # 1 hour
# }
# }
# - { name: 'saml',
# label: 'Our SAML Provider',
......
require './spec/support/sidekiq'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
User.all.sample(10).each do |user|
source_project = Project.public_only.sample
fork_project = Projects::ForkService.new(source_project, user, namespace: user.namespace).execute
if fork_project.valid?
puts '.'
else
puts 'F'
end
end
end
end
......@@ -8,7 +8,7 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
disable_ddl_transaction!
def up
add_concurrent_index :ci_pipelines, :merge_request_id
add_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
add_concurrent_foreign_key :ci_pipelines, :merge_requests, column: :merge_request_id, on_delete: :cascade
end
......@@ -17,6 +17,6 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
remove_foreign_key :ci_pipelines, :merge_requests
end
remove_concurrent_index :ci_pipelines, :merge_request_id
remove_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddCiBuildsPartialIndexOnProjectIdAndStatus < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(*index_arguments)
end
def down
remove_concurrent_index(*index_arguments)
end
private
def index_arguments
[
:ci_builds,
[:project_id, :status],
{
name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial2',
where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))"
}
]
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveRedundantCiBuildsPartialIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_concurrent_index(*index_arguments)
end
def down
add_concurrent_index(*index_arguments)
end
private
def index_arguments
[
:ci_builds,
[:project_id, :status],
{
name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial',
where: "((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text]))"
}
]
end
end
# frozen_string_literal: true
class AddTokenEncryptedToCiBuilds < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_builds, :token_encrypted, :string
end
end
# frozen_string_literal: true
class AddIndexToCiBuildsTokenEncrypted < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_builds, :token_encrypted, unique: true, where: 'token_encrypted IS NOT NULL'
end
def down
remove_concurrent_index :ci_builds, :token_encrypted
end
end
......@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20181126153547) do
ActiveRecord::Schema.define(version: 20181129104944) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -345,6 +345,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.boolean "protected"
t.integer "failure_reason"
t.datetime_with_timezone "scheduled_at"
t.string "token_encrypted"
t.index ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
t.index ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
......@@ -353,6 +354,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree
t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree
t.index ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree
......@@ -360,6 +362,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
t.index ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
t.index ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
t.index ["token_encrypted"], name: "index_ci_builds_on_token_encrypted", unique: true, where: "(token_encrypted IS NOT NULL)", using: :btree
t.index ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
t.index ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
end
......@@ -476,7 +479,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.integer "iid"
t.integer "merge_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", using: :btree
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)", using: :btree
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
t.index ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
......
......@@ -10,7 +10,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID
Bitbucket, Facebook, Shibboleth, Crowd, Azure, Authentiq ID, and JWT
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
......
......@@ -26,15 +26,15 @@ JWT will provide you with a secret key for you to use.
```ruby
gitlab_rails['omniauth_providers'] = [
{ name: 'jwt',
app_secret: 'YOUR_APP_SECRET',
args: {
algorithm: 'HS256',
uid_claim: 'email',
required_claims: ["name", "email"],
info_maps: { name: "name", email: "email" },
auth_url: 'https://example.com/',
valid_within: nil,
}
secret: 'YOUR_APP_SECRET',
algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
uid_claim: 'email',
required_claims: ['name', 'email'],
info_maps: { name: 'name', email: 'email' },
auth_url: 'https://example.com/',
valid_within: 3600 # 1 hour
}
}
]
```
......@@ -43,15 +43,15 @@ JWT will provide you with a secret key for you to use.
```
- { name: 'jwt',
app_secret: 'YOUR_APP_SECRET',
args: {
algorithm: 'HS256',
uid_claim: 'email',
required_claims: ["name", "email"],
info_map: { name: "name", email: "email" },
auth_url: 'https://example.com/',
valid_within: null,
}
secret: 'YOUR_APP_SECRET',
algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
uid_claim: 'email',
required_claims: ['name', 'email'],
info_map: { name: 'name', email: 'email' },
auth_url: 'https://example.com/',
valid_within: 3600 # 1 hour
}
}
```
......@@ -60,7 +60,7 @@ JWT will provide you with a secret key for you to use.
1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL.
1. Save the configuration file.
1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you
1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a JWT icon below the regular sign in form.
......@@ -68,5 +68,5 @@ Click the icon to begin the authentication process. JWT will ask the user to
sign in and authorize the GitLab application. If everything goes well, the user
will be redirected to GitLab and will be signed in.
[reconfigure GitLab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../restart_gitlab.md#installations-from-source
......@@ -77,8 +77,11 @@ that builds on this to add some additional niceties, such as allowing
configuration with a single Yaml file for multiple URLs, and uploading of the
profile and log output to S3.
For GitLab.com, you can find the latest results here:
<http://redash.gitlab.com/dashboard/gitlab-profiler-statistics>
For GitLab.com, currently the latest profiling data has been [moved from
Redash to Looker](https://gitlab.com/gitlab-com/Product/issues/5#note_121194467).
We are [currently investigating how to make this data
public](https://gitlab.com/meltano/looker/issues/294).
## Sherlock
......
......@@ -31,11 +31,7 @@ After that, the next pipeline will use the up-to-date
The GitLab test suite is [monitored] for the `master` branch, and any branch
that includes `rspec-profile` in their name.
A [public dashboard] is available for everyone to see. Feel free to look at the
slowest test files and try to improve them.
[monitored]: ../performance.md#rspec-profiling
[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
## CI setup
......
......@@ -44,9 +44,8 @@ module Gitlab
def update_signature!(cached_signature)
using_keychain do |gpg_key|
cached_signature.update!(attributes(gpg_key))
@signature = cached_signature
end
@signature = cached_signature
end
private
......@@ -59,11 +58,15 @@ module Gitlab
# the proper signature.
# NOTE: the invoked method is #fingerprint but it's only returning
# 16 characters (the format used by keyid) instead of 40.
gpg_key = find_gpg_key(verified_signature.fingerprint)
fingerprint = verified_signature&.fingerprint
break unless fingerprint
gpg_key = find_gpg_key(fingerprint)
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
@verified_signature = nil
clear_memoization(:verified_signature)
end
yield gpg_key
......@@ -71,9 +74,16 @@ module Gitlab
end
def verified_signature
@verified_signature ||= GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
strong_memoize(:verified_signature) { gpgme_signature }
end
def gpgme_signature
GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
# Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-ce/issues/54932
break verified_signature
end
rescue GPGME::Error
nil
end
def create_cached_signature!
......@@ -92,7 +102,7 @@ module Gitlab
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
gpg_key_primary_keyid: gpg_key&.keyid || verified_signature&.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
verification_status: verification_status
......@@ -102,7 +112,7 @@ module Gitlab
def verification_status(gpg_key)
return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
return :unverified unless verified_signature.valid?
return :unverified unless verified_signature&.valid?
if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
:verified
......
......@@ -142,6 +142,7 @@ excluded_attributes:
statuses:
- :trace
- :token
- :token_encrypted
- :when
- :artifacts_file
- :artifacts_metadata
......
# frozen_string_literal: true
require 'omniauth'
require 'openssl'
require 'jwt'
module OmniAuth
......@@ -37,7 +38,19 @@ module OmniAuth
end
def decoded
@decoded ||= ::JWT.decode(request.params['jwt'], options.secret, options.algorithm).first
secret =
case options.algorithm
when *%w[RS256 RS384 RS512]
OpenSSL::PKey::RSA.new(options.secret).public_key
when *%w[ES256 ES384 ES512]
OpenSSL::PKey::EC.new(options.secret).tap { |key| key.private_key = nil }
when *%w(HS256 HS384 HS512)
options.secret
else
raise NotImplementedError, "Unsupported algorithm: #{options.algorithm}"
end
@decoded ||= ::JWT.decode(request.params['jwt'], secret, true, { algorithm: options.algorithm }).first
(options.required_claims || []).each do |field|
raise ClaimInvalid, "Missing required '#{field}' claim" unless @decoded.key?(field.to_s)
......@@ -45,7 +58,7 @@ module OmniAuth
raise ClaimInvalid, "Missing required 'iat' claim" if options.valid_within && !@decoded["iat"]
if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within
if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within.to_i
raise ClaimInvalid, "'iat' timestamp claim is too skewed from present"
end
......
......@@ -7932,6 +7932,9 @@ msgid_plural "replies"
msgstr[0] ""
msgstr[1] ""
msgid "should be higher than %{access} inherited membership from group %{group_name}"
msgstr ""
msgid "source"
msgstr ""
......
......@@ -19,7 +19,7 @@ describe GroupMembersFinder, '#execute' do
end
it 'returns members for nested group', :nested_groups do
group.add_maintainer(user2)
group.add_developer(user2)
nested_group.request_access(user4)
member1 = group.add_maintainer(user1)
member3 = nested_group.add_maintainer(user2)
......
......@@ -180,6 +180,23 @@ describe('Api', () => {
});
});
describe('projectRunners', () => {
it('fetches the runners of a project', done => {
const projectPath = 7;
const params = { scope: 'active' };
const mockData = [{ id: 4 }];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/runners`;
mock.onGet(expectedUrl, { params }).reply(200, mockData);
Api.projectRunners(projectPath, { params })
.then(({ data }) => {
expect(data).toEqual(mockData);
})
.then(done)
.catch(done.fail);
});
});
describe('newLabel', () => {
it('creates a new label', done => {
const namespace = 'some namespace';
......
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Applications', () => {
......@@ -14,9 +15,10 @@ describe('Applications', () => {
vm.$destroy();
});
describe('', () => {
describe('Project cluster applications', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
type: CLUSTER_TYPE.PROJECT,
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
......@@ -30,31 +32,76 @@ describe('Applications', () => {
});
it('renders a row for Helm Tiller', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeDefined();
expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
});
it('renders a row for Ingress', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined();
expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
});
it('renders a row for Cert-Manager', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).toBeDefined();
expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
});
it('renders a row for Prometheus', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined();
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
});
it('renders a row for Jupyter', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
});
it('renders a row for Knative', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null);
expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
});
});
describe('Group cluster applications', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
type: CLUSTER_TYPE.GROUP,
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
cert_manager: { title: 'Cert-Manager' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub' },
knative: { title: 'Knative' },
},
});
});
it('renders a row for Helm Tiller', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
});
it('renders a row for Ingress', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
});
it('renders a row for Cert-Manager', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
});
it('renders a row for Prometheus', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull();
});
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeNull();
});
it('renders a row for Jupyter', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).toBeNull();
});
it('renders a row for Knative', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull();
});
});
......
......@@ -26,6 +26,28 @@ describe Gitlab::Gpg::Commit do
end
end
context 'invalid signature' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
before do
allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
# Corrupt the key
GpgHelpers::User1.signed_commit_signature.tr('=', 'a'),
GpgHelpers::User1.signed_commit_base_data
]
)
end
it 'returns nil' do
expect(described_class.new(commit).signature).to be_nil
end
end
context 'known key' do
context 'user matches the key uid' do
context 'user email matches the email committer' do
......
......@@ -4,12 +4,10 @@ describe OmniAuth::Strategies::Jwt do
include Rack::Test::Methods
include DeviseHelpers
context '.decoded' do
let(:strategy) { described_class.new({}) }
context '#decoded' do
subject { described_class.new({}) }
let(:timestamp) { Time.now.to_i }
let(:jwt_config) { Devise.omniauth_configs[:jwt] }
let(:key) { JWT.encode(claims, jwt_config.strategy.secret) }
let(:claims) do
{
id: 123,
......@@ -18,19 +16,55 @@ describe OmniAuth::Strategies::Jwt do
iat: timestamp
}
end
let(:algorithm) { 'HS256' }
let(:secret) { jwt_config.strategy.secret }
let(:private_key) { secret }
let(:payload) { JWT.encode(claims, private_key, algorithm) }
before do
allow_any_instance_of(OmniAuth::Strategy).to receive(:options).and_return(jwt_config.strategy)
allow_any_instance_of(Rack::Request).to receive(:params).and_return({ 'jwt' => key })
subject.options[:secret] = secret
subject.options[:algorithm] = algorithm
expect_next_instance_of(Rack::Request) do |rack_request|
expect(rack_request).to receive(:params).and_return('jwt' => payload)
end
end
it 'decodes the user information' do
result = strategy.decoded
ECDSA_NAMED_CURVES = {
'ES256' => 'prime256v1',
'ES384' => 'secp384r1',
'ES512' => 'secp521r1'
}.freeze
expect(result["id"]).to eq(123)
expect(result["name"]).to eq("user_example")
expect(result["email"]).to eq("user@example.com")
expect(result["iat"]).to eq(timestamp)
{
OpenSSL::PKey::RSA => %w[RS256 RS384 RS512],
OpenSSL::PKey::EC => %w[ES256 ES384 ES512],
String => %w[HS256 HS384 HS512]
}.each do |private_key_class, algorithms|
algorithms.each do |algorithm|
context "when the #{algorithm} algorithm is used" do
let(:algorithm) { algorithm }
let(:secret) do
if private_key_class == OpenSSL::PKey::RSA
private_key_class.generate(2048)
.to_pem
elsif private_key_class == OpenSSL::PKey::EC
private_key_class.new(ECDSA_NAMED_CURVES[algorithm])
.tap { |key| key.generate_key! }
.to_pem
else
private_key_class.new(jwt_config.strategy.secret)
end
end
let(:private_key) { private_key_class ? private_key_class.new(secret) : secret }
it 'decodes the user information' do
result = subject.decoded
expect(result).to eq(claims.stringify_keys)
end
end
end
end
context 'required claims is missing' do
......@@ -43,7 +77,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
......@@ -57,11 +91,12 @@ describe OmniAuth::Strategies::Jwt do
end
before do
jwt_config.strategy.valid_within = Time.now.to_i
# Omniauth config values are always strings!
subject.options[:valid_within] = 2.days.to_s
end
it 'raises error' do
expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
......@@ -76,11 +111,12 @@ describe OmniAuth::Strategies::Jwt do
end
before do
jwt_config.strategy.valid_within = 2.seconds
# Omniauth config values are always strings!
subject.options[:valid_within] = 2.seconds.to_s
end
it 'raises error' do
expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
end
......
......@@ -1925,7 +1925,7 @@ describe Ci::Build do
context 'when token is empty' do
before do
build.token = nil
build.update_columns(token: nil, token_encrypted: nil)
end
it { is_expected.to be_nil}
......@@ -2141,7 +2141,7 @@ describe Ci::Build do
end
before do
build.token = 'my-token'
build.set_token('my-token')
build.yaml_variables = []
end
......
......@@ -351,3 +351,89 @@ describe PersonalAccessToken, 'TokenAuthenticatable' do
end
end
end
describe Ci::Build, 'TokenAuthenticatable' do
let(:token_field) { :token }
let(:build) { FactoryBot.build(:ci_build) }
it_behaves_like 'TokenAuthenticatable'
describe 'generating new token' do
context 'token is not generated yet' do
describe 'token field accessor' do
it 'makes it possible to access token' do
expect(build.token).to be_nil
build.save!
expect(build.token).to be_present
end
end
describe "ensure_token" do
subject { build.ensure_token }
it { is_expected.to be_a String }
it { is_expected.not_to be_blank }
it 'does not persist token' do
expect(build).not_to be_persisted
end
end
describe 'ensure_token!' do
it 'persists a new token' do
expect(build.ensure_token!).to eq build.reload.token
expect(build).to be_persisted
end
it 'persists new token as an encrypted string' do
build.ensure_token!
encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token)
expect(build.read_attribute('token_encrypted')).to eq encrypted
end
it 'does not persist a token in a clear text' do
build.ensure_token!
expect(build.read_attribute('token')).to be_nil
end
end
end
describe '#reset_token!' do
it 'persists a new token' do
build.save!
build.token.yield_self do |previous_token|
build.reset_token!
expect(build.token).not_to eq previous_token
expect(build.token).to be_a String
end
end
end
end
describe 'setting a new token' do
subject { build.set_token('0123456789') }
it 'returns the token' do
expect(subject).to eq '0123456789'
end
it 'writes a new encrypted token' do
expect(build.read_attribute('token_encrypted')).to be_nil
expect(subject).to eq '0123456789'
expect(build.read_attribute('token_encrypted')).to be_present
end
it 'does not write a new cleartext token' do
expect(build.read_attribute('token')).to be_nil
expect(subject).to eq '0123456789'
expect(build.read_attribute('token')).to be_nil
end
end
end
......@@ -76,7 +76,7 @@ describe Group do
before do
group.add_developer(user)
sub_group.add_developer(user)
sub_group.add_maintainer(user)
end
it 'also gets notification settings from parent groups' do
......@@ -498,7 +498,7 @@ describe Group do
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a)
deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
......
......@@ -53,6 +53,29 @@ describe Member do
expect(member).to be_valid
end
end
context "when a child member inherits its access level" do
let(:user) { create(:user) }
let(:member) { create(:group_member, :developer, user: user) }
let(:child_group) { create(:group, parent: member.group) }
let(:child_member) { build(:group_member, group: child_group, user: user) }
it "requires a higher level" do
child_member.access_level = GroupMember::REPORTER
child_member.validate
expect(child_member).not_to be_valid
end
it "is valid with a higher level" do
child_member.access_level = GroupMember::MAINTAINER
child_member.validate
expect(child_member).to be_valid
end
end
end
describe 'Scopes & finders' do
......
......@@ -50,4 +50,26 @@ describe GroupMember do
group_member.destroy
end
end
context 'access levels', :nested_groups do
context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do
let(:entity) { create(:group, parent: parent_entity) }
end
end
context 'with parent group and a sub subgroup' do
it_behaves_like 'inherited access level as a member of entity' do
let(:subgroup) { create(:group, parent: parent_entity) }
let(:entity) { create(:group, parent: subgroup) }
end
context 'when only the subgroup has the member' do
it_behaves_like 'inherited access level as a member of entity' do
let(:parent_entity) { create(:group, parent: create(:group)) }
let(:entity) { create(:group, parent: parent_entity) }
end
end
end
end
end
......@@ -124,4 +124,19 @@ describe ProjectMember do
end
it_behaves_like 'members notifications', :project
context 'access levels' do
context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do
let(:entity) { create(:project, group: parent_entity) }
end
end
context 'with parent group and a subgroup', :nested_groups do
it_behaves_like 'inherited access level as a member of entity' do
let(:subgroup) { create(:group, parent: parent_entity) }
let(:entity) { create(:project, group: subgroup) }
end
end
end
end
......@@ -538,7 +538,7 @@ describe Namespace do
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a)
deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
......
......@@ -2325,11 +2325,11 @@ describe User do
context 'user is member of all groups' do
before do
group.add_owner(user)
nested_group_1.add_owner(user)
nested_group_1_1.add_owner(user)
nested_group_2.add_owner(user)
nested_group_2_1.add_owner(user)
group.add_reporter(user)
nested_group_1.add_developer(user)
nested_group_1_1.add_maintainer(user)
nested_group_2.add_developer(user)
nested_group_2_1.add_maintainer(user)
end
it 'returns all groups' do
......
......@@ -135,4 +135,12 @@ describe GroupMemberPresenter do
end
end
end
it_behaves_like '#valid_level_roles', :group do
let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Owner' => 50, 'Reporter' => 20 } }
before do
entity.parent = group
end
end
end
......@@ -135,4 +135,10 @@ describe ProjectMemberPresenter do
end
end
end
it_behaves_like '#valid_level_roles', :project do
before do
entity.group = group
end
end
end
......@@ -224,6 +224,37 @@ describe API::Members do
end
end
context 'access levels' do
it 'does not create the member if group level is higher', :nested_groups do
parent = create(:group)
group.update(parent: parent)
project.update(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: stranger.id, access_level: Member::REPORTER
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
end
it 'creates the member if group level is lower', :nested_groups do
parent = create(:group)
group.update(parent: parent)
project.update(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: stranger.id, access_level: Member::MAINTAINER
expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::MAINTAINER)
end
end
it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: maintainer.id, access_level: Member::MAINTAINER
......
......@@ -1906,7 +1906,7 @@ describe API::Projects do
let(:group) { create(:group) }
let(:group2) do
group = create(:group, name: 'group2_name')
group.add_owner(user2)
group.add_maintainer(user2)
group
end
......
......@@ -20,9 +20,9 @@ describe Ci::RetryBuildService do
CLONE_ACCESSORS = described_class::CLONE_ACCESSORS
REJECT_ACCESSORS =
%i[id status user token coverage trace runner artifacts_expire_at
artifacts_file artifacts_metadata artifacts_size created_at
updated_at started_at finished_at queued_at erased_by
%i[id status user token token_encrypted coverage trace runner
artifacts_expire_at artifacts_file artifacts_metadata artifacts_size
created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
......
# frozen_string_literal: true
shared_examples_for 'inherited access level as a member of entity' do
let(:parent_entity) { create(:group) }
let(:user) { create(:user) }
let(:member) { entity.is_a?(Group) ? entity.group_member(user) : entity.project_member(user) }
context 'with root parent_entity developer member' do
before do
parent_entity.add_developer(user)
end
it 'is allowed to be a maintainer of the entity' do
entity.add_maintainer(user)
expect(member.access_level).to eq(Gitlab::Access::MAINTAINER)
end
it 'is not allowed to be a reporter of the entity' do
entity.add_reporter(user)
expect(member).to be_nil
end
it 'is allowed to change to be a developer of the entity' do
entity.add_maintainer(user)
expect { member.update(access_level: Gitlab::Access::DEVELOPER) }
.to change { member.access_level }.to(Gitlab::Access::DEVELOPER)
end
it 'is not allowed to change to be a guest of the entity' do
entity.add_maintainer(user)
expect { member.update(access_level: Gitlab::Access::GUEST) }
.not_to change { member.reload.access_level }
end
it "shows an error if the member can't be updated" do
entity.add_maintainer(user)
member.update(access_level: Gitlab::Access::REPORTER)
expect(member.errors.full_messages).to eq(["Access level should be higher than Developer inherited membership from group #{parent_entity.name}"])
end
it 'allows changing the level from a non existing member' do
non_member_user = create(:user)
entity.add_maintainer(non_member_user)
non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user)
expect { non_member.update(access_level: Gitlab::Access::GUEST) }
.to change { non_member.reload.access_level }
end
end
end
shared_examples_for '#valid_level_roles' do |entity_name|
let(:member_user) { create(:user) }
let(:group) { create(:group) }
let(:entity) { create(entity_name) }
let(:entity_member) { create("#{entity_name}_member", :developer, source: entity, user: member_user) }
let(:presenter) { described_class.new(entity_member, current_user: member_user) }
let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Reporter' => 20 } }
it 'returns all roles when no parent member is present' do
expect(presenter.valid_level_roles).to eq(entity_member.class.access_level_roles)
end
it 'returns higher roles when a parent member is present' do
group.add_reporter(member_user)
expect(presenter.valid_level_roles).to eq(expected_roles)
end
end
......@@ -19,7 +19,7 @@ describe RebaseWorker, '#perform' do
expect(MergeRequests::RebaseService)
.to receive(:new).with(forked_project, merge_request.author).and_call_original
subject.perform(merge_request, merge_request.author)
subject.perform(merge_request.id, merge_request.author.id)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment