diff options
author | GitLab Release Tools Bot <robert+release-tools@gitlab.com> | 2018-12-06 18:18:31 +0300 |
---|---|---|
committer | GitLab Release Tools Bot <robert+release-tools@gitlab.com> | 2018-12-06 18:18:31 +0300 |
commit | b4146c7000231f4a8c71598dcf905d40ea95ab4e (patch) | |
tree | 7b8849f1d9fb2c20a9c892f138746a5e7f755b75 | |
parent | d41d1b0f126935a76f4b777eba4ca7744f6a45ee (diff) | |
parent | 17169a020f6e58149a2a7d336d64f7112b27511d (diff) |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
65 files changed, 803 insertions, 137 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 26aaba0e866..bd8bf882d06 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.2.0 +1.7.0 diff --git a/Gemfile.lock b/Gemfile.lock index 699d77615aa..f51eaef9357 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock index 15e0b782d5b..461550f7ffb 100644 --- a/Gemfile.rails4.lock +++ b/Gemfile.rails4.lock @@ -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) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index f8dbe412f80..de003e70e61 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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); diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 31651658fe6..d899b7fbd8c 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -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" /> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 9a96d0fa6d7..665a9c77822 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -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:') }} diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 5164d87c5fa..533e90e2222 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -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> diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index fbf944499d5..6351948f750 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -1,5 +1,6 @@ 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: { diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index e4852c85378..14c02218990 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -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 = [ diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4041f2b4479..834e7ffce81 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -65,3 +65,4 @@ @import 'framework/feature_highlight'; @import 'framework/terms'; @import 'framework/read_more'; +@import 'framework/flex_grid'; diff --git a/app/assets/stylesheets/framework/flex_grid.scss b/app/assets/stylesheets/framework/flex_grid.scss new file mode 100644 index 00000000000..10537fd5549 --- /dev/null +++ b/app/assets/stylesheets/framework/flex_grid.scss @@ -0,0 +1,52 @@ +.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; + } +} diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb new file mode 100644 index 00000000000..8e50bbc6c04 --- /dev/null +++ b/app/helpers/ide_helper.rb @@ -0,0 +1,16 @@ +# 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 diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d60861dc95f..d86a6eceb59 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -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 diff --git a/app/models/member.rb b/app/models/member.rb index bc8ac14d148..9fc95ea00c3 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 587bada469e..1adcb73806d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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? diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb index d963c188559..ef6bbc0d109 100644 --- a/app/presenters/group_clusterable_presenter.rb +++ b/app/presenters/group_clusterable_presenter.rb @@ -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 diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb index 2497bea4aff..9e9b6973b8e 100644 --- a/app/presenters/member_presenter.rb +++ b/app/presenters/member_presenter.rb @@ -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) diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml new file mode 100644 index 00000000000..b24d6e27536 --- /dev/null +++ b/app/views/ide/_show.html.haml @@ -0,0 +1,10 @@ +- @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...') diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index d8bd37fe986..0323f9d093d 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -1,17 +1 @@ -- @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' diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index a7fd75d85d7..6b3841ebbc4 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -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), diff --git a/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml b/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml new file mode 100644 index 00000000000..96f33a72cc5 --- /dev/null +++ b/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml @@ -0,0 +1,5 @@ +--- +title: Restrict member access level to be higher than that of any parent group +merge_request: 23226 +author: +type: fixed diff --git a/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml b/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml new file mode 100644 index 00000000000..3ef564238c5 --- /dev/null +++ b/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml @@ -0,0 +1,5 @@ +--- +title: Support RSA and ECDSA algorithms in Omniauth JWT provider +merge_request: 23411 +author: Michael Tsyganov +type: fixed diff --git a/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml b/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml new file mode 100644 index 00000000000..4673ba38bae --- /dev/null +++ b/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml @@ -0,0 +1,5 @@ +--- +title: Add partial index for ci_builds on project_id and status +merge_request: 23268 +author: +type: performance diff --git a/changelogs/unreleased/deprecated-instance-find.yml b/changelogs/unreleased/deprecated-instance-find.yml new file mode 100644 index 00000000000..d2ba821e124 --- /dev/null +++ b/changelogs/unreleased/deprecated-instance-find.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix deprecation: You are passing an instance of ActiveRecord::Base to' +merge_request: 23369 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml new file mode 100644 index 00000000000..04fc88bc3d3 --- /dev/null +++ b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml @@ -0,0 +1,5 @@ +--- +title: Encrypt CI/CD builds authentication tokens +merge_request: 23436 +author: +type: security diff --git a/changelogs/unreleased/gt-add-top-padding-for-nested-environment-items-loading-icon.yml b/changelogs/unreleased/gt-add-top-padding-for-nested-environment-items-loading-icon.yml new file mode 100644 index 00000000000..606314b5780 --- /dev/null +++ b/changelogs/unreleased/gt-add-top-padding-for-nested-environment-items-loading-icon.yml @@ -0,0 +1,5 @@ +--- +title: Add top padding for nested environment items loading icon +merge_request: 23580 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/mg-fix-knative-application-row.yml b/changelogs/unreleased/mg-fix-knative-application-row.yml new file mode 100644 index 00000000000..95142d380a4 --- /dev/null +++ b/changelogs/unreleased/mg-fix-knative-application-row.yml @@ -0,0 +1,5 @@ +--- +title: Hide Knative from group cluster applications until supported +merge_request: 23577 +author: +type: fixed diff --git a/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml b/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml new file mode 100644 index 00000000000..185e2547e16 --- /dev/null +++ b/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml @@ -0,0 +1,5 @@ +--- +title: Gracefully handle unknown/invalid GPG keys +merge_request: 23492 +author: +type: fixed diff --git a/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml b/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml new file mode 100644 index 00000000000..18f7da56edb --- /dev/null +++ b/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml @@ -0,0 +1,5 @@ +--- +title: Remove close icon from projects dropdown in issue boards +merge_request: 23567 +author: +type: changed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 58b7c248aaf..1c16b999e55 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -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', diff --git a/db/fixtures/development/24_forks.rb b/db/fixtures/development/24_forks.rb new file mode 100644 index 00000000000..61e39c871e6 --- /dev/null +++ b/db/fixtures/development/24_forks.rb @@ -0,0 +1,16 @@ +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 diff --git a/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb index c2b5b239279..03f677a4678 100644 --- a/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb +++ b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb @@ -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 diff --git a/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb b/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb new file mode 100644 index 00000000000..5b47a279438 --- /dev/null +++ b/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb @@ -0,0 +1,33 @@ +# 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 diff --git a/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb b/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb new file mode 100644 index 00000000000..a0a02e81323 --- /dev/null +++ b/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb @@ -0,0 +1,33 @@ +# 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 diff --git a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb new file mode 100644 index 00000000000..11b98203793 --- /dev/null +++ b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb @@ -0,0 +1,11 @@ +# 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 diff --git a/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb new file mode 100644 index 00000000000..f90aca008e5 --- /dev/null +++ b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb @@ -0,0 +1,17 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index 65a69c2850c..5bc7c7c71fc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 373d4239f71..54be7b616cc 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -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 diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md index 8b00f52ffc1..497298503ad 100644 --- a/doc/administration/auth/jwt.md +++ b/doc/administration/auth/jwt.md @@ -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 diff --git a/doc/development/profiling.md b/doc/development/profiling.md index 0ca8bb67a77..0b0c6dfc8cf 100644 --- a/doc/development/profiling.md +++ b/doc/development/profiling.md @@ -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 diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md index 8d9706a9501..d685cacf9ea 100644 --- a/doc/development/testing_guide/ci.md +++ b/doc/development/testing_guide/ci.md @@ -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 diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 31bab20b044..4fbb87385c3 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -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 diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 93065879ec6..7cdea9d1ce4 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -142,6 +142,7 @@ excluded_attributes: statuses: - :trace - :token + - :token_encrypted - :when - :artifacts_file - :artifacts_metadata diff --git a/lib/omni_auth/strategies/jwt.rb b/lib/omni_auth/strategies/jwt.rb index a792903fde7..2f3d477a591 100644 --- a/lib/omni_auth/strategies/jwt.rb +++ b/lib/omni_auth/strategies/jwt.rb @@ -1,6 +1,7 @@ # 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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f023a9be3eb..23ee90ff0dd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index f545da3aee4..8975ea0f063 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -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) diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 7de38913bae..46f72214831 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -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'; diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 928bf70f3a2..e46edec9abb 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -1,5 +1,6 @@ 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(); }); }); diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 8c6d673391b..8229f0eb794 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -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 diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb index 88d6d0b559a..c2e2db27362 100644 --- a/spec/lib/omni_auth/strategies/jwt_spec.rb +++ b/spec/lib/omni_auth/strategies/jwt_spec.rb @@ -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 diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4cdcae5f670..89f78f629d4 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -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 diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 0cdf430e9ab..55d83bc3a6b 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 0c3a49cd0f2..87aa5a46c21 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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) diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index fca1b1f90d9..188beac1582 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -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 diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 97959ed4304..a3451c67bd8 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -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 diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 334d4f95f53..097b1bb30dc 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -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 diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 6ee19c0ddf4..96561dab1c9 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -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) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e5490e0a156..6cb27246f06 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb index c00e41725d9..bb66523a83d 100644 --- a/spec/presenters/group_member_presenter_spec.rb +++ b/spec/presenters/group_member_presenter_spec.rb @@ -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 diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb index 83db5c56cdf..73ef113a1c5 100644 --- a/spec/presenters/project_member_presenter_spec.rb +++ b/spec/presenters/project_member_presenter_spec.rb @@ -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 diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 93e1c3a2294..bb32d581176 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -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 diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 62b6a3ce42e..e40db55cd20 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -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 diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index e779675744c..87185891470 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb new file mode 100644 index 00000000000..77376496854 --- /dev/null +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -0,0 +1,77 @@ +# 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 diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb index 936b9deaecc..900332ed6b3 100644 --- a/spec/workers/rebase_worker_spec.rb +++ b/spec/workers/rebase_worker_spec.rb @@ -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 |