diff options
106 files changed, 1891 insertions, 638 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d41e5c8642f..d1e324c5518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.5.3 (2018-12-06) + +### Security (1 change) + +- Prevent a path traversal attack on global file templates. + + ## 11.5.2 (2018-12-03) ### Removed (1 change) @@ -621,6 +628,13 @@ entry. - Check frozen string in style builds. (gfyoung) +## 11.3.12 (2018-12-06) + +### Security (1 change) + +- Prevent a path traversal attack on global file templates. + + ## 11.3.11 (2018-11-26) ### Security (33 changes) 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/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/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 8ea34f5d19d..bb6b6f84849 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -259,6 +259,16 @@ ul.related-merge-requests > li { display: block; } +.issue-sort-dropdown { + .btn-group { + width: 100%; + } + + .reverse-sort-btn { + color: $gl-text-color-secondary; + } +} + @include media-breakpoint-up(sm) { .emoji-block .row { display: flex; diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 0837599977f..9aa98e2ca1f 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -167,12 +167,6 @@ module IssuableCollections case value when 'id_asc' then sort_value_oldest_created when 'id_desc' then sort_value_recently_created - when 'created_asc' then sort_value_created_date - when 'created_desc' then sort_value_created_date - when 'due_date_asc' then sort_value_due_date - when 'due_date_desc' then sort_value_due_date - when 'milestone_due_asc' then sort_value_milestone - when 'milestone_due_desc' then sort_value_milestone when 'downvotes_asc' then sort_value_popularity when 'downvotes_desc' then sort_value_popularity else value diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 74113aee89d..f51b96ba8ce 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -136,6 +136,53 @@ module SortingHelper link_to item, path, class: sorted_by == item ? 'is-active' : '' end + def issuable_sort_option_overrides + { + sort_value_oldest_created => sort_value_created_date, + sort_value_oldest_updated => sort_value_recently_updated, + sort_value_milestone_later => sort_value_milestone + } + end + + def issuable_reverse_sort_order_hash + { + sort_value_created_date => sort_value_oldest_created, + sort_value_recently_created => sort_value_oldest_created, + sort_value_recently_updated => sort_value_oldest_updated, + sort_value_milestone => sort_value_milestone_later + }.merge(issuable_sort_option_overrides) + end + + def issuable_sort_option_title(sort_value) + sort_value = issuable_sort_option_overrides[sort_value] || sort_value + + sort_options_hash[sort_value] + end + + def issuable_sort_direction_button(sort_value) + link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort' + reverse_sort = issuable_reverse_sort_order_hash[sort_value] + + if reverse_sort + reverse_url = page_filter_path(sort: reverse_sort) + else + reverse_url = '#' + link_class += ' disabled' + end + + link_to(reverse_url, type: 'button', class: link_class, title: 'Sort direction') do + icon_suffix = + case sort_value + when sort_value_milestone, sort_value_due_date, /_asc\z/ + 'lowest' + else + 'highest' + end + + sprite_icon("sort-#{icon_suffix}", size: 16) + end + end + # Titles. def sort_title_access_level_asc s_('SortOptions|Access level, ascending') 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/ci/pipeline.rb b/app/models/ci/pipeline.rb index 60ff2181a95..d06022a0fb7 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -605,13 +605,18 @@ module Ci end def predefined_variables - Gitlab::Ci::Variables::Collection.new - .append(key: 'CI_PIPELINE_IID', value: iid.to_s) - .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path) - .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s) - .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s) - .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s) - .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s) + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s) + variables.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path) + variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s) + variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s) + variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s) + variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s) + + if merge_request? && merge_request + variables.concat(merge_request.predefined_variables) + end + end end def queued_duration diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 60b7ec2815c..14bc56f0eee 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -43,14 +43,19 @@ module Awardable end def order_upvotes_desc - order_votes_desc(AwardEmoji::UPVOTE_NAME) + order_votes(AwardEmoji::UPVOTE_NAME, 'DESC') + end + + def order_upvotes_asc + order_votes(AwardEmoji::UPVOTE_NAME, 'ASC') end def order_downvotes_desc - order_votes_desc(AwardEmoji::DOWNVOTE_NAME) + order_votes(AwardEmoji::DOWNVOTE_NAME, 'DESC') end - def order_votes_desc(emoji_name) + # Order votes by emoji, optional sort order param `descending` defaults to true + def order_votes(emoji_name, direction) awardable_table = self.arel_table awards_table = AwardEmoji.arel_table @@ -62,7 +67,7 @@ module Awardable ) ).join_sources - joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC") + joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) #{direction}") end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5080fe03cc8..0d363ec68b7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -145,14 +145,16 @@ module Issuable def sort_by_attribute(method, excluded_labels: []) sorted = case method.to_s - when 'downvotes_desc' then order_downvotes_desc - when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) - when 'milestone' then order_milestone_due_asc - when 'milestone_due_asc' then order_milestone_due_asc - when 'milestone_due_desc' then order_milestone_due_desc - when 'popularity' then order_upvotes_desc - when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) - when 'upvotes_desc' then order_upvotes_desc + when 'downvotes_desc' then order_downvotes_desc + when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) + when 'label_priority_desc' then order_labels_priority('DESC', excluded_labels: excluded_labels) + when 'milestone', 'milestone_due_asc' then order_milestone_due_asc + when 'milestone_due_desc' then order_milestone_due_desc + when 'popularity', 'popularity_desc' then order_upvotes_desc + when 'popularity_asc' then order_upvotes_asc + when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) + when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels) + when 'upvotes_desc' then order_upvotes_desc else order_by(method) end @@ -160,7 +162,7 @@ module Issuable sorted.with_order_id_desc end - def order_due_date_and_labels_priority(excluded_labels: []) + def order_due_date_and_labels_priority(direction = 'ASC', excluded_labels: []) # The order_ methods also modify the query in other ways: # # - For milestones, we add a JOIN. @@ -177,11 +179,11 @@ module Issuable order_milestone_due_asc .order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]) - .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'), - Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, direction), + Gitlab::Database.nulls_last_order('highest_priority', direction)) end - def order_labels_priority(excluded_labels: [], extra_select_columns: []) + def order_labels_priority(direction = 'ASC', excluded_labels: [], extra_select_columns: []) params = { target_type: name, target_column: "#{table_name}.id", @@ -198,7 +200,7 @@ module Issuable select(select_columns.join(', ')) .group(arel_table[:id]) - .reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + .reorder(Gitlab::Database.nulls_last_order('highest_priority', direction)) end def with_label(title, sort = nil) 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/merge_request.rb b/app/models/merge_request.rb index f40dff7c1bd..d0811a715bc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1070,6 +1070,42 @@ class MergeRequest < ActiveRecord::Base actual_head_pipeline&.has_test_reports? end + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s) + variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s) + + variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', + value: ref_path.to_s) + + variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', + value: project.id.to_s) + + variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', + value: project.full_path) + + variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', + value: project.web_url) + + variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', + value: target_branch.to_s) + + if source_project + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID', + value: source_project.id.to_s) + + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', + value: source_project.full_path) + + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', + value: source_project.web_url) + + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', + value: source_branch.to_s) + end + end + end + # rubocop: disable CodeReuse/ServiceClass def compare_test_reports unless has_test_reports? 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/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/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml index 3effdf934fb..293a2e3ebfe 100644 --- a/app/views/projects/mirrors/_authentication_method.html.haml +++ b/app/views/projects/mirrors/_authentication_method.html.haml @@ -8,14 +8,14 @@ = f.label :auth_method, _('Authentication method'), class: 'label-bold' = f.select :auth_method, options_for_select(auth_options, mirror.auth_method), - {}, { class: "form-control js-mirror-auth-type" } + {}, { class: "form-control js-mirror-auth-type qa-authentication-method" } .form-group .collapse.js-well-changing-auth .changing-auth-method= icon('spinner spin lg') .well-password-auth.collapse.js-well-password-auth = f.label :password, _("Password"), class: "label-bold" - = f.password_field :password, value: mirror.password, class: 'form-control', autocomplete: 'new-password' + = f.password_field :password, value: mirror.password, class: 'form-control qa-password', autocomplete: 'new-password' - unless is_push .well-ssh-auth.collapse.js-well-ssh-auth %p.js-ssh-public-key-present{ class: ('collapse' unless ssh_public_key_present) } diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index dde0fae740b..21b105e6f80 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -1,7 +1,7 @@ - expanded = Rails.env.test? - protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') -%section.settings.project-mirror-settings.js-mirror-settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) } +%section.settings.project-mirror-settings.js-mirror-settings.no-animate.qa-mirroring-repositories-settings#js-push-remote-settings{ class: ('expanded' if expanded) } .settings-header %h4= _('Mirroring repositories') %button.btn.js-settings-toggle @@ -20,7 +20,7 @@ .form-group.has-feedback = label_tag :url, _('Git repository URL'), class: 'label-light' - = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+" + = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+" = render 'projects/mirrors/instructions' @@ -32,7 +32,7 @@ = link_to icon('question-circle'), help_page_path('user/project/protected_branches') .panel-footer - = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit', name: :update_remote_mirror + = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror .panel.panel-default .table-responsive @@ -50,10 +50,10 @@ = render_if_exists 'projects/mirrors/table_pull_row' - @project.remote_mirrors.each_with_index do |mirror, index| - if mirror.enabled - %tr - %td= mirror.safe_url + %tr.qa-mirrored-repository-row + %td.qa-mirror-repository-url= mirror.safe_url %td= _('Push') - %td= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') + %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') %td - if mirror.last_error.present? .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error') diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml index a2cce83bfab..b49f1d9315e 100644 --- a/app/views/projects/mirrors/_mirror_repos_form.html.haml +++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml @@ -1,5 +1,5 @@ .form-group = label_tag :mirror_direction, _('Mirror direction'), class: 'label-light' - = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction', disabled: true + = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction qa-mirror-direction', disabled: true = render partial: "projects/mirrors/mirror_repos_push", locals: { f: f } diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index a8d4d4af93a..2a602095845 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,7 +1,7 @@ - project = find_project_for_result_blob(blob) - return unless project -- file_name, blob = parse_search_result(blob) -- blob_link = project_blob_path(project, tree_join(blob.ref, file_name)) +- blob = parse_search_result(blob) +- blob_link = project_blob_path(project, tree_join(blob.ref, blob.filename)) -= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: file_name, blob_link: blob_link } += render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: blob.filename, blob_link: blob_link } diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index 4346217c230..389e4cc75b9 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,5 +1,5 @@ - project = find_project_for_result_blob(wiki_blob) -- file_name, wiki_blob = parse_search_result(wiki_blob) +- wiki_blob = parse_search_result(wiki_blob) - wiki_blob_link = project_wiki_path(project, wiki_blob.basename) -= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: file_name, blob_link: wiki_blob_link } += render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link } diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml index f32cff18fa8..721a2af8069 100644 --- a/app/views/shared/_remote_mirror_update_button.html.haml +++ b/app/views/shared/_remote_mirror_update_button.html.haml @@ -2,5 +2,5 @@ %button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') } = icon("refresh spin") - else - = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do + = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do = icon("refresh") diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml deleted file mode 100644 index e4463c1e0d8..00000000000 --- a/app/views/shared/_sort_dropdown.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- sorted_by = sort_options_hash[@sort] -- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues' - -.dropdown.inline.prepend-left-10 - %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } - = sorted_by - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort - %li - = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by) - = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) - = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by) - = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sorted_by) - = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sorted_by) if viewing_issues - = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sorted_by) - = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sorted_by) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml new file mode 100644 index 00000000000..2ca4657851c --- /dev/null +++ b/app/views/shared/issuable/_filter.html.haml @@ -0,0 +1,32 @@ +.issues-filters + .issues-details-filters.row-content-block.second-block + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + .issues-other-filters + .filter-item.inline + - if params[:author_id].present? + = hidden_field_tag(:author_id, params[:author_id]) + = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", + placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) + + .filter-item.inline + - if params[:assignee_id].present? + = hidden_field_tag(:assignee_id, params[:assignee_id]) + = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) + + .filter-item.inline.milestone-filter + = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true + + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } + + - unless @no_filters_set + .float-right + = render 'shared/issuable/sort_dropdown' + + - has_labels = @labels && @labels.any? + .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } + - if has_labels + = render 'shared/labels_row', labels: @labels diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 7c5af0b9775..46634693067 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -2,7 +2,6 @@ - board = local_assigns.fetch(:board, nil) - block_css_class = type != :boards_modal ? 'row-content-block second-block' : '' - user_can_admin_list = board && can?(current_user, :admin_list, board.parent) -- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true) .issues-filters .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal } @@ -142,5 +141,5 @@ - if @project #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } #js-toggle-focus-btn - - elsif show_sorting_dropdown - = render 'shared/sort_dropdown' + - elsif type != :boards_modal + = render 'shared/issuable/sort_dropdown' diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml new file mode 100644 index 00000000000..c211b9fcaa2 --- /dev/null +++ b/app/views/shared/issuable/_sort_dropdown.html.haml @@ -0,0 +1,20 @@ +- sort_value = @sort +- sort_title = issuable_sort_option_title(sort_value) +- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues' + +.dropdown.inline.prepend-left-10.issue-sort-dropdown + .btn-group{ role: 'group' } + .btn-group{ role: 'group' } + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' } + = sort_title + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort + %li + = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sort_title) + = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sort_title) + = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sort_title) + = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sort_title) + = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sort_title) if viewing_issues + = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sort_title) + = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sort_title) + = issuable_sort_direction_button(sort_value) 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/39849_controller_sorts.yml b/changelogs/unreleased/39849_controller_sorts.yml new file mode 100644 index 00000000000..5fad0cb4ede --- /dev/null +++ b/changelogs/unreleased/39849_controller_sorts.yml @@ -0,0 +1,5 @@ +--- +title: Allow sorting issues and MRs in reverse order +merge_request: 21438 +author: +type: changed 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/expose-mr-pipeline-variables.yml b/changelogs/unreleased/expose-mr-pipeline-variables.yml new file mode 100644 index 00000000000..b77b9a69d5c --- /dev/null +++ b/changelogs/unreleased/expose-mr-pipeline-variables.yml @@ -0,0 +1,5 @@ +--- +title: Expose merge request pipeline variables +merge_request: 23398 +author: +type: changed 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/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/remove-blob-search-limit.yml b/changelogs/unreleased/remove-blob-search-limit.yml new file mode 100644 index 00000000000..5bad3a83dbb --- /dev/null +++ b/changelogs/unreleased/remove-blob-search-limit.yml @@ -0,0 +1,5 @@ +--- +title: Remove limit of 100 when searching repository code. +merge_request: 8671 +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/usage-count.yml b/changelogs/unreleased/usage-count.yml new file mode 100644 index 00000000000..efff2615ce4 --- /dev/null +++ b/changelogs/unreleased/usage-count.yml @@ -0,0 +1,5 @@ +--- +title: Use approximate count for big tables for usage statistics. +merge_request: +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/api/search.md b/doc/api/search.md index 9716f682ace..a9369930003 100644 --- a/doc/api/search.md +++ b/doc/api/search.md @@ -722,6 +722,17 @@ Example response: ### Scope: wiki_blobs +Wiki blobs searches are performed on both filenames and contents. Search +results: + +- Found in filenames are displayed before results found in contents. +- May contain multiple matches for the same blob because the search string + might be found in both the filename and content, and matches of the different +types are displayed separately. +- May contain multiple matches for the same blob because the search string + might be found if the search string appears multiple times in the content. + + ```bash curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=wiki_blobs&search=bye ``` @@ -783,6 +794,15 @@ Filters are available for this scope: to use a filter simply include it in your query like so: `a query filename:some_name*`. +Blobs searches are performed on both filenames and contents. Search results: + +- Found in filenames are displayed before results found in contents. +- May contain multiple matches for the same blob because the search string + might be found in both the filename and content, and matches of the different +types are displayed separately. +- May contain multiple matches for the same blob because the search string + might be found if the search string appears multiple times in the content. + You may use wildcards (`*`) to use glob matching. ```bash 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/api/search.rb b/lib/api/search.rb index 5900e1cccc2..f5db692afe5 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -35,12 +35,7 @@ module API end def process_results(results) - case params[:scope] - when 'blobs', 'wiki_blobs' - paginate(results).map { |blob| blob[1] } - else - paginate(results) - end + paginate(results) end def snippets? diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb index c996d786909..f3d37ccd72a 100644 --- a/lib/gitlab/database/count.rb +++ b/lib/gitlab/database/count.rb @@ -40,7 +40,7 @@ module Gitlab if strategy.enabled? models_with_missing_counts = models - counts_by_model.keys - break if models_with_missing_counts.empty? + break counts_by_model if models_with_missing_counts.empty? counts = strategy.new(models_with_missing_counts).count diff --git a/lib/gitlab/database/count/exact_count_strategy.rb b/lib/gitlab/database/count/exact_count_strategy.rb index 0276fe2b54f..fa6951eda22 100644 --- a/lib/gitlab/database/count/exact_count_strategy.rb +++ b/lib/gitlab/database/count/exact_count_strategy.rb @@ -20,6 +20,8 @@ module Gitlab models.each_with_object({}) do |model, data| data[model] = model.count end + rescue *CONNECTION_ERRORS + {} end def self.enabled? diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb index b4db3f93c9c..3958814208c 100644 --- a/lib/gitlab/file_finder.rb +++ b/lib/gitlab/file_finder.rb @@ -4,8 +4,6 @@ # the result is joined and sorted by file name module Gitlab class FileFinder - BATCH_SIZE = 100 - attr_reader :project, :ref delegate :repository, to: :project @@ -16,60 +14,35 @@ module Gitlab end def find(query) - query = Gitlab::Search::Query.new(query) do - filter :filename, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}$/i } - filter :path, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}/i } - filter :extension, matcher: ->(filter, blob) { blob.filename =~ /\.#{filter[:regex_value]}$/i } + query = Gitlab::Search::Query.new(query, encode_binary: true) do + filter :filename, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}$/i } + filter :path, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}/i } + filter :extension, matcher: ->(filter, blob) { blob.binary_filename =~ /\.#{filter[:regex_value]}$/i } end - by_content = find_by_content(query.term) - - already_found = Set.new(by_content.map(&:filename)) - by_filename = find_by_filename(query.term, except: already_found) + files = find_by_filename(query.term) + find_by_content(query.term) - files = (by_content + by_filename) - .sort_by(&:filename) + files = query.filter_results(files) if query.filters.any? - query.filter_results(files).map { |blob| [blob.filename, blob] } + files end private def find_by_content(query) - results = repository.search_files_by_content(query, ref).first(BATCH_SIZE) - results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result, project) } - end - - def find_by_filename(query, except: []) - filenames = search_filenames(query, except) - - blobs(filenames).map do |blob| - Gitlab::SearchResults::FoundBlob.new( - id: blob.id, - filename: blob.path, - basename: File.basename(blob.path, File.extname(blob.path)), - ref: ref, - startline: 1, - data: blob.data, - project: project - ) + repository.search_files_by_content(query, ref).map do |result| + Gitlab::Search::FoundBlob.new(content_match: result, project: project, ref: ref, repository: repository) end end - def search_filenames(query, except) - filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE) - - filenames.delete_if { |filename| except.include?(filename) } unless except.empty? - - filenames - end - - def blob_refs(filenames) - filenames.map { |filename| [ref, filename] } + def find_by_filename(query) + search_filenames(query).map do |filename| + Gitlab::Search::FoundBlob.new(blob_filename: filename, project: project, ref: ref, repository: repository) + end end - def blobs(filenames) - Gitlab::Git::Blob.batch(repository, blob_refs(filenames), blob_size_limit: 1024) + def search_filenames(query) + repository.search_files_by_name(query, ref) end end end 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/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 04df881bf03..a68f8801c2a 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -17,9 +17,9 @@ module Gitlab when 'notes' notes.page(page).per(per_page) when 'blobs' - Kaminari.paginate_array(blobs).page(page).per(per_page) + paginated_blobs(blobs, page) when 'wiki_blobs' - Kaminari.paginate_array(wiki_blobs).page(page).per(per_page) + paginated_blobs(wiki_blobs, page) when 'commits' Kaminari.paginate_array(commits).page(page).per(per_page) else @@ -55,37 +55,6 @@ module Gitlab @commits_count ||= commits.count end - def self.parse_search_result(result, project = nil) - ref = nil - filename = nil - basename = nil - - data = [] - startline = 0 - - result.each_line.each_with_index do |line, index| - prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/)&.tap do |matches| - ref = matches[:ref] - filename = matches[:filename] - startline = matches[:startline] - startline = startline.to_i - index - extname = Regexp.escape(File.extname(filename)) - basename = filename.sub(/#{extname}$/, '') - end - - data << line.sub(prefix.to_s, '') - end - - FoundBlob.new( - filename: filename, - basename: basename, - ref: ref, - startline: startline, - data: data.join, - project: project - ) - end - def single_commit_result? return false if commits_count != 1 @@ -97,6 +66,14 @@ module Gitlab private + def paginated_blobs(blobs, page) + results = Kaminari.paginate_array(blobs).page(page).per(per_page) + + Gitlab::Search::FoundBlob.preload_blobs(results) + + results + end + def blobs return [] unless Ability.allowed?(@current_user, :download_code, @project) diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb new file mode 100644 index 00000000000..a62ab1521a7 --- /dev/null +++ b/lib/gitlab/search/found_blob.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +module Gitlab + module Search + class FoundBlob + include EncodingHelper + include Presentable + include BlobLanguageFromGitAttributes + include Gitlab::Utils::StrongMemoize + + attr_reader :project, :content_match, :blob_filename + + FILENAME_REGEXP = /\A(?<ref>[^:]*):(?<filename>[^\x00]*)\x00/.freeze + CONTENT_REGEXP = /^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze + + def self.preload_blobs(blobs) + to_fetch = blobs.select { |blob| blob.is_a?(self) && blob.blob_filename } + + to_fetch.each { |blob| blob.fetch_blob } + end + + def initialize(opts = {}) + @id = opts.fetch(:id, nil) + @binary_filename = opts.fetch(:filename, nil) + @binary_basename = opts.fetch(:basename, nil) + @ref = opts.fetch(:ref, nil) + @startline = opts.fetch(:startline, nil) + @binary_data = opts.fetch(:data, nil) + @per_page = opts.fetch(:per_page, 20) + @project = opts.fetch(:project, nil) + # Some caller does not have project object (e.g. elastic search), + # yet they can trigger many calls in one go, + # causing duplicated queries. + # Allow those to just pass project_id instead. + @project_id = opts.fetch(:project_id, nil) + @content_match = opts.fetch(:content_match, nil) + @blob_filename = opts.fetch(:blob_filename, nil) + @repository = opts.fetch(:repository, nil) + end + + def id + @id ||= parsed_content[:id] + end + + def ref + @ref ||= parsed_content[:ref] + end + + def startline + @startline ||= parsed_content[:startline] + end + + # binary_filename is used for running filters on all matches, + # for grepped results (which use content_match), we get + # filename from the beginning of the grepped result which is faster + # then parsing whole snippet + def binary_filename + @binary_filename ||= content_match ? search_result_filename : parsed_content[:binary_filename] + end + + def filename + @filename ||= encode_utf8(@binary_filename || parsed_content[:binary_filename]) + end + + def basename + @basename ||= encode_utf8(@binary_basename || parsed_content[:binary_basename]) + end + + def data + @data ||= encode_utf8(@binary_data || parsed_content[:binary_data]) + end + + def path + filename + end + + def project_id + @project_id || @project&.id + end + + def present + super(presenter_class: BlobPresenter) + end + + def fetch_blob + path = [ref, blob_filename] + missing_blob = { binary_filename: blob_filename } + + BatchLoader.for(path).batch(default_value: missing_blob) do |refs, loader| + Gitlab::Git::Blob.batch(repository, refs, blob_size_limit: 1024).each do |blob| + # if the blob couldn't be fetched for some reason, + # show at least the blob filename + data = { + id: blob.id, + binary_filename: blob.path, + binary_basename: File.basename(blob.path, File.extname(blob.path)), + ref: ref, + startline: 1, + binary_data: blob.data, + project: project + } + + loader.call([ref, blob.path], data) + end + end + end + + private + + def search_result_filename + content_match.match(FILENAME_REGEXP) { |matches| matches[:filename] } + end + + def parsed_content + strong_memoize(:parsed_content) do + if content_match + parse_search_result + elsif blob_filename + fetch_blob + else + {} + end + end + end + + def parse_search_result + ref = nil + filename = nil + basename = nil + + data = [] + startline = 0 + + content_match.each_line.each_with_index do |line, index| + prefix ||= line.match(CONTENT_REGEXP)&.tap do |matches| + ref = matches[:ref] + filename = matches[:filename] + startline = matches[:startline] + startline = startline.to_i - index + extname = Regexp.escape(File.extname(filename)) + basename = filename.sub(/#{extname}$/, '') + end + + data << line.sub(prefix.to_s, '') + end + + { + binary_filename: filename, + binary_basename: basename, + ref: ref, + startline: startline, + binary_data: data.join, + project: project + } + end + + def repository + @repository ||= project.repository + end + end + end +end diff --git a/lib/gitlab/search/query.rb b/lib/gitlab/search/query.rb index 7f69083a492..ba0e16607a6 100644 --- a/lib/gitlab/search/query.rb +++ b/lib/gitlab/search/query.rb @@ -3,6 +3,8 @@ module Gitlab module Search class Query < SimpleDelegator + include EncodingHelper + def initialize(query, filter_opts = {}, &block) @raw_query = query.dup @filters = [] @@ -50,7 +52,9 @@ module Gitlab end def parse_filter(filter, input) - filter[:parser].call(input) + result = filter[:parser].call(input) + + @filter_options[:encode_binary] ? encode_binary(result) : result end end end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 458737f31eb..491148ec1a6 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -2,42 +2,6 @@ module Gitlab class SearchResults - class FoundBlob - include EncodingHelper - include Presentable - include BlobLanguageFromGitAttributes - - attr_reader :id, :filename, :basename, :ref, :startline, :data, :project - - def initialize(opts = {}) - @id = opts.fetch(:id, nil) - @filename = encode_utf8(opts.fetch(:filename, nil)) - @basename = encode_utf8(opts.fetch(:basename, nil)) - @ref = opts.fetch(:ref, nil) - @startline = opts.fetch(:startline, nil) - @data = encode_utf8(opts.fetch(:data, nil)) - @per_page = opts.fetch(:per_page, 20) - @project = opts.fetch(:project, nil) - # Some caller does not have project object (e.g. elastic search), - # yet they can trigger many calls in one go, - # causing duplicated queries. - # Allow those to just pass project_id instead. - @project_id = opts.fetch(:project_id, nil) - end - - def path - filename - end - - def project_id - @project_id || @project&.id - end - - def present - super(presenter_class: BlobPresenter) - end - end - attr_reader :current_user, :query, :per_page # Limit search results by passed projects diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index bfcc8efdc96..008e9cd1d24 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -2,6 +2,8 @@ module Gitlab class UsageData + APPROXIMATE_COUNT_MODELS = [Label, MergeRequest, Note, Todo].freeze + class << self def data(force_refresh: false) Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) { uncached_data } @@ -73,12 +75,9 @@ module Gitlab issues: count(Issue), keys: count(Key), label_lists: count(List.label), - labels: count(Label), lfs_objects: count(LfsObject), - merge_requests: count(MergeRequest), milestone_lists: count(List.milestone), milestones: count(Milestone), - notes: count(Note), pages_domains: count(PagesDomain), projects: count(Project), projects_imported_from_github: count(Project.where(import_type: 'github')), @@ -86,10 +85,9 @@ module Gitlab releases: count(Release), remote_mirrors: count(RemoteMirror), snippets: count(Snippet), - todos: count(Todo), uploads: count(Upload), web_hooks: count(WebHook) - }.merge(services_usage) + }.merge(services_usage).merge(approximate_counts) } end # rubocop: enable CodeReuse/ActiveRecord @@ -164,6 +162,16 @@ module Gitlab fallback end # rubocop: enable CodeReuse/ActiveRecord + + def approximate_counts + approx_counts = Gitlab::Database::Count.approximate_counts(APPROXIMATE_COUNT_MODELS) + + APPROXIMATE_COUNT_MODELS.each_with_object({}) do |model, result| + key = model.name.underscore.pluralize.to_sym + + result[key] = approx_counts[model] || -1 + end + end end end end diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb index a00cd65594c..5303b3582ab 100644 --- a/lib/gitlab/wiki_file_finder.rb +++ b/lib/gitlab/wiki_file_finder.rb @@ -2,6 +2,8 @@ module Gitlab class WikiFileFinder < FileFinder + BATCH_SIZE = 100 + attr_reader :repository def initialize(project, ref) @@ -12,13 +14,11 @@ module Gitlab private - def search_filenames(query, except) + def search_filenames(query) safe_query = Regexp.escape(query.tr(' ', '-')) safe_query = Regexp.new(safe_query, Regexp::IGNORECASE) filenames = repository.ls_files(ref) - filenames.delete_if { |filename| except.include?(filename) } unless except.empty? - filenames.grep(safe_query).first(BATCH_SIZE) end end 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 "" @@ -184,6 +184,7 @@ module QA autoload :Runners, 'qa/page/project/settings/runners' autoload :MergeRequest, 'qa/page/project/settings/merge_request' autoload :Members, 'qa/page/project/settings/members' + autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories' end module Issue diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 91e229c4c8c..f4bba3c9560 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -15,7 +15,7 @@ module QA def_delegators :evaluator, :view, :views def refresh - visit current_url + page.refresh end def wait(max: 60, time: 0.1, reload: true) @@ -80,8 +80,8 @@ module QA page.evaluate_script('xhr.status') == 200 end - def find_element(name) - find(element_selector_css(name)) + def find_element(name, wait: Capybara.default_max_wait_time) + find(element_selector_css(name), wait: wait) end def all_elements(name) @@ -100,6 +100,14 @@ module QA find_element(name).set(content) end + def select_element(name, value) + element = find_element(name) + + return if element.text.downcase.to_s == value.to_s + + element.select value.to_s.capitalize + end + def has_element?(name) has_css?(element_selector_css(name)) end @@ -110,6 +118,12 @@ module QA end end + def within_element_by_index(name, index) + page.within all_elements(name)[index] do + yield + end + end + def scroll_to_element(name, *args) scroll_to(element_selector_css(name), *args) end diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb new file mode 100644 index 00000000000..a73be7dfeda --- /dev/null +++ b/qa/qa/page/project/settings/mirroring_repositories.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class MirroringRepositories < Page::Base + view 'app/views/projects/mirrors/_authentication_method.html.haml' do + element :authentication_method + element :password + end + + view 'app/views/projects/mirrors/_mirror_repos.html.haml' do + element :mirror_repository_url_input + element :mirror_repository_button + element :mirror_repository_url + element :mirror_last_update_at + element :mirrored_repository_row + end + + view 'app/views/projects/mirrors/_mirror_repos_form.html.haml' do + element :mirror_direction + end + + view 'app/views/shared/_remote_mirror_update_button.html.haml' do + element :update_now_button + end + + def repository_url=(value) + fill_element :mirror_repository_url_input, value + end + + def password=(value) + fill_element :password, value + end + + def mirror_direction=(value) + raise ArgumentError, "Mirror direction must be :push or :pull" unless [:push, :pull].include? value + + select_element(:mirror_direction, value) + end + + def authentication_method=(value) + raise ArgumentError, "Authentication method must be :password or :none" unless [:password, :none].include? value + + select_element(:authentication_method, value) + end + + def mirror_repository + click_element :mirror_repository_button + end + + def update(url) + row_index = find_repository_row_index url + + within_element_by_index(:mirrored_repository_row, row_index) do + click_element :update_now_button + end + + # Wait a few seconds for the sync to occur and then refresh the page + # so that 'last update' shows 'just now' or a period in seconds + sleep 5 + refresh + + wait(time: 1) do + within_element_by_index(:mirrored_repository_row, row_index) do + last_update = find_element(:mirror_last_update_at, wait: 0) + last_update.has_text?('just now') || last_update.has_text?('seconds') + end + end + + # Fail early if the page still shows that there has been no update + within_element_by_index(:mirrored_repository_row, row_index) do + find_element(:mirror_last_update_at, wait: 0).assert_no_text('Never') + end + end + + private + + def find_repository_row_index(target_url) + all_elements(:mirror_repository_url).index do |url| + # The url might be a sanitized url but the target_url won't be so + # we compare just the paths instead of the full url + URI.parse(url.text).path == target_url.path + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 53ebe28970b..ac0b87aca5e 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -13,6 +13,10 @@ module QA element :protected_branches_settings end + view 'app/views/projects/mirrors/_mirror_repos.html.haml' do + element :mirroring_repositories_settings + end + def expand_deploy_keys(&block) expand_section(:deploy_keys_settings) do DeployKeys.perform(&block) @@ -30,6 +34,12 @@ module QA DeployTokens.perform(&block) end end + + def expand_mirroring_repositories(&block) + expand_section(:mirroring_repositories_settings) do + MirroringRepositories.perform(&block) + end + end end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb new file mode 100644 index 00000000000..2d0e281ab59 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Push mirror a repository over HTTP' do + it 'configures and syncs a (push) mirrored repository' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + target_project = Resource::Project.fabricate! do |project| + project.name = 'push-mirror-target-project' + end + target_project_uri = target_project.repository_http_location.uri + target_project_uri.user = Runtime::User.username + + source_project_push = Resource::Repository::ProjectPush.fabricate! do |push| + push.file_name = 'README.md' + push.file_content = '# This is a test project' + push.commit_message = 'Add README.md' + end + source_project_push.project.visit! + + Page::Project::Show.perform(&:wait_for_push) + + Page::Project::Menu.perform(&:click_repository_settings) + Page::Project::Settings::Repository.perform do |settings| + settings.expand_mirroring_repositories do |mirror_settings| + # Configure the source project to push to the target project + mirror_settings.repository_url = target_project_uri + mirror_settings.mirror_direction = :push + mirror_settings.authentication_method = :password + mirror_settings.password = Runtime::User.password + mirror_settings.mirror_repository + mirror_settings.update target_project_uri + end + end + + # Check that the target project has the commit from the source + target_project.visit! + expect(page).to have_content('README.md') + expect(page).to have_content('This is a test project') + end + end + end +end diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index cf5cd3a79f8..43bc16d8c9a 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -37,8 +37,8 @@ module QA exists end - def find_element(name) - log("finding :#{name}") + def find_element(name, wait: Capybara.default_max_wait_time) + log("finding :#{name} (wait: #{wait})") element = super @@ -71,6 +71,12 @@ module QA super end + def select_element(name, value) + log(%Q(selecting "#{value}" in :#{name})) + + super + end + def has_element?(name) found = super @@ -89,6 +95,16 @@ module QA element end + def within_element_by_index(name, index) + log("within elements :#{name} at index #{index}") + + element = super + + log("end within elements :#{name} at index #{index}") + + element + end + private def log(msg) diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb deleted file mode 100644 index caee7a67aec..00000000000 --- a/spec/features/issuables/default_sort_order_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -require 'spec_helper' - -describe 'Projects > Issuables > Default sort order' do - let(:project) { create(:project, :public) } - - let(:first_created_issuable) { issuables.order_created_asc.first } - let(:last_created_issuable) { issuables.order_created_desc.first } - - let(:first_updated_issuable) { issuables.order_updated_asc.first } - let(:last_updated_issuable) { issuables.order_updated_desc.first } - - context 'for merge requests' do - include MergeRequestHelpers - - let!(:issuables) do - timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, - { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, - { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] - - timestamps.each_with_index do |ts, i| - create issuable_type, { title: "#{issuable_type}_#{i}", - source_branch: "#{issuable_type}_#{i}", - source_project: project }.merge(ts) - end - - MergeRequest.all - end - - context 'in the "merge requests" tab', :js do - let(:issuable_type) { :merge_request } - - it 'is "last created"' do - visit_merge_requests project - - expect(first_merge_request).to include(last_created_issuable.title) - expect(last_merge_request).to include(first_created_issuable.title) - end - end - - context 'in the "merge requests / open" tab', :js do - let(:issuable_type) { :merge_request } - - it 'is "created date"' do - visit_merge_requests_with_state(project, 'open') - - expect(selected_sort_order).to eq('created date') - expect(first_merge_request).to include(last_created_issuable.title) - expect(last_merge_request).to include(first_created_issuable.title) - end - end - - context 'in the "merge requests / merged" tab', :js do - let(:issuable_type) { :merged_merge_request } - - it 'is "last updated"' do - visit_merge_requests_with_state(project, 'merged') - - expect(find('.issues-other-filters')).to have_content('Last updated') - expect(first_merge_request).to include(last_updated_issuable.title) - expect(last_merge_request).to include(first_updated_issuable.title) - end - end - - context 'in the "merge requests / closed" tab', :js do - let(:issuable_type) { :closed_merge_request } - - it 'is "last updated"' do - visit_merge_requests_with_state(project, 'closed') - - expect(find('.issues-other-filters')).to have_content('Last updated') - expect(first_merge_request).to include(last_updated_issuable.title) - expect(last_merge_request).to include(first_updated_issuable.title) - end - end - - context 'in the "merge requests / all" tab', :js do - let(:issuable_type) { :merge_request } - - it 'is "created date"' do - visit_merge_requests_with_state(project, 'all') - - expect(find('.issues-other-filters')).to have_content('Created date') - expect(first_merge_request).to include(last_created_issuable.title) - expect(last_merge_request).to include(first_created_issuable.title) - end - end - end - - context 'for issues' do - include IssueHelpers - - let!(:issuables) do - timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, - { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, - { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] - - timestamps.each_with_index do |ts, i| - create issuable_type, { title: "#{issuable_type}_#{i}", - project: project }.merge(ts) - end - - Issue.all - end - - context 'in the "issues" tab', :js do - let(:issuable_type) { :issue } - - it 'is "created date"' do - visit_issues project - - expect(find('.issues-other-filters')).to have_content('Created date') - expect(first_issue).to include(last_created_issuable.title) - expect(last_issue).to include(first_created_issuable.title) - end - end - - context 'in the "issues / open" tab', :js do - let(:issuable_type) { :issue } - - it 'is "created date"' do - visit_issues_with_state(project, 'open') - - expect(find('.issues-other-filters')).to have_content('Created date') - expect(first_issue).to include(last_created_issuable.title) - expect(last_issue).to include(first_created_issuable.title) - end - end - - context 'in the "issues / closed" tab', :js do - let(:issuable_type) { :closed_issue } - - it 'is "last updated"' do - visit_issues_with_state(project, 'closed') - - expect(find('.issues-other-filters')).to have_content('Last updated') - expect(first_issue).to include(last_updated_issuable.title) - expect(last_issue).to include(first_updated_issuable.title) - end - end - - context 'in the "issues / all" tab', :js do - let(:issuable_type) { :issue } - - it 'is "created date"' do - visit_issues_with_state(project, 'all') - - expect(find('.issues-other-filters')).to have_content('Created date') - expect(first_issue).to include(last_created_issuable.title) - expect(last_issue).to include(first_created_issuable.title) - end - end - - context 'when the sort in the URL is id_desc' do - let(:issuable_type) { :issue } - - before do - visit_issues(project, sort: 'id_desc') - end - - it 'shows the sort order as created date' do - expect(find('.issues-other-filters')).to have_content('Created date') - expect(first_issue).to include(last_created_issuable.title) - expect(last_issue).to include(first_created_issuable.title) - end - end - end - - def selected_sort_order - find('.filter-dropdown-container .dropdown button').text.downcase - end - - def visit_merge_requests_with_state(project, state) - visit_merge_requests project, state: state - end - - def visit_issues_with_state(project, state) - visit_issues project, state: state - end -end diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb new file mode 100644 index 00000000000..0601dd47c03 --- /dev/null +++ b/spec/features/issuables/sorting_list_spec.rb @@ -0,0 +1,226 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'Sort Issuable List' do + let(:project) { create(:project, :public) } + + let(:first_created_issuable) { issuables.order_created_asc.first } + let(:last_created_issuable) { issuables.order_created_desc.first } + + let(:first_updated_issuable) { issuables.order_updated_asc.first } + let(:last_updated_issuable) { issuables.order_updated_desc.first } + + context 'for merge requests' do + include MergeRequestHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + source_branch: "#{issuable_type}_#{i}", + source_project: project }.merge(ts) + end + + MergeRequest.all + end + + context 'default sort order' do + context 'in the "merge requests" tab', :js do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests project + + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / open" tab', :js do + let(:issuable_type) { :merge_request } + + it 'is "created date"' do + visit_merge_requests_with_state(project, 'open') + + expect(selected_sort_order).to eq('created date') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / merged" tab', :js do + let(:issuable_type) { :merged_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'merged') + + expect(find('.issues-other-filters')).to have_content('Last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / closed" tab', :js do + let(:issuable_type) { :closed_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'closed') + + expect(find('.issues-other-filters')).to have_content('Last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / all" tab', :js do + let(:issuable_type) { :merge_request } + + it 'is "created date"' do + visit_merge_requests_with_state(project, 'all') + + expect(find('.issues-other-filters')).to have_content('Created date') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'custom sorting' do + let(:issuable_type) { :merge_request } + + it 'supports sorting in asc and desc order' do + visit_merge_requests_with_state(project, 'open') + + page.within('.issues-other-filters') do + click_button('Created date') + click_link('Last updated') + end + + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + + find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click + + expect(first_merge_request).to include(first_updated_issuable.title) + expect(last_merge_request).to include(last_updated_issuable.title) + end + end + end + end + + context 'for issues' do + include IssueHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + project: project }.merge(ts) + end + + Issue.all + end + + context 'default sort order' do + context 'in the "issues" tab', :js do + let(:issuable_type) { :issue } + + it 'is "created date"' do + visit_issues project + + expect(find('.issues-other-filters')).to have_content('Created date') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / open" tab', :js do + let(:issuable_type) { :issue } + + it 'is "created date"' do + visit_issues_with_state(project, 'open') + + expect(find('.issues-other-filters')).to have_content('Created date') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / closed" tab', :js do + let(:issuable_type) { :closed_issue } + + it 'is "last updated"' do + visit_issues_with_state(project, 'closed') + + expect(find('.issues-other-filters')).to have_content('Last updated') + expect(first_issue).to include(last_updated_issuable.title) + expect(last_issue).to include(first_updated_issuable.title) + end + end + + context 'in the "issues / all" tab', :js do + let(:issuable_type) { :issue } + + it 'is "created date"' do + visit_issues_with_state(project, 'all') + + expect(find('.issues-other-filters')).to have_content('Created date') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'when the sort in the URL is id_desc' do + let(:issuable_type) { :issue } + + before do + visit_issues(project, sort: 'id_desc') + end + + it 'shows the sort order as created date' do + expect(find('.issues-other-filters')).to have_content('Created date') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + end + + context 'custom sorting' do + let(:issuable_type) { :issue } + + it 'supports sorting in asc and desc order' do + visit_issues_with_state(project, 'open') + + page.within('.issues-other-filters') do + click_button('Created date') + click_link('Last updated') + end + + expect(first_issue).to include(last_updated_issuable.title) + expect(last_issue).to include(first_updated_issuable.title) + + find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click + + expect(first_issue).to include(first_updated_issuable.title) + expect(last_issue).to include(last_updated_issuable.title) + end + end + end + + def selected_sort_order + find('.filter-dropdown-container .dropdown button').text.downcase + end + + def visit_merge_requests_with_state(project, state) + visit_merge_requests project, state: state + end + + def visit_issues_with_state(project, state) + visit_issues project, state: state + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 4d9b8262f21..a29380a180e 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -430,7 +430,7 @@ describe 'Filter issues', :js do expect_issues_list_count(2) - sort_toggle = find('.filter-dropdown-container .dropdown-menu-toggle') + sort_toggle = find('.filter-dropdown-container .dropdown') sort_toggle.click find('.filter-dropdown-container .dropdown-menu li a', text: 'Created date').click diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb index 3bc93933183..eebd2d57cca 100644 --- a/spec/features/issues/user_sorts_issues_spec.rb +++ b/spec/features/issues/user_sorts_issues_spec.rb @@ -20,9 +20,9 @@ describe "User sorts issues" do end it 'keeps the sort option' do - find('.filter-dropdown-container button.dropdown-menu-toggle').click + find('.filter-dropdown-container .dropdown').click - page.within('.content ul.dropdown-menu.dropdown-menu-right li') do + page.within('ul.dropdown-menu.dropdown-menu-right li') do click_link('Milestone') end @@ -40,9 +40,9 @@ describe "User sorts issues" do end it "sorts by popularity" do - find(".filter-dropdown-container button.dropdown-menu-toggle").click + find('.filter-dropdown-container .dropdown').click - page.within(".content ul.dropdown-menu.dropdown-menu-right li") do + page.within('ul.dropdown-menu.dropdown-menu-right li') do click_link("Popularity") end diff --git a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb index 61e8f1c4662..fa887110c13 100644 --- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb @@ -19,9 +19,9 @@ describe 'User sorts merge requests' do end it 'keeps the sort option' do - find('.filter-dropdown-container button.dropdown-menu-toggle').click + find('.filter-dropdown-container .dropdown').click - page.within('.content ul.dropdown-menu.dropdown-menu-right li') do + page.within('ul.dropdown-menu.dropdown-menu-right li') do click_link('Milestone') end @@ -49,9 +49,9 @@ describe 'User sorts merge requests' do it 'separates remember sorting with issues' do create(:issue, project: project) - find('.filter-dropdown-container button.dropdown-menu-toggle').click + find('.filter-dropdown-container .dropdown').click - page.within('.content ul.dropdown-menu.dropdown-menu-right li') do + page.within('ul.dropdown-menu.dropdown-menu-right li') do click_link('Milestone') end @@ -70,9 +70,9 @@ describe 'User sorts merge requests' do end it 'sorts by popularity' do - find('.filter-dropdown-container button.dropdown-menu-toggle').click + find('.filter-dropdown-container .dropdown').click - page.within('.content ul.dropdown-menu.dropdown-menu-right li') do + page.within('ul.dropdown-menu.dropdown-menu-right li') do click_link('Popularity') end diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index b778c72bc76..25417cf4955 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -32,7 +32,7 @@ describe 'Issue prioritization' do visit project_issues_path(project, sort: 'label_priority') # Ensure we are indicating that issues are sorted by priority - expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority') + expect(page).to have_selector('.dropdown', text: 'Label priority') page.within('.issues-holder') do issue_titles = all('.issues-list .issue-title-text').map(&:text) @@ -70,7 +70,7 @@ describe 'Issue prioritization' do sign_in user visit project_issues_path(project, sort: 'label_priority') - expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority') + expect(page).to have_selector('.dropdown', text: 'Label priority') page.within('.issues-holder') do issue_titles = all('.issues-list .issue-title-text').map(&:text) 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/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json index 314f04107eb..ce66f562175 100644 --- a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json +++ b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json @@ -11,7 +11,13 @@ "name": "Gemnasium" }, "location": { - "file": "app/pom.xml" + "file": "app/pom.xml", + "dependency": { + "package": { + "name": "io.netty/netty" + }, + "version": "3.9.1.Final" + } }, "identifiers": [ { @@ -55,7 +61,13 @@ "name": "Gemnasium" }, "location": { - "file": "app/requirements.txt" + "file": "app/requirements.txt", + "dependency": { + "package": { + "name": "Django" + }, + "version": "1.11.3" + } }, "identifiers": [ { @@ -93,7 +105,13 @@ "name": "Gemnasium" }, "location": { - "file": "rails/Gemfile.lock" + "file": "rails/Gemfile.lock", + "dependency": { + "package": { + "name": "nokogiri" + }, + "version": "1.8.0" + } }, "identifiers": [ { @@ -131,7 +149,13 @@ "name": "bundler-audit" }, "location": { - "file": "sast-sample-rails/Gemfile.lock" + "file": "sast-sample-rails/Gemfile.lock", + "dependency": { + "package": { + "name": "ffi" + }, + "version": "1.9.18" + } }, "identifiers": [ { diff --git a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json index 314f04107eb..ce66f562175 100644 --- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json +++ b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json @@ -11,7 +11,13 @@ "name": "Gemnasium" }, "location": { - "file": "app/pom.xml" + "file": "app/pom.xml", + "dependency": { + "package": { + "name": "io.netty/netty" + }, + "version": "3.9.1.Final" + } }, "identifiers": [ { @@ -55,7 +61,13 @@ "name": "Gemnasium" }, "location": { - "file": "app/requirements.txt" + "file": "app/requirements.txt", + "dependency": { + "package": { + "name": "Django" + }, + "version": "1.11.3" + } }, "identifiers": [ { @@ -93,7 +105,13 @@ "name": "Gemnasium" }, "location": { - "file": "rails/Gemfile.lock" + "file": "rails/Gemfile.lock", + "dependency": { + "package": { + "name": "nokogiri" + }, + "version": "1.8.0" + } }, "identifiers": [ { @@ -131,7 +149,13 @@ "name": "bundler-audit" }, "location": { - "file": "sast-sample-rails/Gemfile.lock" + "file": "sast-sample-rails/Gemfile.lock", + "dependency": { + "package": { + "name": "ffi" + }, + "version": "1.9.18" + } }, "identifiers": [ { diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb new file mode 100644 index 00000000000..cba0d93e144 --- /dev/null +++ b/spec/helpers/sorting_helper_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe SortingHelper do + include ApplicationHelper + include IconsHelper + + describe '#issuable_sort_option_title' do + it 'returns correct title for issuable_sort_option_overrides key' do + expect(issuable_sort_option_title('created_asc')).to eq('Created date') + end + + it 'returns correct title for a valid sort value' do + expect(issuable_sort_option_title('priority')).to eq('Priority') + end + + it 'returns nil for invalid sort value' do + expect(issuable_sort_option_title('invalid_key')).to eq(nil) + end + end + + describe '#issuable_sort_direction_button' do + before do + allow(self).to receive(:request).and_return(double(path: 'http://test.com', query_parameters: {})) + end + + it 'returns icon with sort-highest when sort is created_date' do + expect(issuable_sort_direction_button('created_date')).to include('sort-highest') + end + + it 'returns icon with sort-lowest when sort is asc' do + expect(issuable_sort_direction_button('created_asc')).to include('sort-lowest') + end + + it 'returns icon with sort-lowest when sorting by milestone' do + expect(issuable_sort_direction_button('milestone')).to include('sort-lowest') + end + + it 'returns icon with sort-lowest when sorting by due_date' do + expect(issuable_sort_direction_button('due_date')).to include('sort-lowest') + end + end +end 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/database/count/exact_count_strategy_spec.rb b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb index f518bb3dc3e..3991c737a26 100644 --- a/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb +++ b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb @@ -16,6 +16,12 @@ describe Gitlab::Database::Count::ExactCountStrategy do expect(subject).to eq({ Project => 3, Identity => 1 }) end + + it 'returns default value if count times out' do + allow(models.first).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new('')) + + expect(subject).to eq({}) + end end describe '.enabled?' do 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/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 4a0dc3686ec..6831274d37c 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -54,11 +54,18 @@ describe Gitlab::ProjectSearchResults do end it 'finds by name' do - expect(results.map(&:first)).to include(expected_file_by_name) + expect(results.map(&:filename)).to include(expected_file_by_name) + end + + it "loads all blobs for filename matches in single batch" do + expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original + + expected = project.repository.search_files_by_name(query, 'master') + expect(results.map(&:filename)).to include(*expected) end it 'finds by content' do - blob = results.select { |result| result.first == expected_file_by_content }.flatten.last + blob = results.select { |result| result.filename == expected_file_by_content }.flatten.last expect(blob.filename).to eq(expected_file_by_content) end @@ -122,126 +129,6 @@ describe Gitlab::ProjectSearchResults do let(:blob_type) { 'blobs' } let(:entity) { project } end - - describe 'parsing results' do - let(:results) { project.repository.search_files_by_content('feature', 'master') } - let(:search_result) { results.first } - - subject { described_class.parse_search_result(search_result) } - - it "returns a valid FoundBlob" do - is_expected.to be_an Gitlab::SearchResults::FoundBlob - expect(subject.id).to be_nil - expect(subject.path).to eq('CHANGELOG') - expect(subject.filename).to eq('CHANGELOG') - expect(subject.basename).to eq('CHANGELOG') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(188) - expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") - end - - context 'when the matching filename contains a colon' do - let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" } - - it 'returns a valid FoundBlob' do - expect(subject.filename).to eq('testdata/project::function1.yaml') - expect(subject.basename).to eq('testdata/project::function1') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(1) - expect(subject.data).to eq("---\n") - end - end - - context 'when the matching content contains a number surrounded by colons' do - let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" } - - it 'returns a valid FoundBlob' do - expect(subject.filename).to eq('testdata/foo.txt') - expect(subject.basename).to eq('testdata/foo') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(1) - expect(subject.data).to eq('blah:9:blah') - end - end - - context 'when the matching content contains multiple null bytes' do - let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" } - - it 'returns a valid FoundBlob' do - expect(subject.filename).to eq('testdata/foo.txt') - expect(subject.basename).to eq('testdata/foo') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(1) - expect(subject.data).to eq("blah\x001\x00foo") - end - end - - context 'when the search result ends with an empty line' do - let(:results) { project.repository.search_files_by_content('Role models', 'master') } - - it 'returns a valid FoundBlob that ends with an empty line' do - expect(subject.filename).to eq('files/markdown/ruby-style-guide.md') - expect(subject.basename).to eq('files/markdown/ruby-style-guide') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(1) - expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n") - end - end - - context 'when the search returns non-ASCII data' do - context 'with UTF-8' do - let(:results) { project.repository.search_files_by_content('файл', 'master') } - - it 'returns results as UTF-8' do - expect(subject.filename).to eq('encoding/russian.rb') - expect(subject.basename).to eq('encoding/russian') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(1) - expect(subject.data).to eq("Хороший файл\n") - end - end - - context 'with UTF-8 in the filename' do - let(:results) { project.repository.search_files_by_content('webhook', 'master') } - - it 'returns results as UTF-8' do - expect(subject.filename).to eq('encoding/テスト.txt') - expect(subject.basename).to eq('encoding/テスト') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(3) - expect(subject.data).to include('WebHookの確認') - end - end - - context 'with ISO-8859-1' do - let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) } - - it 'returns results as UTF-8' do - expect(subject.filename).to eq('encoding/iso8859.txt') - expect(subject.basename).to eq('encoding/iso8859') - expect(subject.ref).to eq('master') - expect(subject.startline).to eq(1) - expect(subject.data).to eq("Äü\n\nfoo\n") - end - end - end - - context "when filename has extension" do - let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" } - - it { expect(subject.path).to eq('CONTRIBUTE.md') } - it { expect(subject.filename).to eq('CONTRIBUTE.md') } - it { expect(subject.basename).to eq('CONTRIBUTE') } - end - - context "when file under directory" do - let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" } - - it { expect(subject.path).to eq('a/b/c.md') } - it { expect(subject.filename).to eq('a/b/c.md') } - it { expect(subject.basename).to eq('a/b/c') } - end - end end describe 'wiki search' do diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb new file mode 100644 index 00000000000..74157e5c67c --- /dev/null +++ b/spec/lib/gitlab/search/found_blob_spec.rb @@ -0,0 +1,138 @@ +# coding: utf-8 + +require 'spec_helper' + +describe Gitlab::Search::FoundBlob do + describe 'parsing results' do + let(:project) { create(:project, :public, :repository) } + let(:results) { project.repository.search_files_by_content('feature', 'master') } + let(:search_result) { results.first } + + subject { described_class.new(content_match: search_result, project: project) } + + it "returns a valid FoundBlob" do + is_expected.to be_an described_class + expect(subject.id).to be_nil + expect(subject.path).to eq('CHANGELOG') + expect(subject.filename).to eq('CHANGELOG') + expect(subject.basename).to eq('CHANGELOG') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(188) + expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") + end + + it "doesn't parses content if not needed" do + expect(subject).not_to receive(:parse_search_result) + expect(subject.project_id).to eq(project.id) + expect(subject.binary_filename).to eq('CHANGELOG') + end + + it "parses content only once when needed" do + expect(subject).to receive(:parse_search_result).once.and_call_original + expect(subject.filename).to eq('CHANGELOG') + expect(subject.startline).to eq(188) + end + + context 'when the matching filename contains a colon' do + let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/project::function1.yaml') + expect(subject.basename).to eq('testdata/project::function1') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("---\n") + end + end + + context 'when the matching content contains a number surrounded by colons' do + let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/foo.txt') + expect(subject.basename).to eq('testdata/foo') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq('blah:9:blah') + end + end + + context 'when the matching content contains multiple null bytes' do + let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/foo.txt') + expect(subject.basename).to eq('testdata/foo') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("blah\x001\x00foo") + end + end + + context 'when the search result ends with an empty line' do + let(:results) { project.repository.search_files_by_content('Role models', 'master') } + + it 'returns a valid FoundBlob that ends with an empty line' do + expect(subject.filename).to eq('files/markdown/ruby-style-guide.md') + expect(subject.basename).to eq('files/markdown/ruby-style-guide') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n") + end + end + + context 'when the search returns non-ASCII data' do + context 'with UTF-8' do + let(:results) { project.repository.search_files_by_content('файл', 'master') } + + it 'returns results as UTF-8' do + expect(subject.filename).to eq('encoding/russian.rb') + expect(subject.basename).to eq('encoding/russian') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("Хороший файл\n") + end + end + + context 'with UTF-8 in the filename' do + let(:results) { project.repository.search_files_by_content('webhook', 'master') } + + it 'returns results as UTF-8' do + expect(subject.filename).to eq('encoding/テスト.txt') + expect(subject.basename).to eq('encoding/テスト') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(3) + expect(subject.data).to include('WebHookの確認') + end + end + + context 'with ISO-8859-1' do + let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) } + + it 'returns results as UTF-8' do + expect(subject.filename).to eq('encoding/iso8859.txt') + expect(subject.basename).to eq('encoding/iso8859') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("Äü\n\nfoo\n") + end + end + end + + context "when filename has extension" do + let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" } + + it { expect(subject.path).to eq('CONTRIBUTE.md') } + it { expect(subject.filename).to eq('CONTRIBUTE.md') } + it { expect(subject.basename).to eq('CONTRIBUTE') } + end + + context "when file under directory" do + let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" } + + it { expect(subject.path).to eq('a/b/c.md') } + it { expect(subject.filename).to eq('a/b/c.md') } + it { expect(subject.basename).to eq('a/b/c') } + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index e2de612ff46..deb19fe1a4b 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -213,4 +213,29 @@ describe Gitlab::UsageData do expect(described_class.count(relation, fallback: 15)).to eq(15) end end + + describe '#approximate_counts' do + it 'gets approximate counts for selected models' do + create(:label) + + expect(Gitlab::Database::Count).to receive(:approximate_counts) + .with(described_class::APPROXIMATE_COUNT_MODELS).once.and_call_original + + counts = described_class.approximate_counts.values + + expect(counts.count).to eq(described_class::APPROXIMATE_COUNT_MODELS.count) + expect(counts.any? { |count| count < 0 }).to be_falsey + end + + it 'returns default values if counts can not be retrieved' do + described_class::APPROXIMATE_COUNT_MODELS.map do |model| + model.name.underscore.pluralize.to_sym + end + + expect(Gitlab::Database::Count).to receive(:approximate_counts) + .and_return({}) + + expect(described_class.approximate_counts.values.uniq).to eq([-1]) + end + end end 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/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ba9540c84d4..b67c6a4cffa 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -350,6 +350,50 @@ describe Ci::Pipeline, :mailer do CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION] end + + context 'when source is merge request' do + let(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'exposes merge request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, + 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, + 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, + 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, + 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, + 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, + 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s) + end + + context 'when source project does not exist' do + before do + merge_request.update_column(:source_project_id, nil) + end + + it 'does not expose source project related variables' do + expect(subject.to_hash.keys).not_to include( + %w[CI_MERGE_REQUEST_SOURCE_PROJECT_ID + CI_MERGE_REQUEST_SOURCE_PROJECT_PATH + CI_MERGE_REQUEST_SOURCE_PROJECT_URL + CI_MERGE_REQUEST_SOURCE_BRANCH_NAME]) + end + end + end end describe '#protected_ref?' do 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/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb index a1ae428586e..003ecb251fe 100644 --- a/spec/support/helpers/features/sorting_helpers.rb +++ b/spec/support/helpers/features/sorting_helpers.rb @@ -13,9 +13,9 @@ module Spec module Features module SortingHelpers def sort_by(value) - find('.filter-dropdown-container button.dropdown-menu-toggle').click + find('.filter-dropdown-container .dropdown').click - page.within('.content ul.dropdown-menu.dropdown-menu-right li') do + page.within('ul.dropdown-menu.dropdown-menu-right li') do click_link(value) end end diff --git a/spec/support/shared_examples/file_finder.rb b/spec/support/shared_examples/file_finder.rb index ef144bdf61c..0dc351b5149 100644 --- a/spec/support/shared_examples/file_finder.rb +++ b/spec/support/shared_examples/file_finder.rb @@ -3,18 +3,19 @@ shared_examples 'file finder' do let(:search_results) { subject.find(query) } it 'finds by name' do - filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_name } - expect(filename).to eq(expected_file_by_name) - expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) + blob = search_results.find { |blob| blob.filename == expected_file_by_name } + + expect(blob.filename).to eq(expected_file_by_name) + expect(blob).to be_a(Gitlab::Search::FoundBlob) expect(blob.ref).to eq(subject.ref) expect(blob.data).not_to be_empty end it 'finds by content' do - filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_content } + blob = search_results.find { |blob| blob.filename == expected_file_by_content } - expect(filename).to eq(expected_file_by_content) - expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) + expect(blob.filename).to eq(expected_file_by_content) + expect(blob).to be_a(Gitlab::Search::FoundBlob) expect(blob.ref).to eq(subject.ref) expect(blob.data).not_to be_empty end 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 |