diff options
31 files changed, 1174 insertions, 560 deletions
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index d8a1a758a0d..2916762e31f 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -32,7 +32,7 @@ module PreviewMarkdown def markdown_context_params case controller_name - when 'wikis' then { pipeline: :wiki, wiki: @project_wiki, page_slug: params[:id] } + when 'wikis' then { pipeline: :wiki, wiki: wiki, page_slug: params[:id] } when 'snippets' then { skip_project_check: true } when 'groups' then { group: group } when 'projects' then projects_filter_params diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb new file mode 100644 index 00000000000..7aaa53da452 --- /dev/null +++ b/app/controllers/concerns/wiki_actions.rb @@ -0,0 +1,228 @@ +# frozen_string_literal: true + +module WikiActions + include SendsBlob + include Gitlab::Utils::StrongMemoize + extend ActiveSupport::Concern + + included do + before_action :authorize_read_wiki! + before_action :authorize_create_wiki!, only: [:edit, :create] + before_action :authorize_admin_wiki!, only: :destroy + + before_action :wiki + before_action :page, only: [:show, :edit, :update, :history, :destroy] + before_action :load_sidebar, except: [:pages] + + before_action only: [:show, :edit, :update] do + @valid_encoding = valid_encoding? + end + + before_action only: [:edit, :update], unless: :valid_encoding? do + redirect_to wiki_page_path(wiki, page) + end + end + + def new + redirect_to wiki_page_path(wiki, SecureRandom.uuid, random_title: true) + end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def pages + @wiki_pages = Kaminari.paginate_array( + wiki.list_pages(sort: params[:sort], direction: params[:direction]) + ).page(params[:page]) + + @wiki_entries = WikiPage.group_by_directory(@wiki_pages) + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + # `#show` handles a number of scenarios: + # + # - If `id` matches a WikiPage, then show the wiki page. + # - If `id` is a file in the wiki repository, then send the file. + # - If we know the user wants to create a new page with the given `id`, + # then display a create form. + # - Otherwise show the empty wiki page and invite the user to create a page. + # + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def show + if page + set_encoding_error unless valid_encoding? + + # Assign vars expected by MarkupHelper + @ref = params[:version_id] + @path = page.path + + render 'show' + elsif file_blob + send_blob(wiki.repository, file_blob, allow_caching: container.public?) + elsif show_create_form? + # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new + title = params[:id] unless params[:random_title].present? + + @page = build_page(title: title) + + render 'edit' + else + render 'empty' + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def edit + end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def update + return render('empty') unless can?(current_user, :create_wiki, container) + + @page = WikiPages::UpdateService.new(container: container, current_user: current_user, params: wiki_params).execute(page) + + if page.valid? + redirect_to( + wiki_page_path(wiki, page), + notice: _('Wiki was successfully updated.') + ) + else + render 'edit' + end + rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e + @error = e + render 'edit' + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def create + @page = WikiPages::CreateService.new(container: container, current_user: current_user, params: wiki_params).execute + + if page.persisted? + redirect_to( + wiki_page_path(wiki, page), + notice: _('Wiki was successfully updated.') + ) + else + render action: "edit" + end + rescue Gitlab::Git::Wiki::OperationError => e + @page = build_page(wiki_params) + @error = e + + render 'edit' + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def history + if page + @page_versions = Kaminari.paginate_array(page.versions(page: params[:page].to_i), + total_count: page.count_versions) + .page(params[:page]) + else + redirect_to( + wiki_page_path(wiki, :home), + notice: _("Page not found") + ) + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def destroy + WikiPages::DestroyService.new(container: container, current_user: current_user).execute(page) + + redirect_to wiki_page_path(wiki, :home), + status: :found, + notice: _("Page was successfully deleted") + rescue Gitlab::Git::Wiki::OperationError => e + @error = e + render 'edit' + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + private + + def container + raise NotImplementedError + end + + def show_create_form? + can?(current_user, :create_wiki, container) && + page.nil? && + # Always show the create form when the wiki has had at least one page created. + # Otherwise, we only show the form when the user has navigated from + # the 'empty wiki' page + (wiki.exists? || params[:view] == 'create') + end + + def wiki + strong_memoize(:wiki) do + wiki = Wiki.for_container(container, current_user) + + # Call #wiki to make sure the Wiki Repo is initialized + wiki.wiki + + wiki + end + rescue Wiki::CouldNotCreateWikiError + flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.") + redirect_to container + false + end + + def page + strong_memoize(:page) do + wiki.find_page(*page_params) + end + end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def load_sidebar + @sidebar_page = wiki.find_sidebar(params[:version_id]) + + unless @sidebar_page # Fallback to default sidebar + @sidebar_wiki_entries, @sidebar_limited = wiki.sidebar_entries + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def wiki_params + params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha) + end + + def build_page(args = {}) + WikiPage.new(wiki).tap do |page| + page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases + end + end + + def page_params + keys = [:id] + keys << :version_id if params[:action] == 'show' + + params.values_at(*keys) + end + + def valid_encoding? + page_encoding == Encoding::UTF_8 + end + + def page_encoding + strong_memoize(:page_encoding) { page&.content&.encoding } + end + + def set_encoding_error + flash.now[:notice] = _("The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.") + end + + def file_blob + strong_memoize(:file_blob) do + commit = wiki.repository.commit(wiki.default_branch) + + next unless commit + + wiki.repository.blob_at(commit.id, params[:id]) + end + end +end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 508b1f5bd0a..85e643aa212 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,206 +1,11 @@ # frozen_string_literal: true class Projects::WikisController < Projects::ApplicationController + include WikiActions include PreviewMarkdown - include SendsBlob - include Gitlab::Utils::StrongMemoize - before_action :authorize_read_wiki! - before_action :authorize_create_wiki!, only: [:edit, :create] - before_action :authorize_admin_wiki!, only: :destroy - before_action :load_project_wiki - before_action :load_page, only: [:show, :edit, :update, :history, :destroy] - before_action only: [:show, :edit, :update] do - @valid_encoding = valid_encoding? - end - before_action only: [:edit, :update], unless: :valid_encoding? do - redirect_to(project_wiki_path(@project, @page)) - end - - def new - redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true) - end - - def pages - @wiki_pages = Kaminari.paginate_array( - @project_wiki.list_pages(sort: params[:sort], direction: params[:direction]) - ).page(params[:page]) - - @wiki_entries = WikiPage.group_by_directory(@wiki_pages) - end - - # `#show` handles a number of scenarios: - # - # - If `id` matches a WikiPage, then show the wiki page. - # - If `id` is a file in the wiki repository, then send the file. - # - If we know the user wants to create a new page with the given `id`, - # then display a create form. - # - Otherwise show the empty wiki page and invite the user to create a page. - def show - if @page - set_encoding_error unless valid_encoding? - - # Assign vars expected by MarkupHelper - @ref = params[:version_id] - @path = @page.path - - render 'show' - elsif file_blob - send_blob(@project_wiki.repository, file_blob, allow_caching: @project.public?) - elsif show_create_form? - # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new - title = params[:id] unless params[:random_title].present? - - @page = build_page(title: title) - - render 'edit' - else - render 'empty' - end - end - - def edit - end - - def update - return render('empty') unless can?(current_user, :create_wiki, @project) - - @page = WikiPages::UpdateService.new(container: @project, current_user: current_user, params: wiki_params).execute(@page) - - if @page.valid? - redirect_to( - project_wiki_path(@project, @page), - notice: _('Wiki was successfully updated.') - ) - else - render 'edit' - end - rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e - @error = e - render 'edit' - end - - def create - @page = WikiPages::CreateService.new(container: @project, current_user: current_user, params: wiki_params).execute - - if @page.persisted? - redirect_to( - project_wiki_path(@project, @page), - notice: _('Wiki was successfully updated.') - ) - else - render action: "edit" - end - rescue Gitlab::Git::Wiki::OperationError => e - @page = build_page(wiki_params) - @error = e - - render 'edit' - end - - def history - if @page - @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i), - total_count: @page.count_versions) - .page(params[:page]) - else - redirect_to( - project_wiki_path(@project, :home), - notice: _("Page not found") - ) - end - end - - def destroy - WikiPages::DestroyService.new(container: @project, current_user: current_user).execute(@page) - - redirect_to project_wiki_path(@project, :home), - status: :found, - notice: _("Page was successfully deleted") - rescue Gitlab::Git::Wiki::OperationError => e - @error = e - render 'edit' - end + alias_method :container, :project def git_access end - - private - - def show_create_form? - can?(current_user, :create_wiki, @project) && - @page.nil? && - # Always show the create form when the wiki has had at least one page created. - # Otherwise, we only show the form when the user has navigated from - # the 'empty wiki' page - (@project_wiki.exists? || params[:view] == 'create') - end - - def load_project_wiki - @project_wiki = load_wiki - - # Call #wiki to make sure the Wiki Repo is initialized - @project_wiki.wiki - - @sidebar_page = @project_wiki.find_sidebar(params[:version_id]) - - unless @sidebar_page # Fallback to default sidebar - @sidebar_wiki_entries, @sidebar_limited = @project_wiki.sidebar_entries - end - rescue ProjectWiki::CouldNotCreateWikiError - flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.") - redirect_to project_path(@project) - false - end - - def load_wiki - ProjectWiki.new(@project, current_user) - end - - def wiki_params - params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha) - end - - def build_page(args = {}) - WikiPage.new(@project_wiki).tap do |page| - page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases - end - end - - def load_page - @page ||= find_page - end - - def find_page - @project_wiki.find_page(*page_params) - end - - def page_params - keys = [:id] - keys << :version_id if params[:action] == 'show' - - params.values_at(*keys) - end - - def valid_encoding? - page_encoding == Encoding::UTF_8 - end - - def page_encoding - strong_memoize(:page_encoding) { @page&.content&.encoding } - end - - def set_encoding_error - flash.now[:notice] = _("The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.") - end - - def file_blob - strong_memoize(:file_blob) do - commit = @project_wiki.repository.commit(@project_wiki.default_branch) - - next unless commit - - @project_wiki.repository.blob_at(commit.id, params[:id]) - end - end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3f2e9f73e37..2b50e61db99 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -310,8 +310,8 @@ class ProjectsController < Projects::ApplicationController render 'projects/empty' if @project.empty_repo? else if can?(current_user, :read_wiki, @project) - @project_wiki = @project.wiki - @wiki_home = @project_wiki.find_page('home', params[:version_id]) + @wiki = @project.wiki + @wiki_home = @wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) @issues = issuables_collection.page(params[:page]) @issuable_meta_data = Gitlab::IssuableMetadata.new(current_user, @issues).data diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 4474534045b..b47e3884072 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -298,6 +298,12 @@ module GitlabRoutingHelper toggle_award_emoji_snippet_url(snippet, *new_args) end + # Wikis + + def wiki_page_path(wiki, page, **options) + Gitlab::UrlBuilder.wiki_page_url(wiki, page, **options, only_path: true) + end + private def snippet_query_params(snippet, *args) diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 1a1daa297c6..7ab2b33de8c 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -129,8 +129,8 @@ module MarkupHelper context.merge!( pipeline: :wiki, project: @project, - wiki: @project_wiki, - repository: @project_wiki.repository, + wiki: @wiki, + repository: @wiki.repository, page_slug: wiki_page.slug, issuable_state_filter_enabled: true ) @@ -300,7 +300,7 @@ module MarkupHelper # RepositoryLinkFilter and UploadLinkFilter commit: @commit, - project_wiki: @project_wiki, + wiki: @wiki, ref: @ref, requested_path: @path ) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0ba660aadbd..6faf45d1009 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -104,7 +104,7 @@ class MergeRequest < ApplicationRecord after_create :ensure_merge_request_diff after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed - after_save :ensure_metrics, unless: :importing? + after_commit :ensure_metrics, on: [:create, :update], unless: :importing? after_commit :expire_etag_cache, unless: :importing? # When this attribute is true some MR validation is ignored diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index fcb7fc908f1..7d7ee8d829e 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -128,7 +128,7 @@ module SystemNotes body = cross_reference_note_content(gfm_reference) if noteable.is_a?(ExternalIssue) - noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author) + noteable.project.external_issue_tracker.create_cross_reference_note(noteable, mentioner, author) else create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference')) end diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 72c9f45779a..e8696a0a86d 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -8,10 +8,10 @@ .git-access-header.w-100.d-flex.flex-column.justify-content-center %span = _("Clone repository") - %strong= @project_wiki.full_path + %strong= @wiki.full_path .pt-3.pt-lg-0.w-100 - = render "shared/clone_panel", project: @project_wiki + = render "shared/clone_panel", project: @wiki .wiki-git-access %h3= s_("WikiClone|Install Gollum") @@ -22,8 +22,8 @@ %h3= s_("WikiClone|Clone your wiki") %pre.dark :preserve - git clone #{ content_tag(:span, h(default_url_to_repo(@project_wiki)), class: 'clone')} - cd #{h @project_wiki.path} + git clone #{ content_tag(:span, h(default_url_to_repo(@wiki)), class: 'clone')} + cd #{h @wiki.path} %h3= s_("WikiClone|Start Gollum and edit locally") %pre.dark diff --git a/changelogs/unreleased/219151-follow-up-from-fallback-to-lowest-visibility-level-in-snippet-visi.yml b/changelogs/unreleased/219151-follow-up-from-fallback-to-lowest-visibility-level-in-snippet-visi.yml new file mode 100644 index 00000000000..b9f98664dff --- /dev/null +++ b/changelogs/unreleased/219151-follow-up-from-fallback-to-lowest-visibility-level-in-snippet-visi.yml @@ -0,0 +1,5 @@ +--- +title: Add snippet DB visibility check in spec +merge_request: 33388 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/500-metrics-creation.yml b/changelogs/unreleased/500-metrics-creation.yml new file mode 100644 index 00000000000..83a93e2ff04 --- /dev/null +++ b/changelogs/unreleased/500-metrics-creation.yml @@ -0,0 +1,5 @@ +--- +title: Fix for metrics creation when saving MR +merge_request: 32668 +author: +type: fixed diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index f5dc2b558d4..57993061c58 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -107,6 +107,10 @@ class Gitlab::Seeder::CycleAnalytics pipeline = FactoryBot.create(:ci_pipeline, :success, project: project) build = FactoryBot.create(:ci_build, pipeline: pipeline, project: project, user: developers.sample) + # Required because seeds run in a transaction and these are now + # created in an `after_commit` hook. + merge_request.ensure_metrics + merge_request.metrics.update!( latest_build_started_at: merge_request.created_at, latest_build_finished_at: within_end_time(merge_request.created_at + TEST_STAGE_MAX_DURATION_IN_HOURS.hours), diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 93c4c9e93a7..b80c725dad5 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -97,6 +97,8 @@ From there, you can see the following actions: - Permission to approve merge requests by authors was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9) - Number of required approvals was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9) +Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events-starter) + ### Instance events **(PREMIUM ONLY)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2336) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3. diff --git a/doc/api/audit_events.md b/doc/api/audit_events.md index 36b3722475f..ce2a9afd53c 100644 --- a/doc/api/audit_events.md +++ b/doc/api/audit_events.md @@ -225,3 +225,115 @@ Example response: "created_at": "2019-08-28T19:36:44.162Z" } ``` + +## Project Audit Events **(STARTER)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219238) in GitLab 13.1. + +The Project Audit Events API allows you to retrieve [project audit events](../administration/audit_events.md#project-events-starter). + +To retrieve project audit events using the API, you must [authenticate yourself](README.md#authentication) as a Maintainer or an Owner of the project. + +### Retrieve all project audit events + +```plaintext +GET /projects/:id/audit_events +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `created_after` | string | no | Return project audit events created on or after the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ | +| `created_before` | string | no | Return project audit events created on or before the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ | + +By default, `GET` requests return 20 results at a time because the API results +are paginated. + +Read more on [pagination](README.md#pagination). + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events +``` + +Example response: + +```json +[ + { + "id": 5, + "author_id": 1, + "entity_id": 7, + "entity_type": "Project", + "details": { + "change": "prevent merge request approval from reviewers", + "from": "", + "to": "true", + "author_name": "Administrator", + "target_id": 7, + "target_type": "Project", + "target_details": "twitter/typeahead-js", + "ip_address": "127.0.0.1", + "entity_path": "twitter/typeahead-js" + }, + "created_at": "2020-05-26T22:55:04.230Z" + }, + { + "id": 4, + "author_id": 1, + "entity_id": 7, + "entity_type": "Project", + "details": { + "change": "prevent merge request approval from authors", + "from": "false", + "to": "true", + "author_name": "Administrator", + "target_id": 7, + "target_type": "Project", + "target_details": "twitter/typeahead-js", + "ip_address": "127.0.0.1", + "entity_path": "twitter/typeahead-js" + }, + "created_at": "2020-05-26T22:55:04.218Z" + } +] +``` + +### Retrieve a specific project audit event + +Only available to project maintainers or owners. + +```plaintext +GET /projects/:id/audit_events/:audit_event_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `audit_event_id` | integer | yes | The ID of the audit event | + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events/5 +``` + +Example response: + +```json +{ + "id": 5, + "author_id": 1, + "entity_id": 7, + "entity_type": "Project", + "details": { + "change": "prevent merge request approval from reviewers", + "from": "", + "to": "true", + "author_name": "Administrator", + "target_id": 7, + "target_type": "Project", + "target_details": "twitter/typeahead-js", + "ip_address": "127.0.0.1", + "entity_path": "twitter/typeahead-js" + }, + "created_at": "2020-05-26T22:55:04.230Z" +} +``` diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 0a08d113e37..650a3878d38 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -12614,6 +12614,36 @@ type Vulnerability { id: ID! """ + List of issue links related to the vulnerability + """ + issueLinks( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter issue links by link type + """ + linkType: [VulnerabilityIssueLinkType!] + ): VulnerabilityIssueLinkConnection! + + """ Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability """ location: VulnerabilityLocation @@ -12696,6 +12726,69 @@ type VulnerabilityEdge { } """ +Represents an issue link of a vulnerability. +""" +type VulnerabilityIssueLink { + """ + GraphQL ID of the vulnerability + """ + id: ID! + + """ + The issue attached to issue link + """ + issue: Issue! + + """ + Type of the issue link + """ + linkType: VulnerabilityIssueLinkType! +} + +""" +The connection type for VulnerabilityIssueLink. +""" +type VulnerabilityIssueLinkConnection { + """ + A list of edges. + """ + edges: [VulnerabilityIssueLinkEdge] + + """ + A list of nodes. + """ + nodes: [VulnerabilityIssueLink] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type VulnerabilityIssueLinkEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: VulnerabilityIssueLink +} + +""" +The type of the issue link related to a vulnerability. +""" +enum VulnerabilityIssueLinkType { + CREATED + RELATED +} + +""" Represents a vulnerability location. The fields with data will depend on the vulnerability report type """ union VulnerabilityLocation = VulnerabilityLocationContainerScanning | VulnerabilityLocationDast | VulnerabilityLocationDependencyScanning | VulnerabilityLocationSast diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 506c060f5e0..92ff57f6604 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -37151,6 +37151,81 @@ "deprecationReason": null }, { + "name": "issueLinks", + "description": "List of issue links related to the vulnerability", + "args": [ + { + "name": "linkType", + "description": "Filter issue links by link type", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityIssueLinkType", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VulnerabilityIssueLinkConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "location", "description": "Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability", "args": [ @@ -37405,6 +37480,208 @@ "possibleTypes": null }, { + "kind": "OBJECT", + "name": "VulnerabilityIssueLink", + "description": "Represents an issue link of a vulnerability.", + "fields": [ + { + "name": "id", + "description": "GraphQL ID of the vulnerability", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issue", + "description": "The issue attached to issue link", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "linkType", + "description": "Type of the issue link", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityIssueLinkType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VulnerabilityIssueLinkConnection", + "description": "The connection type for VulnerabilityIssueLink.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VulnerabilityIssueLinkEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VulnerabilityIssueLink", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VulnerabilityIssueLinkEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "VulnerabilityIssueLink", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "VulnerabilityIssueLinkType", + "description": "The type of the issue link related to a vulnerability.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "RELATED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREATED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { "kind": "UNION", "name": "VulnerabilityLocation", "description": "Represents a vulnerability location. The fields with data will depend on the vulnerability report type", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 405a32be0e8..ba335b67cc8 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1871,6 +1871,16 @@ Represents a vulnerability. | `userPermissions` | VulnerabilityPermissions! | Permissions for the current user on the resource | | `vulnerabilityPath` | String | URL to the vulnerability's details page | +## VulnerabilityIssueLink + +Represents an issue link of a vulnerability. + +| Name | Type | Description | +| --- | ---- | ---------- | +| `id` | ID! | GraphQL ID of the vulnerability | +| `issue` | Issue! | The issue attached to issue link | +| `linkType` | VulnerabilityIssueLinkType! | Type of the issue link | + ## VulnerabilityLocationContainerScanning Represents the location of a vulnerability found by a container security scan diff --git a/doc/api/users.md b/doc/api/users.md index cb0d24d8566..6ac1cd089e7 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -70,12 +70,12 @@ Username search is case insensitive. GET /users ``` -| Attribute | Type | Required | Description | -| ------------ | ------ | -------- | ----------- | -| `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | -| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` | -| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users | -| `without_projects` | boolean | no | Filter users without projects. Default is `false` | +| Attribute | Type | Required | Description | +| ------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------- | +| `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | +| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` | +| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users | +| `without_projects` | boolean | no | Filter users without projects. Default is `false` | ```json [ @@ -375,7 +375,7 @@ POST /users Parameters: | Attribute | Required | Description | -|:-------------------------------------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------------------------| +| :----------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | | `admin` | No | User is admin - true or false (default) | | `avatar` | No | Image file for user's avatar | | `bio` | No | User's biography | @@ -417,7 +417,7 @@ PUT /users/:id Parameters: | Attribute | Required | Description | -|:-------------------------------------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------------------------| +| :----------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | | `admin` | No | User is admin - true or false (default) | | `avatar` | No | Image file for user's avatar | | `bio` | No | User's biography | @@ -432,7 +432,7 @@ Parameters: | `linkedin` | No | LinkedIn | | `location` | No | User's location | | `name` | No | Name | -| `note` | No | Admin notes for this user | +| `note` | No | Admin notes for this user | | `organization` | No | Organization name | | `password` | No | Password | | `private_profile` | No | User's profile is private - true, false (default), or null (will be converted to false) | @@ -609,8 +609,8 @@ Get the status of a user. GET /users/:id_or_username/status ``` -| Attribute | Type | Required | Description | -| ---------------- | ------ | -------- | ----------- | +| Attribute | Type | Required | Description | +| ---------------- | ------ | -------- | ------------------------------------------------- | | `id_or_username` | string | yes | The ID or username of the user to get a status of | ```shell @@ -635,10 +635,10 @@ Set the status of the current user. PUT /user/status ``` -| Attribute | Type | Required | Description | -| --------- | ------ | -------- | ----------- | -| `emoji` | string | no | The name of the emoji to use as status, if omitted `speech_balloon` is used. Emoji name can be one of the specified names in the [Gemojione index](https://github.com/bonusly/gemojione/blob/master/config/index.json). | -| `message` | string | no | The message to set as a status. It can also contain emoji codes. | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `emoji` | string | no | The name of the emoji to use as status. If omitted `speech_balloon` is used. Emoji name can be one of the specified names in the [Gemojione index](https://github.com/bonusly/gemojione/blob/master/config/index.json). | +| `message` | string | no | The message to set as a status. It can also contain emoji codes. | When both parameters `emoji` and `message` are empty, the status will be cleared. @@ -660,9 +660,9 @@ Example responses Get the counts (same as in top right menu) of the currently signed in user. -| Attribute | Type | Description | -| --------- | ---- | ----------- | -| `merge_requests` | number | Merge requests that are active and assigned to current user. | +| Attribute | Type | Description | +| ---------------- | ------ | ------------------------------------------------------------ | +| `merge_requests` | number | Merge requests that are active and assigned to current user. | ```plaintext GET /user_counts @@ -721,8 +721,8 @@ Get a list of a specified user's SSH keys. GET /users/:id_or_username/keys ``` -| Attribute | Type | Required | Description | -| ---------------- | ------ | -------- | ----------- | +| Attribute | Type | Required | Description | +| ---------------- | ------ | -------- | ------------------------------------------------------- | | `id_or_username` | string | yes | The ID or username of the user to get the SSH keys for. | ## Single SSH key @@ -758,13 +758,13 @@ Parameters: - `title` (required) - new SSH Key's title - `key` (required) - new SSH key +- `expires_at` (optional) - The expiration date of the SSH key in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) ```json { - "created_at": "2015-01-21T17:44:33.512Z", - "key": "ssh-dss AAAAB3NzaC1kc3MAAACBAMLrhYgI3atfrSD6KDas1b/3n6R/HP+bLaHHX6oh+L1vg31mdUqK0Ac/NjZoQunavoyzqdPYhFz9zzOezCrZKjuJDS3NRK9rspvjgM0xYR4d47oNZbdZbwkI4cTv/gcMlquRy0OvpfIvJtjtaJWMwTLtM5VhRusRuUlpH99UUVeXAAAAFQCVyX+92hBEjInEKL0v13c/egDCTQAAAIEAvFdWGq0ccOPbw4f/F8LpZqvWDydAcpXHV3thwb7WkFfppvm4SZte0zds1FJ+Hr8Xzzc5zMHe6J4Nlay/rP4ewmIW7iFKNBEYb/yWa+ceLrs+TfR672TaAgO6o7iSRofEq5YLdwgrwkMmIawa21FrZ2D9SPao/IwvENzk/xcHu7YAAACAQFXQH6HQnxOrw4dqf0NqeKy1tfIPxYYUZhPJfo9O0AmBW2S36pD2l14kS89fvz6Y1g8gN/FwFnRncMzlLY/hX70FSc/3hKBSbH6C6j8hwlgFKfizav21eS358JJz93leOakJZnGb8XlWvz1UJbwCsnR2VEY8Dz90uIk1l/UqHkA= loic@call", "title": "ABC", - "id": 4 + "key": "ssh-dss AAAAB3NzaC1kc3MAAACBAMLrhYgI3atfrSD6KDas1b/3n6R/HP+bLaHHX6oh+L1vg31mdUqK0Ac/NjZoQunavoyzqdPYhFz9zzOezCrZKjuJDS3NRK9rspvjgM0xYR4d47oNZbdZbwkI4cTv/gcMlquRy0OvpfIvJtjtaJWMwTLtM5VhRusRuUlpH99UUVeXAAAAFQCVyX+92hBEjInEKL0v13c/egDCTQAAAIEAvFdWGq0ccOPbw4f/F8LpZqvWDydAcpXHV3thwb7WkFfppvm4SZte0zds1FJ+Hr8Xzzc5zMHe6J4Nlay/rP4ewmIW7iFKNBEYb/yWa+ceLrs+TfR672TaAgO6o7iSRofEq5YLdwgrwkMmIawa21FrZ2D9SPao/IwvENzk/xcHu7YAAACAQFXQH6HQnxOrw4dqf0NqeKy1tfIPxYYUZhPJfo9O0AmBW2S36pD2l14kS89fvz6Y1g8gN/FwFnRncMzlLY/hX70FSc/3hKBSbH6C6j8hwlgFKfizav21eS358JJz93leOakJZnGb8XlWvz1UJbwCsnR2VEY8Dz90uIk1l/UqHkA= loic@call", + "expires_at": "2016-01-21T00:00:00.000Z" } ``` @@ -797,6 +797,7 @@ Parameters: - `id` (required) - ID of specified user - `title` (required) - new SSH Key's title - `key` (required) - new SSH key +- `expires_at` (optional) - The expiration date of the SSH key in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) ## Delete SSH key for current user @@ -858,8 +859,8 @@ GET /user/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | ----------- | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | | `key_id` | integer | yes | The ID of the GPG key | ```shell @@ -886,8 +887,8 @@ POST /user/gpg_keys Parameters: -| Attribute | Type | Required | Description | -| --------- | ------ | -------- | ----------- | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | --------------- | | key | string | yes | The new GPG key | ```shell @@ -916,8 +917,8 @@ DELETE /user/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | ----------- | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | | `key_id` | integer | yes | The ID of the GPG key | ```shell @@ -936,8 +937,8 @@ GET /users/:id/gpg_keys Parameters: -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | ----------- | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ------------------ | | `id` | integer | yes | The ID of the user | ```shell @@ -966,9 +967,9 @@ GET /users/:id/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the user | | `key_id` | integer | yes | The ID of the GPG key | ```shell @@ -995,9 +996,9 @@ POST /users/:id/gpg_keys Parameters: -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the user | | `key_id` | integer | yes | The ID of the GPG key | ```shell @@ -1026,9 +1027,9 @@ DELETE /users/:id/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the user | | `key_id` | integer | yes | The ID of the GPG key | ```shell @@ -1347,12 +1348,12 @@ settings page. POST /users/:user_id/impersonation_tokens ``` -| Attribute | Type | Required | Description | -| ------------ | ------- | -------- | ----------- | -| `user_id` | integer | yes | The ID of the user | -| `name` | string | yes | The name of the impersonation token | -| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`)| -| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) | +| Attribute | Type | Required | Description | +| ------------ | ------- | -------- | --------------------------------------------------------------------------- | +| `user_id` | integer | yes | The ID of the user | +| `name` | string | yes | The name of the impersonation token | +| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`) | +| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "name=mytoken" --data "expires_at=2017-04-04" --data "scopes[]=api" "https://gitlab.example.com/api/v4/users/42/impersonation_tokens" @@ -1392,10 +1393,10 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `user_id` | integer | yes | The ID of the user | -| `impersonation_token_id` | integer | yes | The ID of the impersonation token | +| Attribute | Type | Required | Description | +| ------------------------ | ------- | -------- | --------------------------------- | +| `user_id` | integer | yes | The ID of the user | +| `impersonation_token_id` | integer | yes | The ID of the impersonation token | ### Get user activities (admin only) @@ -1420,9 +1421,9 @@ GET /user/activities Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `from` | string | no | Date string in the format YEAR-MONTH-DAY. For example, `2016-03-11`. Defaults to 6 months ago. | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ---------------------------------------------------------------------------------------------- | +| `from` | string | no | Date string in the format YEAR-MONTH-DAY. For example, `2016-03-11`. Defaults to 6 months ago. | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/user/activities" @@ -1467,10 +1468,10 @@ GET /users/:id/memberships Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a specified user | -| `type` | string | no | Filter memberships by type. Can be either `Project` or `Namespace` | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ------------------------------------------------------------------ | +| `id` | integer | yes | The ID of a specified user | +| `type` | string | no | Filter memberships by type. Can be either `Project` or `Namespace` | Returns: diff --git a/lib/api/users.rb b/lib/api/users.rb index 8457ebb00ed..ed9dac8b494 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -255,6 +255,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' requires :key, type: String, desc: 'The new SSH key' requires :title, type: String, desc: 'The title of the new SSH key' + optional :expires_at, type: DateTime, desc: 'The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)' end # rubocop: disable CodeReuse/ActiveRecord post ":id/keys" do @@ -720,6 +721,7 @@ module API params do requires :key, type: String, desc: 'The new SSH key' requires :title, type: String, desc: 'The title of the new SSH key' + optional :expires_at, type: DateTime, desc: 'The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)' end post "keys" do key = current_user.keys.new(declared_params) diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 329f87d8be8..b82ab30cc34 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -39,9 +39,9 @@ module Gitlab when User instance.user_url(object, **options) when Wiki - wiki_url(object, **options) + wiki_page_url(object, Wiki::HOMEPAGE, **options) when WikiPage - instance.project_wiki_url(object.wiki.project, object.slug, **options) + wiki_page_url(object.wiki, object, **options) when ::DesignManagement::Design design_url(object, **options) else @@ -78,11 +78,11 @@ module Gitlab end end - def wiki_url(object, **options) - if object.container.is_a?(Project) - instance.project_wiki_url(object.container, Wiki::HOMEPAGE, **options) + def wiki_page_url(wiki, page, **options) + if wiki.container.is_a?(Project) + instance.project_wiki_url(wiki.container, page, **options) else - raise NotImplementedError.new("No URL builder defined for #{object.inspect}") + raise NotImplementedError.new("No URL builder defined for #{wiki.container.inspect} wikis") end end diff --git a/lib/tasks/gitlab/container_registry.rake b/lib/tasks/gitlab/container_registry.rake index f414ea0be6c..7687cb237cc 100644 --- a/lib/tasks/gitlab/container_registry.rake +++ b/lib/tasks/gitlab/container_registry.rake @@ -2,10 +2,15 @@ namespace :gitlab do namespace :container_registry do desc "GitLab | Container Registry | Configure" task configure: :gitlab_environment do + configure + end + + def configure registry_config = Gitlab.config.registry unless registry_config.enabled && registry_config.api_url.presence - raise 'Registry is not enabled or registry api url is not present.' + puts "Registry is not enabled or registry api url is not present.".color(:yellow) + return end warn_user_is_not_gitlab diff --git a/package.json b/package.json index 06b23455b4d..8e7852b191e 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/plugin-syntax-import-meta": "^7.8.3", "@babel/preset-env": "^7.8.4", "@gitlab/at.js": "1.5.5", - "@gitlab/svgs": "1.135.0", + "@gitlab/svgs": "1.137.0", "@gitlab/ui": "16.2.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3", diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb index 2671e3db982..4e58822b613 100644 --- a/spec/controllers/projects/wikis_controller_spec.rb +++ b/spec/controllers/projects/wikis_controller_spec.rb @@ -3,282 +3,8 @@ require 'spec_helper' RSpec.describe Projects::WikisController do - let_it_be(:project) { create(:project, :public, :repository) } - let(:user) { project.owner } - let(:project_wiki) { ProjectWiki.new(project, user) } - let(:wiki) { project_wiki.wiki } - let(:wiki_title) { 'page title test' } - - before do - create_page(wiki_title, 'hello world') - - sign_in(user) - end - - after do - destroy_page(wiki_title) - end - - describe 'GET #new' do - subject { get :new, params: { namespace_id: project.namespace, project_id: project } } - - it 'redirects to #show and appends a `random_title` param' do - subject - - expect(response).to have_gitlab_http_status(:found) - expect(Rails.application.routes.recognize_path(response.redirect_url)).to include( - controller: 'projects/wikis', - action: 'show' - ) - expect(response.redirect_url).to match(/\?random_title=true\Z/) - end - end - - describe 'GET #pages' do - subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } } - - it 'does not load the pages content' do - expect(controller).to receive(:load_wiki).and_return(project_wiki) - - expect(project_wiki).to receive(:list_pages).twice.and_call_original - - subject - end - end - - describe 'GET #history' do - before do - allow(controller) - .to receive(:can?) - .with(any_args) - .and_call_original - - # The :create_wiki permission is irrelevant to reading history. - expect(controller) - .not_to receive(:can?) - .with(anything, :create_wiki, any_args) - - allow(controller) - .to receive(:can?) - .with(anything, :read_wiki, any_args) - .and_return(allow_read_wiki) - end - - shared_examples 'fetching history' do |expected_status| - before do - get :history, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } - end - - it "returns status #{expected_status}" do - expect(response).to have_gitlab_http_status(expected_status) - end - end - - it_behaves_like 'fetching history', :ok do - let(:allow_read_wiki) { true } - - it 'assigns @page_versions' do - expect(assigns(:page_versions)).to be_present - end - end - - it_behaves_like 'fetching history', :not_found do - let(:allow_read_wiki) { false } - end - end - - describe 'GET #show' do - render_views - - let(:random_title) { nil } - - subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: id, random_title: random_title } } - - context 'when page exists' do - let(:id) { wiki_title } - - it 'limits the retrieved pages for the sidebar' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:page).title).to eq(wiki_title) - expect(assigns(:sidebar_wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) - expect(assigns(:sidebar_limited)).to be(false) - end - - context 'when page content encoding is invalid' do - it 'sets flash error' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(flash[:notice]).to eq(_('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')) - end - end - end - - context 'when the page does not exist' do - let(:id) { 'does not exist' } - - before do - subject - end - - it 'builds a new wiki page with the id as the title' do - expect(assigns(:page).title).to eq(id) - end - - context 'when a random_title param is present' do - let(:random_title) { true } - - it 'builds a new wiki page with no title' do - expect(assigns(:page).title).to be_empty - end - end - end - - context 'when page is a file' do - include WikiHelpers - - let(:id) { upload_file_to_wiki(project, user, file_name) } - - context 'when file is an image' do - let(:file_name) { 'dk.png' } - - it 'delivers the image' do - subject - - expect(response.headers['Content-Disposition']).to match(/^inline/) - expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" - end - - context 'when file is a svg' do - let(:file_name) { 'unsanitized.svg' } - - it 'delivers the image' do - subject - - expect(response.headers['Content-Disposition']).to match(/^inline/) - expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" - end - end - - it_behaves_like 'project cache control headers' - end - - context 'when file is a pdf' do - let(:file_name) { 'git-cheat-sheet.pdf' } - - it 'sets the content type to sets the content response headers' do - subject - - expect(response.headers['Content-Disposition']).to match(/^inline/) - expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" - end - - it_behaves_like 'project cache control headers' - end - end - end - - describe 'POST #preview_markdown' do - it 'renders json in a correct format' do - post :preview_markdown, params: { namespace_id: project.namespace, project_id: project, id: 'page/path', text: '*Markdown* text' } - - expect(json_response.keys).to match_array(%w(body references)) - end - end - - describe 'GET #edit' do - subject { get(:edit, params: { namespace_id: project.namespace, project_id: project, id: wiki_title }) } - - context 'when page content encoding is invalid' do - it 'redirects to show' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject - - expect(response).to redirect_to_wiki(project, project_wiki.list_pages.first) - end - end - - context 'when the page has nil content' do - let(:page) { create(:wiki_page) } - - it 'redirects to show' do - allow(page).to receive(:content).and_return(nil) - allow(controller).to receive(:find_page).and_return(page) - - subject - - expect(response).to redirect_to_wiki(project, page) - end - end - - context 'when page content encoding is valid' do - render_views - - it 'shows the edit page' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.body).to include(s_('Wiki|Edit Page')) - end - end - end - - describe 'PATCH #update' do - let(:new_title) { 'New title' } - let(:new_content) { 'New content' } - - subject do - patch(:update, - params: { - namespace_id: project.namespace, - project_id: project, - id: wiki_title, - wiki: { title: new_title, content: new_content } - }) - end - - context 'when page content encoding is invalid' do - it 'redirects to show' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject - expect(response).to redirect_to_wiki(project, project_wiki.list_pages.first) - end - end - - context 'when page content encoding is valid' do - render_views - - it 'updates the page' do - subject - - wiki_page = project_wiki.list_pages(load_content: true).first - - expect(wiki_page.title).to eq new_title - expect(wiki_page.content).to eq new_content - end - end - end - - def create_page(name, content) - wiki.write_page(name, :markdown, content, commit_details(name)) - end - - def commit_details(name) - Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}") - end - - def destroy_page(title, dir = '') - page = wiki.page(title: title, dir: dir) - project_wiki.delete_page(page, "test commit") - end - - def redirect_to_wiki(project, page) - redirect_to(controller.project_wiki_path(project, page)) + it_behaves_like 'wiki controller actions' do + let(:container) { create(:project, :public, :repository, namespace: user.namespace) } + let(:routing_params) { { namespace_id: container.namespace, project_id: container } } end end diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index 284aa77c742..b100e035d38 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -176,6 +176,8 @@ RSpec.shared_examples_for 'snippet editor' do click_link('Internal') expect(page).to have_content('My Snippet Title') + created_snippet = Snippet.last + expect(created_snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) end end end diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb index 1955927e2df..2bc0d9fbb42 100644 --- a/spec/helpers/gitlab_routing_helper_spec.rb +++ b/spec/helpers/gitlab_routing_helper_spec.rb @@ -229,4 +229,14 @@ describe GitlabRoutingHelper do end end end + + context 'wikis' do + let(:wiki) { create(:project_wiki) } + + describe '#wiki_page_path' do + it 'returns the url for the wiki page' do + expect(wiki_page_path(wiki, 'page')).to eq("/#{wiki.project.full_path}/-/wikis/page") + end + end + end end diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index c5e38103f03..1fc79a9762a 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -327,7 +327,7 @@ describe MarkupHelper do expect(wiki).to receive(:content).and_return('wiki content') expect(wiki).to receive(:slug).and_return('nested/page') expect(wiki).to receive(:repository).and_return(wiki_repository) - helper.instance_variable_set(:@project_wiki, wiki) + helper.instance_variable_set(:@wiki, wiki) end context 'when file is Markdown' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 1bf2c0c533a..7c0b6ac2e45 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1242,6 +1242,16 @@ describe API::Users, :do_not_mock_admin_mode do end.to change { user.keys.count }.by(1) end + it 'creates SSH key with `expires_at` attribute' do + optional_attributes = { expires_at: '2016-01-21T00:00:00.000Z' } + attributes = attributes_for(:key).merge(optional_attributes) + + post api("/users/#{user.id}/keys", admin), params: attributes + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['expires_at']).to eq(optional_attributes[:expires_at]) + end + it "returns 400 for invalid ID" do post api("/users/0/keys", admin) expect(response).to have_gitlab_http_status(:bad_request) @@ -1798,6 +1808,16 @@ describe API::Users, :do_not_mock_admin_mode do expect(response).to have_gitlab_http_status(:created) end + it 'creates SSH key with `expires_at` attribute' do + optional_attributes = { expires_at: '2016-01-21T00:00:00.000Z' } + attributes = attributes_for(:key).merge(optional_attributes) + + post api("/user/keys", user), params: attributes + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['expires_at']).to eq(optional_attributes[:expires_at]) + end + it "returns a 401 error if unauthorized" do post api("/user/keys"), params: { title: 'some title', key: 'some key' } expect(response).to have_gitlab_http_status(:unauthorized) diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb index e6818ff8f0c..ae0d53d1297 100644 --- a/spec/support/helpers/wiki_helpers.rb +++ b/spec/support/helpers/wiki_helpers.rb @@ -8,14 +8,14 @@ module WikiHelpers find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js) end - def upload_file_to_wiki(project, user, file_name) + def upload_file_to_wiki(container, user, file_name) opts = { file_name: file_name, file_content: File.read(expand_fixture_path(file_name)) } ::Wikis::CreateAttachmentService.new( - container: project, + container: container, current_user: user, params: opts ).execute[:result][:file_path] diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb new file mode 100644 index 00000000000..db6af403112 --- /dev/null +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -0,0 +1,290 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'wiki controller actions' do + let(:container) { raise NotImplementedError } + let(:routing_params) { raise NotImplementedError } + + let_it_be(:user) { create(:user) } + let(:wiki) { Wiki.for_container(container, user) } + let(:wiki_title) { 'page title test' } + + before do + create(:wiki_page, wiki: wiki, title: wiki_title, content: 'hello world') + + sign_in(user) + end + + describe 'GET #new' do + subject { get :new, params: routing_params } + + it 'redirects to #show and appends a `random_title` param' do + subject + + expect(response).to be_redirect + expect(response.redirect_url).to match(%r{ + #{Regexp.quote(wiki.wiki_base_path)} # wiki base path + /[-\h]{36} # page slug + \?random_title=true\Z # random_title param + }x) + end + + context 'when the wiki repository cannot be created' do + before do + expect(Wiki).to receive(:for_container).and_return(wiki) + expect(wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError } + end + + it 'redirects to the wiki container and displays an error message' do + subject + + expect(response).to redirect_to(container) + expect(flash[:notice]).to eq('Could not create Wiki Repository at this time. Please try again later.') + end + end + end + + describe 'GET #pages' do + before do + get :pages, params: routing_params.merge(id: wiki_title) + end + + it 'assigns the page collections' do + expect(assigns(:wiki_pages)).to contain_exactly(an_instance_of(WikiPage)) + expect(assigns(:wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) + end + + it 'does not load the page content' do + expect(assigns(:page)).to be_nil + end + + it 'does not load the sidebar' do + expect(assigns(:sidebar_wiki_entries)).to be_nil + expect(assigns(:sidebar_limited)).to be_nil + end + end + + describe 'GET #history' do + before do + allow(controller) + .to receive(:can?) + .with(any_args) + .and_call_original + + # The :create_wiki permission is irrelevant to reading history. + expect(controller) + .not_to receive(:can?) + .with(anything, :create_wiki, any_args) + + allow(controller) + .to receive(:can?) + .with(anything, :read_wiki, any_args) + .and_return(allow_read_wiki) + end + + shared_examples 'fetching history' do |expected_status| + before do + get :history, params: routing_params.merge(id: wiki_title) + end + + it "returns status #{expected_status}" do + expect(response).to have_gitlab_http_status(expected_status) + end + end + + it_behaves_like 'fetching history', :ok do + let(:allow_read_wiki) { true } + + it 'assigns @page_versions' do + expect(assigns(:page_versions)).to be_present + end + end + + it_behaves_like 'fetching history', :not_found do + let(:allow_read_wiki) { false } + end + end + + describe 'GET #show' do + render_views + + let(:random_title) { nil } + + subject { get :show, params: routing_params.merge(id: id, random_title: random_title) } + + context 'when page exists' do + let(:id) { wiki_title } + + it 'renders the page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:page).title).to eq(wiki_title) + expect(assigns(:sidebar_wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) + expect(assigns(:sidebar_limited)).to be(false) + end + + context 'when page content encoding is invalid' do + it 'sets flash error' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(flash[:notice]).to eq(_('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')) + end + end + end + + context 'when the page does not exist' do + let(:id) { 'does not exist' } + + before do + subject + end + + it 'builds a new wiki page with the id as the title' do + expect(assigns(:page).title).to eq(id) + end + + context 'when a random_title param is present' do + let(:random_title) { true } + + it 'builds a new wiki page with no title' do + expect(assigns(:page).title).to be_empty + end + end + end + + context 'when page is a file' do + include WikiHelpers + + let(:id) { upload_file_to_wiki(container, user, file_name) } + + context 'when file is an image' do + let(:file_name) { 'dk.png' } + + it 'delivers the image' do + subject + + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + + context 'when file is a svg' do + let(:file_name) { 'unsanitized.svg' } + + it 'delivers the image' do + subject + + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + end + + it_behaves_like 'project cache control headers' do + let(:project) { container } + end + end + + context 'when file is a pdf' do + let(:file_name) { 'git-cheat-sheet.pdf' } + + it 'sets the content type to sets the content response headers' do + subject + + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + + it_behaves_like 'project cache control headers' do + let(:project) { container } + end + end + end + end + + describe 'POST #preview_markdown' do + it 'renders json in a correct format' do + post :preview_markdown, params: routing_params.merge(id: 'page/path', text: '*Markdown* text') + + expect(json_response.keys).to match_array(%w(body references)) + end + end + + describe 'GET #edit' do + subject { get(:edit, params: routing_params.merge(id: wiki_title)) } + + context 'when page content encoding is invalid' do + it 'redirects to show' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + + expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first) + end + end + + context 'when the page has nil content' do + let(:page) { create(:wiki_page) } + + it 'redirects to show' do + allow(page).to receive(:content).and_return(nil) + allow(controller).to receive(:page).and_return(page) + + subject + + expect(response).to redirect_to_wiki(wiki, page) + end + end + + context 'when page content encoding is valid' do + render_views + + it 'shows the edit page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to include(s_('Wiki|Edit Page')) + end + end + end + + describe 'PATCH #update' do + let(:new_title) { 'New title' } + let(:new_content) { 'New content' } + + subject do + patch(:update, + params: routing_params.merge( + id: wiki_title, + wiki: { title: new_title, content: new_content } + )) + end + + context 'when page content encoding is invalid' do + it 'redirects to show' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first) + end + end + + context 'when page content encoding is valid' do + render_views + + it 'updates the page' do + subject + + wiki_page = wiki.list_pages(load_content: true).first + + expect(wiki_page.title).to eq new_title + expect(wiki_page.content).to eq new_content + end + end + end + + def redirect_to_wiki(wiki, page) + redirect_to(controller.wiki_page_path(wiki, page)) + end +end diff --git a/spec/tasks/gitlab/container_registry_rake_spec.rb b/spec/tasks/gitlab/container_registry_rake_spec.rb index 41fc348aa24..181d5c8b7c8 100644 --- a/spec/tasks/gitlab/container_registry_rake_spec.rb +++ b/spec/tasks/gitlab/container_registry_rake_spec.rb @@ -16,10 +16,21 @@ describe 'gitlab:container_registry namespace rake tasks' do stub_container_registry_config(enabled: true, api_url: api_url) end + subject { run_rake_task('gitlab:container_registry:configure') } + shared_examples 'invalid config' do it 'does not update the application settings' do - expect { run_rake_task('gitlab:container_registry:configure') } - .to raise_error(/Registry is not enabled or registry api url is not present./) + expect(application_settings).not_to receive(:update!) + + subject + end + + it 'does not raise an error' do + expect { subject }.not_to raise_error + end + + it 'prints a warning message' do + expect { subject }.to output(/Registry is not enabled or registry api url is not present./).to_stdout end end diff --git a/yarn.lock b/yarn.lock index bbad72ea8fd..92a9fbfdf05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -782,10 +782,10 @@ eslint-plugin-vue "^6.2.1" vue-eslint-parser "^7.0.0" -"@gitlab/svgs@1.135.0": - version "1.135.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.135.0.tgz#b190f50c0a744d3b915f11defcdd17d7ae585928" - integrity sha512-ziNYtJ6NXk/XVbptKvgdyVqbYocisPK63oRvWldYDSi/H8IMpdBo0upe+VhjepoEzxuUZ1yxc8Y1JMJ+RTpM+w== +"@gitlab/svgs@1.137.0": + version "1.137.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.137.0.tgz#49fb1f33340cfdf0a47c83b7a613e3c7306dd53c" + integrity sha512-dhyiedyTKYJt/mXV+PjfY2pivAAPh3BAOHpVzNCZj6HmJ9VZFIJDzOAQTTxlxRz4UyPmHPuCiaal63q+JfLzcQ== "@gitlab/ui@16.2.0": version "16.2.0" |