diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index 81cc3e7dd2f..4d6c02336fe 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -142,6 +142,7 @@ dependency_scanning: variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" + DS_EXCLUDED_PATHS: "qa/qa/ee/fixtures/secure_premade_reports,spec,ee/spec" allow_failure: true services: - docker:stable-dind diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index dbd41264aa9..6b4de0a42b0 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.81.0 +1.83.0 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 0eed1a29efd..850e742404b 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.12.0 +1.14.0 diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index bee079c6643..6de9ab9efb3 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -44,6 +44,7 @@ const Api = { releasePath: '/api/:version/projects/:id/releases/:tag_name', mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', adminStatisticsPath: '/api/:version/application/statistics', + pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -448,6 +449,14 @@ const Api = { return axios.get(url); }, + pipelineSingle(id, pipelineId) { + const url = Api.buildUrl(this.pipelineSinglePath) + .replace(':id', encodeURIComponent(id)) + .replace(':pipeline_id', encodeURIComponent(pipelineId)); + + return axios.get(url); + }, + buildUrl(url) { return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version)); }, diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 393948fcede..d22983f2164 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -17,6 +17,25 @@ module Types group.avatar_url(only_path: false) end + field :share_with_group_lock, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Indicates if sharing a project with another group within this group is prevented' + + field :project_creation_level, GraphQL::STRING_TYPE, null: true, method: :project_creation_level_str, + description: 'The permission level required to create projects in the group' + field :subgroup_creation_level, GraphQL::STRING_TYPE, null: true, method: :subgroup_creation_level_str, + description: 'The permission level required to create subgroups within the group' + + field :require_two_factor_authentication, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Indicates if all users in this group are required to set up two-factor authentication' + field :two_factor_grace_period, GraphQL::INT_TYPE, null: true, + description: 'Time before two-factor authentication is enforced' + + field :auto_devops_enabled, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Indicates whether Auto DevOps is enabled for all projects within this group' + + field :emails_disabled, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Indicates if a group has email notifications disabled' + field :mentions_disabled, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if a group is disabled from getting mentioned' diff --git a/app/models/member.rb b/app/models/member.rb index 2654453cf3f..57924161b63 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -82,6 +82,7 @@ class Member < ApplicationRecord scope :with_user, -> (user) { where(user: user) } scope :with_source_id, ->(source_id) { where(source_id: source_id) } + scope :including_source, -> { includes(:source) } scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } diff --git a/app/workers/error_tracking_issue_link_worker.rb b/app/workers/error_tracking_issue_link_worker.rb index 5048f361c31..b306ecc154b 100644 --- a/app/workers/error_tracking_issue_link_worker.rb +++ b/app/workers/error_tracking_issue_link_worker.rb @@ -20,17 +20,11 @@ class ErrorTrackingIssueLinkWorker def perform(issue_id) @issue = Issue.find_by_id(issue_id) - return unless issue && error_tracking && sentry_issue_id + return unless valid? try_obtain_lease do logger.info("Linking Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id}") - if integration_id.nil? - logger.info("Sentry integration unavailable for #{error_tracking.api_url}") - - break - end - sentry_client.create_issue_link(integration_id, sentry_issue_id, issue) rescue Sentry::Client::Error logger.info("Failed to link Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id}") @@ -39,6 +33,10 @@ class ErrorTrackingIssueLinkWorker private + def valid? + issue && error_tracking && sentry_issue_id + end + def error_tracking strong_memoize(:error_tracking) do issue.project.error_tracking_setting diff --git a/changelogs/unreleased/33596-add-pipeline-metadata-to-the-package-registry-ui.yml b/changelogs/unreleased/33596-add-pipeline-metadata-to-the-package-registry-ui.yml new file mode 100644 index 00000000000..6de64dc562e --- /dev/null +++ b/changelogs/unreleased/33596-add-pipeline-metadata-to-the-package-registry-ui.yml @@ -0,0 +1,6 @@ +--- +title: Packages published to the package registry via CI/CD with a CI_JOB_TOKEN will + display pipeline information on the details page +merge_request: 22485 +author: +type: added diff --git a/changelogs/unreleased/admin_user_memberships_endpoint.yml b/changelogs/unreleased/admin_user_memberships_endpoint.yml new file mode 100644 index 00000000000..e1e3bfd144d --- /dev/null +++ b/changelogs/unreleased/admin_user_memberships_endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Add users memberships endpoints for admins +merge_request: 22518 +author: +type: added diff --git a/changelogs/unreleased/allow-minimum-processes-in-sidekiq-cluster.yml b/changelogs/unreleased/allow-minimum-processes-in-sidekiq-cluster.yml new file mode 100644 index 00000000000..e1e2bd21d5d --- /dev/null +++ b/changelogs/unreleased/allow-minimum-processes-in-sidekiq-cluster.yml @@ -0,0 +1,5 @@ +--- +title: Allow setting minimum concurrency for sidekiq-cluster processes +merge_request: 23408 +author: +type: added diff --git a/changelogs/unreleased/feat-backfill-group-graphql-api.yml b/changelogs/unreleased/feat-backfill-group-graphql-api.yml new file mode 100644 index 00000000000..df1c1019bab --- /dev/null +++ b/changelogs/unreleased/feat-backfill-group-graphql-api.yml @@ -0,0 +1,5 @@ +--- +title: Backfill missing GraphQL API Group type properties +merge_request: 23389 +author: Fabio Huser +type: added diff --git a/changelogs/unreleased/gitaly-version-v1-83-0.yml b/changelogs/unreleased/gitaly-version-v1-83-0.yml new file mode 100644 index 00000000000..c36181005f4 --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1-83-0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.83.0 +merge_request: 23431 +author: +type: changed diff --git a/changelogs/unreleased/pages-1-14.yml b/changelogs/unreleased/pages-1-14.yml new file mode 100644 index 00000000000..679b51da3da --- /dev/null +++ b/changelogs/unreleased/pages-1-14.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade pages to 1.14.0 +merge_request: 23317 +author: +type: added diff --git a/changelogs/unreleased/sy-sync-plugin-sentry-issues.yml b/changelogs/unreleased/sy-sync-plugin-sentry-issues.yml new file mode 100644 index 00000000000..a1165adde8f --- /dev/null +++ b/changelogs/unreleased/sy-sync-plugin-sentry-issues.yml @@ -0,0 +1,5 @@ +--- +title: Sync GitLab issues with Sentry plugin integration +merge_request: 23355 +author: +type: added diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md index acb57debe26..8ab75da3501 100644 --- a/doc/administration/operations/extra_sidekiq_processes.md +++ b/doc/administration/operations/extra_sidekiq_processes.md @@ -124,9 +124,18 @@ number of threads that equals the number of queues, plus one spare thread. For example, a process that handles the `process_commit` and `post_receive` queues will use three threads in total. -## Limiting concurrency +## Managing concurrency -To limit the concurrency of the Sidekiq process: +When setting the maximum concurrency, keep in mind this normally should +not exceed the number of CPU cores available. The values in the examples +below are arbitrary and not particular recommendations. + +Each thread requires a Redis connection, so adding threads may increase Redis +latency and potentially cause client timeouts. See the [Sidekiq documentation +about Redis](https://github.com/mperham/sidekiq/wiki/Using-Redis) for more +details. + +### When running a single Sidekiq process (default) 1. Edit `/etc/gitlab/gitlab.rb` and add: @@ -140,11 +149,14 @@ To limit the concurrency of the Sidekiq process: sudo gitlab-ctl reconfigure ``` -To limit the max concurrency of the Sidekiq cluster processes: +This will set the concurrency (number of threads) for the Sidekiq process. + +### When running Sidekiq cluster 1. Edit `/etc/gitlab/gitlab.rb` and add: ```ruby + sidekiq_cluster['min_concurrency'] = 15 sidekiq_cluster['max_concurrency'] = 25 ``` @@ -154,14 +166,21 @@ To limit the max concurrency of the Sidekiq cluster processes: sudo gitlab-ctl reconfigure ``` -For each queue group, the concurrency factor will be set to `min(number of queues, N)`. -Setting the value to 0 will disable the limit. Keep in mind this normally would -not exceed the number of CPU cores available. +`min_concurrency` and `max_concurrency` are independent; one can be set without +the other. Setting `min_concurrency` to 0 will disable the limit. -Each thread requires a Redis connection, so adding threads may -increase Redis latency and potentially cause client timeouts. See the [Sidekiq -documentation about Redis](https://github.com/mperham/sidekiq/wiki/Using-Redis) -for more details. +For each queue group, let N be one more than the number of queues. The +concurrency factor will be set to: + +1. `N`, if it's between `min_concurrency` and `max_concurrency`. +1. `max_concurrency`, if `N` exceeds this value. +1. `min_concurrency`, if `N` is less than this value. + +If `min_concurrency` is equal to `max_concurrency`, then this value will be used +regardless of the number of queues. + +When `min_concurrency` is greater than `max_concurrency`, it is treated as +being equal to `max_concurrency`. ## Modifying the check interval diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md index 584a4ecb89c..a9ebffbc606 100644 --- a/doc/api/access_requests.md +++ b/doc/api/access_requests.md @@ -2,7 +2,7 @@ >**Note:** This feature was introduced in GitLab 8.11 - **Valid access levels** +## Valid access levels The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index f3bf45b0b3d..7d18a0abfe5 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2434,6 +2434,11 @@ type GrafanaIntegration { } type Group { + """ + Indicates whether Auto DevOps is enabled for all projects within this group + """ + autoDevopsEnabled: Boolean + """ Avatar URL of the group """ @@ -2449,6 +2454,11 @@ type Group { """ descriptionHtml: String + """ + Indicates if a group has email notifications disabled + """ + emailsDisabled: Boolean + """ Find a single epic """ @@ -2623,6 +2633,11 @@ type Group { """ path: String! + """ + The permission level required to create projects in the group + """ + projectCreationLevel: String + """ Projects within this namespace """ @@ -2658,11 +2673,26 @@ type Group { """ requestAccessEnabled: Boolean + """ + Indicates if all users in this group are required to set up two-factor authentication + """ + requireTwoFactorAuthentication: Boolean + """ Aggregated storage statistics of the namespace. Only available for root namespaces """ rootStorageStatistics: RootStorageStatistics + """ + Indicates if sharing a project with another group within this group is prevented + """ + shareWithGroupLock: Boolean + + """ + The permission level required to create subgroups within the group + """ + subgroupCreationLevel: String + """ Time logged in issues by group members """ @@ -2698,6 +2728,11 @@ type Group { startDate: Time! ): TimelogConnection! + """ + Time before two-factor authentication is enforced + """ + twoFactorGracePeriod: Int + """ Permissions for the current user on the resource """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 645df8c0184..9f888eb89c4 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -3044,6 +3044,20 @@ "name": "Group", "description": null, "fields": [ + { + "name": "autoDevopsEnabled", + "description": "Indicates whether Auto DevOps is enabled for all projects within this group", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "avatarUrl", "description": "Avatar URL of the group", @@ -3086,6 +3100,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "emailsDisabled", + "description": "Indicates if a group has email notifications disabled", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "epic", "description": "Find a single epic", @@ -3524,6 +3552,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "projectCreationLevel", + "description": "The permission level required to create projects in the group", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "projects", "description": "Projects within this namespace", @@ -3605,6 +3647,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "requireTwoFactorAuthentication", + "description": "Indicates if all users in this group are required to set up two-factor authentication", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "rootStorageStatistics", "description": "Aggregated storage statistics of the namespace. Only available for root namespaces", @@ -3619,6 +3675,34 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "shareWithGroupLock", + "description": "Indicates if sharing a project with another group within this group is prevented", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subgroupCreationLevel", + "description": "The permission level required to create subgroups within the group", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "timelogs", "description": "Time logged in issues by group members", @@ -3704,6 +3788,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "twoFactorGracePeriod", + "description": "Time before two-factor authentication is enforced", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "userPermissions", "description": "Permissions for the current user on the resource", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6696863faff..1177ffff36c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -393,6 +393,13 @@ Autogenerated return type of EpicTreeReorder | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource | | `webUrl` | String! | Web URL of the group | | `avatarUrl` | String | Avatar URL of the group | +| `shareWithGroupLock` | Boolean | Indicates if sharing a project with another group within this group is prevented | +| `projectCreationLevel` | String | The permission level required to create projects in the group | +| `subgroupCreationLevel` | String | The permission level required to create subgroups within the group | +| `requireTwoFactorAuthentication` | Boolean | Indicates if all users in this group are required to set up two-factor authentication | +| `twoFactorGracePeriod` | Int | Time before two-factor authentication is enforced | +| `autoDevopsEnabled` | Boolean | Indicates whether Auto DevOps is enabled for all projects within this group | +| `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled | | `mentionsDisabled` | Boolean | Indicates if a group is disabled from getting mentioned | | `parent` | Group | Parent group | | `epicsEnabled` | Boolean | Indicates if Epics are enabled for namespace | diff --git a/doc/api/users.md b/doc/api/users.md index 4ba524d6d1d..7694345d08b 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1405,3 +1405,53 @@ Example response: ``` Please note that `last_activity_at` is deprecated, please use `last_activity_on`. + +## User memberships (admin only) + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/22518) in GitLab 12.8. + +Lists all projects and groups a user is a member of. This endpoint is available for admins only. +It returns the `source_id`, `source_name`, `source_type` and `access_level` of a membership. +Source can be of type `Namespace` (representing a group) or `Project`. The response represents only direct memberships. Inherited memberships, for example in subgroups, will not be included. +Access levels will be represented by an integer value. Read more about the meaning of access level values [here](access_requests.md#valid-access-levels). + +``` +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` | + +Returns: + +- `200 OK` on success. +- `404 User Not Found` if user cannot be found. +- `403 Forbidden` when not requested by an admin. +- `400 Bad Request` when requested type is not supported. + +```bash +curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/users//memberships +``` + +Example response: + +```json +[ + { + "source_id": 1, + "source_name": "Project one", + "source_type": "Project", + "access_level": "20" + }, + { + "source_id": 3, + "source_name": "Group three", + "source_type": "Namespace", + "access_level": "20" + }, +] +``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 97be90030c8..899418831bf 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -2,6 +2,15 @@ module API module Entities + class Membership < Grape::Entity + expose :source_id + expose :source_name do |member| + member.source.name + end + expose :source_type + expose :access_level + end + class BlameRangeCommit < Grape::Entity expose :id expose :parent_ids diff --git a/lib/api/users.rb b/lib/api/users.rb index 120f66b6a71..eba7c50435c 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -533,6 +533,32 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + desc 'Get memberships' do + success Entities::Membership + end + params do + requires :user_id, type: Integer, desc: 'The ID of the user' + optional :type, type: String, values: %w[Project Namespace] + use :pagination + end + get ":user_id/memberships" do + authenticated_as_admin! + user = find_user_by_id(params) + + members = case params[:type] + when 'Project' + user.project_members + when 'Namespace' + user.group_members + else + user.members + end + + members = members.including_source + + present paginate(members), with: Entities::Membership + end + params do requires :user_id, type: Integer, desc: 'The ID of the user' end diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index 8898960c24d..36ec1caf80c 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -54,6 +54,12 @@ module Sentry end end + def http_post(url, params = {}) + http_request do + Gitlab::HTTP.post(url, **request_params.merge(body: params.to_json)) + end + end + def http_request response = handle_request_exceptions do yield diff --git a/lib/sentry/client/issue_link.rb b/lib/sentry/client/issue_link.rb index 200b1a6b435..91498c19f8b 100644 --- a/lib/sentry/client/issue_link.rb +++ b/lib/sentry/client/issue_link.rb @@ -3,8 +3,22 @@ module Sentry class Client module IssueLink - def create_issue_link(integration_id, sentry_issue_identifier, issue) - issue_link_url = issue_link_api_url(integration_id, sentry_issue_identifier) + # Creates a link in Sentry corresponding to the provided + # Sentry issue and GitLab issue + # @param integration_id [Integer, nil] Representing a global + # GitLab integration in Sentry. Nil for plugins. + # @param sentry_issue_id [Integer] Id for an issue from Sentry + # @param issue [Issue] Issue for which the link should be created + def create_issue_link(integration_id, sentry_issue_id, issue) + return create_plugin_link(sentry_issue_id, issue) unless integration_id + + create_global_integration_link(integration_id, sentry_issue_id, issue) + end + + private + + def create_global_integration_link(integration_id, sentry_issue_id, issue) + issue_link_url = global_integration_link_api_url(integration_id, sentry_issue_id) params = { project: issue.project.id, @@ -14,11 +28,22 @@ module Sentry http_put(issue_link_url, params) end - private - - def issue_link_api_url(integration_id, sentry_issue_identifier) + def global_integration_link_api_url(integration_id, sentry_issue_id) issue_link_url = URI(url) - issue_link_url.path = "/api/0/groups/#{sentry_issue_identifier}/integrations/#{integration_id}/" + issue_link_url.path = "/api/0/groups/#{sentry_issue_id}/integrations/#{integration_id}/" + + issue_link_url + end + + def create_plugin_link(sentry_issue_id, issue) + issue_link_url = plugin_link_api_url(sentry_issue_id) + + http_post(issue_link_url, issue_id: issue.iid) + end + + def plugin_link_api_url(sentry_issue_id) + issue_link_url = URI(url) + issue_link_url.path = "/api/0/issues/#{sentry_issue_id}/plugins/gitlab/link/" issue_link_url end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index aad7e54b9db..788d4d43b70 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13026,6 +13026,12 @@ msgstr "" msgid "PackageRegistry|There was a problem fetching the details for this package." msgstr "" +msgid "PackageRegistry|There was an error fetching the pipeline information." +msgstr "" + +msgid "PackageRegistry|Unable to fetch pipeline information" +msgstr "" + msgid "PackageRegistry|Unable to load package" msgstr "" diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 340f6dc0356..ec78759f6c8 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -66,14 +66,23 @@ module QA metadata[:type] = :feature end - config.before do + config.before(:suite) do unless browser.rspec_configured browser.rspec_configured = true ## # Perform before hooks, which are different for CE and EE # - Runtime::Release.perform_before_hooks + begin + Runtime::Release.perform_before_hooks + rescue + saved = Capybara::Screenshot.screenshot_and_save_page + + QA::Runtime::Logger.error("Screenshot: #{saved[:image]}") if saved&.key?(:image) + QA::Runtime::Logger.error("HTML capture: #{saved[:html]}") if saved&.key?(:html) + + raise + end end end end diff --git a/spec/factories/project_error_tracking_settings.rb b/spec/factories/project_error_tracking_settings.rb index 5d3fb284eef..7af881f4214 100644 --- a/spec/factories/project_error_tracking_settings.rb +++ b/spec/factories/project_error_tracking_settings.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :project_error_tracking_setting, class: 'ErrorTracking::ProjectErrorTrackingSetting' do project - api_url { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + api_url { 'https://gitlab.com/api/0/projects/sentry-org/sentry-project' } enabled { true } token { 'access_token_123' } project_name { 'Sentry Project' } diff --git a/spec/features/error_tracking/user_sees_error_details_spec.rb b/spec/features/error_tracking/user_sees_error_details_spec.rb deleted file mode 100644 index 6f72c44c689..00000000000 --- a/spec/features/error_tracking/user_sees_error_details_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'View error details page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do - include_context 'sentry error tracking context feature' - - context 'with current user as project owner' do - before do - sign_in(project.owner) - - visit details_project_error_tracking_index_path(project, issue_id: issue_id) - end - - it_behaves_like 'error tracking show page' - end - - context 'with current user as project guest' do - let_it_be(:user) { create(:user) } - - before do - project.add_guest(user) - sign_in(user) - - visit details_project_error_tracking_index_path(project, issue_id: issue_id) - end - - it 'renders not found' do - expect(page).to have_content('Page Not Found') - end - end -end diff --git a/spec/features/error_tracking/user_sees_error_index_spec.rb b/spec/features/error_tracking/user_sees_error_index_spec.rb deleted file mode 100644 index 0d23df31e29..00000000000 --- a/spec/features/error_tracking/user_sees_error_index_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'View error details page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do - include_context 'sentry error tracking context feature' - - let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') } - let_it_be(:issues_response) { JSON.parse(issues_response_body) } - let(:issues_api_url) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved" } - - before do - stub_request(:get, issues_api_url).with( - headers: { 'Authorization' => 'Bearer access_token_123' } - ).to_return(status: 200, body: issues_response_body, headers: { 'Content-Type' => 'application/json' }) - end - - context 'with current user as project owner' do - before do - sign_in(project.owner) - - visit project_error_tracking_index_path(project) - end - - it_behaves_like 'error tracking index page' - end - - # A bug caused the detail link to be broken for all users but the project owner - context 'with current user as project maintainer' do - let_it_be(:user) { create(:user) } - - before do - project.add_maintainer(user) - sign_in(user) - - visit project_error_tracking_index_path(project) - end - - it_behaves_like 'error tracking index page' - end - - context 'with error tracking settings disabled' do - before do - project_error_tracking_settings.update(enabled: false) - sign_in(project.owner) - - visit project_error_tracking_index_path(project) - end - - it 'renders call to action' do - expect(page).to have_content('Enable error tracking') - end - end - - context 'with current user as project guest' do - let_it_be(:user) { create(:user) } - - before do - project.add_guest(user) - sign_in(user) - - visit project_error_tracking_index_path(project) - end - - it 'renders not found' do - expect(page).to have_content('Page Not Found') - end - end -end diff --git a/spec/fixtures/api/schemas/public_api/v4/membership.json b/spec/fixtures/api/schemas/public_api/v4/membership.json new file mode 100644 index 00000000000..3412076f84a --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/membership.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties" : { + "source_id": { "type": "integer" }, + "source_name": { "type": "string" }, + "source_type": { "type": "string" }, + "access_level": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/memberships.json b/spec/fixtures/api/schemas/public_api/v4/memberships.json new file mode 100644 index 00000000000..54c98b9cb99 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/memberships.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "membership.json" } +} diff --git a/spec/fixtures/sentry/issue_link_sample_response.json b/spec/fixtures/sentry/global_integration_link_sample_response.json similarity index 100% rename from spec/fixtures/sentry/issue_link_sample_response.json rename to spec/fixtures/sentry/global_integration_link_sample_response.json diff --git a/spec/fixtures/sentry/issue_sample_response.json b/spec/fixtures/sentry/issue_sample_response.json index 43d55f584b8..a320a21de34 100644 --- a/spec/fixtures/sentry/issue_sample_response.json +++ b/spec/fixtures/sentry/issue_sample_response.json @@ -38,7 +38,7 @@ }, "firstSeen": "2018-11-06T21:19:55Z", "hasSeen": false, - "id": "11", + "id": "503504", "isBookmarked": false, "isPublic": false, "isSubscribed": true, @@ -72,64 +72,232 @@ "shortId": "PUMP-STATION-1", "stats": { "24h": [ - [1541451600.0, 557], - [1541455200.0, 473], - [1541458800.0, 914], - [1541462400.0, 991], - [1541466000.0, 925], - [1541469600.0, 881], - [1541473200.0, 182], - [1541476800.0, 490], - [1541480400.0, 820], - [1541484000.0, 322], - [1541487600.0, 836], - [1541491200.0, 565], - [1541494800.0, 758], - [1541498400.0, 880], - [1541502000.0, 677], - [1541505600.0, 381], - [1541509200.0, 814], - [1541512800.0, 329], - [1541516400.0, 446], - [1541520000.0, 731], - [1541523600.0, 111], - [1541527200.0, 926], - [1541530800.0, 772], - [1541534400.0, 400], - [1541538000.0, 943] + [ + 1541451600.0, + 557 + ], + [ + 1541455200.0, + 473 + ], + [ + 1541458800.0, + 914 + ], + [ + 1541462400.0, + 991 + ], + [ + 1541466000.0, + 925 + ], + [ + 1541469600.0, + 881 + ], + [ + 1541473200.0, + 182 + ], + [ + 1541476800.0, + 490 + ], + [ + 1541480400.0, + 820 + ], + [ + 1541484000.0, + 322 + ], + [ + 1541487600.0, + 836 + ], + [ + 1541491200.0, + 565 + ], + [ + 1541494800.0, + 758 + ], + [ + 1541498400.0, + 880 + ], + [ + 1541502000.0, + 677 + ], + [ + 1541505600.0, + 381 + ], + [ + 1541509200.0, + 814 + ], + [ + 1541512800.0, + 329 + ], + [ + 1541516400.0, + 446 + ], + [ + 1541520000.0, + 731 + ], + [ + 1541523600.0, + 111 + ], + [ + 1541527200.0, + 926 + ], + [ + 1541530800.0, + 772 + ], + [ + 1541534400.0, + 400 + ], + [ + 1541538000.0, + 943 + ] ], "30d": [ - [1538870400.0, 565], - [1538956800.0, 12862], - [1539043200.0, 15617], - [1539129600.0, 10809], - [1539216000.0, 15065], - [1539302400.0, 12927], - [1539388800.0, 12994], - [1539475200.0, 13139], - [1539561600.0, 11838], - [1539648000.0, 12088], - [1539734400.0, 12338], - [1539820800.0, 12768], - [1539907200.0, 12816], - [1539993600.0, 15356], - [1540080000.0, 10910], - [1540166400.0, 12306], - [1540252800.0, 12912], - [1540339200.0, 14700], - [1540425600.0, 11890], - [1540512000.0, 11684], - [1540598400.0, 13510], - [1540684800.0, 12625], - [1540771200.0, 12811], - [1540857600.0, 13180], - [1540944000.0, 14651], - [1541030400.0, 14161], - [1541116800.0, 12612], - [1541203200.0, 14316], - [1541289600.0, 14742], - [1541376000.0, 12505], - [1541462400.0, 14180] + [ + 1538870400.0, + 565 + ], + [ + 1538956800.0, + 12862 + ], + [ + 1539043200.0, + 15617 + ], + [ + 1539129600.0, + 10809 + ], + [ + 1539216000.0, + 15065 + ], + [ + 1539302400.0, + 12927 + ], + [ + 1539388800.0, + 12994 + ], + [ + 1539475200.0, + 13139 + ], + [ + 1539561600.0, + 11838 + ], + [ + 1539648000.0, + 12088 + ], + [ + 1539734400.0, + 12338 + ], + [ + 1539820800.0, + 12768 + ], + [ + 1539907200.0, + 12816 + ], + [ + 1539993600.0, + 15356 + ], + [ + 1540080000.0, + 10910 + ], + [ + 1540166400.0, + 12306 + ], + [ + 1540252800.0, + 12912 + ], + [ + 1540339200.0, + 14700 + ], + [ + 1540425600.0, + 11890 + ], + [ + 1540512000.0, + 11684 + ], + [ + 1540598400.0, + 13510 + ], + [ + 1540684800.0, + 12625 + ], + [ + 1540771200.0, + 12811 + ], + [ + 1540857600.0, + 13180 + ], + [ + 1540944000.0, + 14651 + ], + [ + 1541030400.0, + 14161 + ], + [ + 1541116800.0, + 12612 + ], + [ + 1541203200.0, + 14316 + ], + [ + 1541289600.0, + 14742 + ], + [ + 1541376000.0, + 12505 + ], + [ + 1541462400.0, + 14180 + ] ] }, "status": "unresolved", diff --git a/spec/fixtures/sentry/plugin_link_sample_response.json b/spec/fixtures/sentry/plugin_link_sample_response.json new file mode 100644 index 00000000000..2e07e412844 --- /dev/null +++ b/spec/fixtures/sentry/plugin_link_sample_response.json @@ -0,0 +1,6 @@ +{ + "message": "Successfully linked issue.", + "link": "https://gitlab.com/test/tanuki-inc/issues/3", + "id": 3, + "label": "GL-3" +} diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb index de11bad0723..6a0028f6529 100644 --- a/spec/graphql/types/group_type_spec.rb +++ b/spec/graphql/types/group_type_spec.rb @@ -10,7 +10,14 @@ describe GitlabSchema.types['Group'] do it { expect(described_class).to require_graphql_authorizations(:read_group) } it 'has the expected fields' do - expected_fields = %w[web_url avatar_url mentions_disabled parent] + expected_fields = %w[ + id name path full_name full_path description description_html visibility + lfs_enabled request_access_enabled projects root_storage_statistics + web_url avatar_url share_with_group_lock project_creation_level + subgroup_creation_level require_two_factor_authentication + two_factor_grace_period auto_devops_enabled emails_disabled + mentions_disabled parent + ] is_expected.to include_graphql_fields(*expected_fields) end diff --git a/spec/lib/sentry/client/issue_link_spec.rb b/spec/lib/sentry/client/issue_link_spec.rb index 35a69be6de5..3434e93365e 100644 --- a/spec/lib/sentry/client/issue_link_spec.rb +++ b/spec/lib/sentry/client/issue_link_spec.rb @@ -5,18 +5,18 @@ require 'spec_helper' describe Sentry::Client::IssueLink do include SentryClientHelpers - let(:error_tracking_setting) { create(:project_error_tracking_setting, api_url: sentry_url) } - let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } - let(:client) { error_tracking_setting.sentry_client } + let_it_be(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + let_it_be(:error_tracking_setting) { create(:project_error_tracking_setting, api_url: sentry_url) } + let_it_be(:issue) { create(:issue, project: error_tracking_setting.project) } - let(:issue_link_sample_response) { JSON.parse(fixture_file('sentry/issue_link_sample_response.json')) } + let(:client) { error_tracking_setting.sentry_client } + let(:sentry_issue_id) { 11111111 } describe '#create_issue_link' do - let(:integration_id) { 44444 } - let(:sentry_issue_id) { 11111111 } - let(:issue) { create(:issue, project: error_tracking_setting.project) } - let(:sentry_issue_link_url) { "https://sentrytest.gitlab.com/api/0/groups/#{sentry_issue_id}/integrations/#{integration_id}/" } + let(:integration_id) { 44444 } + + let(:issue_link_sample_response) { JSON.parse(fixture_file('sentry/global_integration_link_sample_response.json')) } let(:sentry_api_response) { issue_link_sample_response } let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :put, body: sentry_api_response, status: 201) } @@ -37,5 +37,29 @@ describe Sentry::Client::IssueLink do it_behaves_like 'maps Sentry exceptions', :put end + + context 'when integration_id is not provided' do + let(:sentry_issue_link_url) { "https://sentrytest.gitlab.com/api/0/issues/#{sentry_issue_id}/plugins/gitlab/link/" } + let(:integration_id) { nil } + + let(:issue_link_sample_response) { JSON.parse(fixture_file('sentry/plugin_link_sample_response.json')) } + let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :post, body: sentry_api_response) } + + it_behaves_like 'calls sentry api' + + it { is_expected.to be_present } + + context 'redirects' do + let(:sentry_api_url) { sentry_issue_link_url } + + it_behaves_like 'no Sentry redirects', :post + end + + context 'when exception is raised' do + let(:sentry_request_url) { sentry_issue_link_url } + + it_behaves_like 'maps Sentry exceptions', :post + end + end end end diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb index 2762c5b5cb9..061ebcfdc06 100644 --- a/spec/lib/sentry/client/issue_spec.rb +++ b/spec/lib/sentry/client/issue_spec.rb @@ -8,7 +8,7 @@ describe Sentry::Client::Issue do let(:token) { 'test-token' } let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' } let(:client) { Sentry::Client.new(sentry_url, token) } - let(:issue_id) { 11 } + let(:issue_id) { 503504 } describe '#list_issues' do shared_examples 'issues have correct return type' do |klass| @@ -243,7 +243,7 @@ describe Sentry::Client::Issue do end it 'has a correct external URL' do - expect(subject.external_url).to eq('https://sentrytest.gitlab.com/api/0/issues/11') + expect(subject.external_url).to eq('https://sentrytest.gitlab.com/api/0/issues/503504') end it 'issue has a correct external base url' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5af56aa6f75..4438d3aab82 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -2100,6 +2100,83 @@ describe API::Users do end end + describe "GET /users/:id/memberships" do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let(:requesting_user) { create(:user) } + + before_all do + project.add_guest(user) + group.add_guest(user) + end + + it "responses with 403" do + get api("/users/#{user.id}/memberships", requesting_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'requested by admin user' do + let(:requesting_user) { create(:user, :admin) } + + it "responses successfully" do + get api("/users/#{user.id}/memberships", requesting_user) + + aggregate_failures 'expect successful response including groups and projects' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/memberships') + expect(response).to include_pagination_headers + expect(json_response).to contain_exactly( + a_hash_including('source_type' => 'Project'), + a_hash_including('source_type' => 'Namespace') + ) + end + end + + it 'does not submit N+1 DB queries' do + # Avoid setup queries + get api("/users/#{user.id}/memberships", requesting_user) + + control = ActiveRecord::QueryRecorder.new do + get api("/users/#{user.id}/memberships", requesting_user) + end + + create_list(:project, 5).map { |project| project.add_guest(user) } + + expect do + get api("/users/#{user.id}/memberships", requesting_user) + end.not_to exceed_query_limit(control) + end + + context 'with type filter' do + it "only returns project memberships" do + get api("/users/#{user.id}/memberships?type=Project", requesting_user) + + aggregate_failures do + expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Project')) + expect(json_response).not_to include(a_hash_including('source_type' => 'Namespace')) + end + end + + it "only returns group memberships" do + get api("/users/#{user.id}/memberships?type=Namespace", requesting_user) + + aggregate_failures do + expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Namespace')) + expect(json_response).not_to include(a_hash_including('source_type' => 'Project')) + end + end + + it "recognizes unsupported types" do + get api("/users/#{user.id}/memberships?type=foo", requesting_user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + end + context "user activities", :clean_gitlab_redis_shared_state do let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) } let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) } diff --git a/spec/support/shared_contexts/features/error_tracking_shared_context.rb b/spec/support/shared_contexts/features/error_tracking_shared_context.rb deleted file mode 100644 index 230554ce7ac..00000000000 --- a/spec/support/shared_contexts/features/error_tracking_shared_context.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -shared_context 'sentry error tracking context feature' do - include ReactiveCachingHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:project_error_tracking_settings) { create(:project_error_tracking_setting, project: project) } - let_it_be(:issue_response_body) { fixture_file('sentry/issue_sample_response.json') } - let_it_be(:issue_response) { JSON.parse(issue_response_body) } - let_it_be(:event_response_body) { fixture_file('sentry/issue_latest_event_sample_response.json') } - let_it_be(:event_response) { JSON.parse(event_response_body) } - let(:sentry_api_urls) { Sentry::ApiUrls.new(project_error_tracking_settings.api_url) } - let(:issue_id) { issue_response['id'] } - - before do - stub_request(:get, sentry_api_urls.issue_url(issue_id)).with( - headers: { 'Authorization' => 'Bearer access_token_123' } - ).to_return(status: 200, body: issue_response_body, headers: { 'Content-Type' => 'application/json' }) - stub_request(:get, sentry_api_urls.issue_latest_event_url(issue_id)).with( - headers: { 'Authorization' => 'Bearer access_token_123' } - ).to_return(status: 200, body: event_response_body, headers: { 'Content-Type' => 'application/json' }) - end -end diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb deleted file mode 100644 index 4343ffe9255..00000000000 --- a/spec/support/shared_examples/features/error_tracking_shared_example.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'error tracking index page' do - it 'renders the error index page' do - within('div.js-title-container') do - expect(page).to have_content(project.namespace.name) - expect(page).to have_content(project.name) - end - - within('div.error-list') do - expect(page).to have_content('Error') - expect(page).to have_content('Events') - expect(page).to have_content('Users') - expect(page).to have_content('Last Seen') - end - end - - it 'renders the error index data' do - Timecop.freeze(2020, 01, 01, 12, 0, 0) do - within('div.error-list') do - expect(page).to have_content(issues_response[0]['title']) - expect(page).to have_content(issues_response[0]['count'].to_s) - expect(page).to have_content(issues_response[0]['last_seen']) - expect(page).to have_content('1 year ago') - end - end - end - - context 'when error is clicked' do - before do - click_on issues_response[0]['title'] - end - - it 'loads the error page' do - expect(page).to have_content('Error details') - end - end -end - -shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1| - it 'expands the stack trace context' do - within('div.stacktrace') do - find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line - - expanded_line = find("div.file-holder:nth-child(#{expected_line})") - expect(expanded_line).to have_css('svg.ic-chevron-down') - - event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'][-expected_line]['context'].each do |context| - expect(page).to have_content(context[0]) - end - end - end -end - -shared_examples 'error tracking show page' do - it 'renders the error details' do - release_short_version = issue_response['firstRelease']['shortVersion'] - - Timecop.freeze(2020, 01, 01, 12, 0, 0) do - expect(page).to have_content('1 month ago by raven.scripts.runner in main') - expect(page).to have_content(issue_response['metadata']['title']) - expect(page).to have_content('level: error') - expect(page).to have_content('Error details') - expect(page).to have_content('GitLab Issue: https://gitlab.com/gitlab-org/gitlab/issues/1') - expect(page).to have_content("Sentry event: https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/#{issue_id}") - expect(page).to have_content("First seen: 1 year ago (2018-11-06 9:19:55PM UTC) Release: #{release_short_version}") - expect(page).to have_content('Events: 1') - expect(page).to have_content('Users: 0') - end - end - - it 'renders the stack trace heading' do - expect(page).to have_content('Stack trace') - end - - it 'renders the stack trace' do - event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'].each do |frame| - expect(frame['filename']).not_to be_nil - expect(page).to have_content(frame['filename']) - end - end - - # The first line is expanded by default if no line is selected - it_behaves_like 'expanded stack trace context', selected_line: nil, expected_line: 1 - it_behaves_like 'expanded stack trace context', selected_line: 8, expected_line: 8 -end diff --git a/spec/workers/error_tracking_issue_link_worker_spec.rb b/spec/workers/error_tracking_issue_link_worker_spec.rb index cf16e4a0803..b9206e7e12c 100644 --- a/spec/workers/error_tracking_issue_link_worker_spec.rb +++ b/spec/workers/error_tracking_issue_link_worker_spec.rb @@ -40,14 +40,17 @@ describe ErrorTrackingIssueLinkWorker do end end - shared_examples_for 'terminates after one API request' do + shared_examples_for 'attempts to create a link via plugin' do it 'takes no action' do expect_next_instance_of(Sentry::Client) do |client| expect(client).to receive(:repos).with('sentry-org').and_return([repo]) + expect(client) + .to receive(:create_issue_link) + .with(nil, sentry_issue.sentry_issue_identifier, issue) + .and_return(true) end - expect_any_instance_of(Sentry::Client).not_to receive(:create_issue_link) - expect(subject).to be nil + expect(subject).to be true end end @@ -78,7 +81,7 @@ describe ErrorTrackingIssueLinkWorker do ) end - it_behaves_like 'terminates after one API request' + it_behaves_like 'attempts to create a link via plugin' end context 'when Sentry the GitLab integration is for another project' do @@ -90,7 +93,7 @@ describe ErrorTrackingIssueLinkWorker do ) end - it_behaves_like 'terminates after one API request' + it_behaves_like 'attempts to create a link via plugin' end end end