diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index d29aedff63f..a0df088b088 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -800,7 +800,6 @@ RSpec/ContextWording: - 'ee/spec/services/projects/operations/update_service_spec.rb' - 'ee/spec/services/projects/protect_default_branch_service_spec.rb' - 'ee/spec/services/projects/restore_service_spec.rb' - - 'ee/spec/services/projects/slack_application_install_service_spec.rb' - 'ee/spec/services/projects/transfer_service_spec.rb' - 'ee/spec/services/projects/update_mirror_service_spec.rb' - 'ee/spec/services/projects/update_service_spec.rb' diff --git a/.rubocop_todo/rspec/expect_in_hook.yml b/.rubocop_todo/rspec/expect_in_hook.yml index ceeffd1f098..4b588f265c9 100644 --- a/.rubocop_todo/rspec/expect_in_hook.yml +++ b/.rubocop_todo/rspec/expect_in_hook.yml @@ -4,7 +4,6 @@ RSpec/ExpectInHook: - 'ee/spec/controllers/ee/projects/merge_requests/content_controller_spec.rb' - 'ee/spec/controllers/groups/analytics/productivity_analytics_controller_spec.rb' - 'ee/spec/controllers/groups/seat_usage_controller_spec.rb' - - 'ee/spec/controllers/projects/settings/slacks_controller_spec.rb' - 'ee/spec/controllers/subscriptions_controller_spec.rb' - 'ee/spec/elastic/migrate/20220118150500_delete_orphaned_commits_spec.rb' - 'ee/spec/elastic/migrate/20220119120500_populate_commit_permissions_in_main_index_spec.rb' diff --git a/.rubocop_todo/style/class_and_module_children.yml b/.rubocop_todo/style/class_and_module_children.yml index 4257adbdc6f..81b154f906b 100644 --- a/.rubocop_todo/style/class_and_module_children.yml +++ b/.rubocop_todo/style/class_and_module_children.yml @@ -412,7 +412,6 @@ Style/ClassAndModuleChildren: - 'ee/app/controllers/groups/wikis_controller.rb' - 'ee/app/controllers/oauth/geo_auth_controller.rb' - 'ee/app/controllers/profiles/billings_controller.rb' - - 'ee/app/controllers/profiles/slacks_controller.rb' - 'ee/app/controllers/projects/analytics/issues_analytics_controller.rb' - 'ee/app/controllers/projects/analytics/merge_request_analytics_controller.rb' - 'ee/app/controllers/projects/approver_groups_controller.rb' diff --git a/.rubocop_todo/style/empty_method.yml b/.rubocop_todo/style/empty_method.yml index adf3e8ee9b2..184e0d926d5 100644 --- a/.rubocop_todo/style/empty_method.yml +++ b/.rubocop_todo/style/empty_method.yml @@ -94,7 +94,6 @@ Style/EmptyMethod: - 'ee/app/controllers/projects/security/dast_scanner_profiles_controller.rb' - 'ee/app/controllers/projects/security/dast_site_profiles_controller.rb' - 'ee/app/controllers/projects/security/sast_configuration_controller.rb' - - 'ee/app/controllers/projects/settings/slacks_controller.rb' - 'ee/app/controllers/subscriptions/groups_controller.rb' - 'ee/app/models/ee/epic.rb' - 'ee/app/services/feature_flag_issues/destroy_service.rb' diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml index 10d4a22876a..4fcefa6ef85 100644 --- a/.rubocop_todo/style/guard_clause.yml +++ b/.rubocop_todo/style/guard_clause.yml @@ -274,7 +274,6 @@ Style/GuardClause: - 'ee/app/controllers/profiles/billings_controller.rb' - 'ee/app/controllers/projects/path_locks_controller.rb' - 'ee/app/controllers/projects/security/policies_controller.rb' - - 'ee/app/controllers/projects/settings/slacks_controller.rb' - 'ee/app/controllers/smartcard_controller.rb' - 'ee/app/finders/ee/template_finder.rb' - 'ee/app/finders/iterations_finder.rb' diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml index 78e27951786..9fdfb6c960d 100644 --- a/.rubocop_todo/style/if_unless_modifier.yml +++ b/.rubocop_todo/style/if_unless_modifier.yml @@ -411,7 +411,6 @@ Style/IfUnlessModifier: - 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb' - 'ee/app/controllers/projects/path_locks_controller.rb' - 'ee/app/controllers/projects/push_rules_controller.rb' - - 'ee/app/controllers/projects/settings/slacks_controller.rb' - 'ee/app/finders/security/pipeline_vulnerabilities_finder.rb' - 'ee/app/finders/security/vulnerabilities_finder.rb' - 'ee/app/graphql/mutations/audit_events/external_audit_event_destinations/create.rb' diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue index 89d7a2c958c..c53007b68cf 100644 --- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue @@ -133,8 +133,3 @@ export default { - diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js index b98050b61ea..1aa6568848f 100644 --- a/app/assets/javascripts/content_editor/content_editor.stories.js +++ b/app/assets/javascripts/content_editor/content_editor.stories.js @@ -1,9 +1,11 @@ +import { withGitLabAPIAccess } from 'storybook_addons/gitlab_api_access'; import Api from '~/api'; import { ContentEditor } from './index'; export default { component: ContentEditor, title: 'ce/content_editor/content_editor', + decorators: [withGitLabAPIAccess], }; const Template = (_, { argTypes }) => ({ diff --git a/app/controllers/profiles/slacks_controller.rb b/app/controllers/profiles/slacks_controller.rb new file mode 100644 index 00000000000..7c78c01416a --- /dev/null +++ b/app/controllers/profiles/slacks_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Profiles + class SlacksController < Profiles::ApplicationController + include IntegrationsHelper + + skip_before_action :authenticate_user! + + layout 'application' + + feature_category :integrations + + def edit + @projects = disabled_projects.inc_routes if current_user + end + + def slack_link + project = disabled_projects.find(params[:project_id]) + link = add_to_slack_link(project, Gitlab::CurrentSettings.slack_app_id) + + render json: { add_to_slack_link: link } + end + + private + + def disabled_projects + @disabled_projects ||= current_user + .authorized_projects(Gitlab::Access::MAINTAINER) + .with_slack_application_disabled + end + end +end diff --git a/app/controllers/projects/settings/slacks_controller.rb b/app/controllers/projects/settings/slacks_controller.rb new file mode 100644 index 00000000000..4e55103cb4c --- /dev/null +++ b/app/controllers/projects/settings/slacks_controller.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Projects + module Settings + class SlacksController < Projects::ApplicationController + before_action :handle_oauth_error, only: :slack_auth + before_action :check_oauth_state, only: :slack_auth + before_action :authorize_admin_project! + before_action :slack_integration, only: [:edit, :update] + before_action :service, only: [:destroy, :edit, :update] + + layout 'project_settings' + + feature_category :integrations + + def slack_auth + result = Projects::SlackApplicationInstallService.new(project, current_user, params).execute + + flash[:alert] = result[:message] if result[:status] == :error + + session[:slack_install_success] = true + redirect_to_service_page + end + + def destroy + slack_integration.destroy + + redirect_to_service_page + end + + def edit; end + + def update + if slack_integration.update(slack_integration_params) + flash[:notice] = 'The project alias was updated successfully' + + redirect_to_service_page + else + render :edit + end + end + + private + + def redirect_to_service_page + redirect_to edit_project_settings_integration_path( + project, + project.gitlab_slack_application_integration || project.build_gitlab_slack_application_integration + ) + end + + def check_oauth_state + render_403 unless valid_authenticity_token?(session, params[:state]) + + true + end + + def handle_oauth_error + return unless params[:error] == 'access_denied' + + flash[:alert] = 'Access denied' + redirect_to_service_page + end + + def slack_integration + @slack_integration ||= project.gitlab_slack_application_integration.slack_integration + end + + def service + @service = project.gitlab_slack_application_integration + end + + def slack_integration_params + params.require(:slack_integration).permit(:alias) + end + end + end +end diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb index 5471109e6d5..ffea23bf55d 100644 --- a/app/helpers/integrations_helper.rb +++ b/app/helpers/integrations_helper.rb @@ -136,6 +136,11 @@ module IntegrationsHelper form_data[:jira_issue_transition_id] = integration.jira_issue_transition_id end + if integration.is_a?(::Integrations::GitlabSlackApplication) + form_data[:upgrade_slack_url] = add_to_slack_link(project, slack_app_id) + form_data[:should_upgrade_slack] = integration.upgrade_needed?.to_s + end + form_data end @@ -212,6 +217,28 @@ module IntegrationsHelper event_i18n_map[event] || event.to_s.humanize end + def add_to_slack_link(project, slack_app_id) + query = { + scope: SlackIntegration::SCOPES.join(','), + client_id: slack_app_id, + redirect_uri: slack_auth_project_settings_slack_url(project), + state: form_authenticity_token + } + + "#{::Projects::SlackApplicationInstallService::SLACK_AUTHORIZE_URL}?#{query.to_query}" + end + + def gitlab_slack_application_data(projects) + { + projects: (projects || []).to_json(only: [:id, :name], methods: [:avatar_url, :name_with_namespace]), + sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'), + is_signed_in: current_user.present?.to_s, + slack_link_path: slack_link_profile_slack_path, + gitlab_logo_path: image_path('illustrations/gitlab_logo.svg'), + slack_logo_path: image_path('illustrations/slack_logo.svg') + } + end + extend self private diff --git a/app/models/user.rb b/app/models/user.rb index 3016532fc1c..c5c0353e6f4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2279,6 +2279,14 @@ class User < ApplicationRecord abuse_trust_scores.telesign.order(created_at: :desc).first&.score || 0.0 end + def arkose_global_score + abuse_trust_scores.arkose_global_score.order(created_at: :desc).first&.score || 0.0 + end + + def arkose_custom_score + abuse_trust_scores.arkose_custom_score.order(created_at: :desc).first&.score || 0.0 + end + def trust_scores_for_source(source) abuse_trust_scores.where(source: source) end diff --git a/app/services/projects/slack_application_install_service.rb b/app/services/projects/slack_application_install_service.rb new file mode 100644 index 00000000000..812b8b0a082 --- /dev/null +++ b/app/services/projects/slack_application_install_service.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Projects + class SlackApplicationInstallService < BaseService + include Gitlab::Routing + + # Endpoint to initiate the OAuth flow, redirects to Slack's authorization screen + # https://api.slack.com/authentication/oauth-v2#asking + SLACK_AUTHORIZE_URL = 'https://slack.com/oauth/v2/authorize' + + # Endpoint to exchange the temporary authorization code for an access token + # https://api.slack.com/authentication/oauth-v2#exchanging + SLACK_EXCHANGE_TOKEN_URL = 'https://slack.com/api/oauth.v2.access' + + def execute + slack_data = exchange_slack_token + + return error("Slack: #{slack_data['error']}") unless slack_data['ok'] + + integration = project.gitlab_slack_application_integration \ + || project.create_gitlab_slack_application_integration! + + installation = integration.slack_integration || integration.build_slack_integration + + installation.update!( + bot_user_id: slack_data['bot_user_id'], + bot_access_token: slack_data['access_token'], + team_id: slack_data.dig('team', 'id'), + team_name: slack_data.dig('team', 'name'), + alias: project.full_path, + user_id: slack_data.dig('authed_user', 'id'), + authorized_scope_names: slack_data['scope'] + ) + + update_legacy_installations!(installation) + + success + end + + private + + def exchange_slack_token + query = { + client_id: Gitlab::CurrentSettings.slack_app_id, + client_secret: Gitlab::CurrentSettings.slack_app_secret, + code: params[:code], + # NOTE: Needs to match the `redirect_uri` passed to the authorization endpoint, + # otherwise we get a `bad_redirect_uri` error. + redirect_uri: slack_auth_project_settings_slack_url(project) + } + + Gitlab::HTTP.get(SLACK_EXCHANGE_TOKEN_URL, query: query).to_hash + end + + # Update any legacy SlackIntegration records for the Slack Workspace. Legacy SlackIntegration records + # are any created before our Slack App was upgraded to use Granular Bot Permissions and issue a + # bot_access_token. Any SlackIntegration records for the Slack Workspace will already have the same + # bot_access_token. + def update_legacy_installations!(installation) + updatable_attributes = installation.attributes.slice( + 'user_id', + 'bot_user_id', + 'encrypted_bot_access_token', + 'encrypted_bot_access_token_iv', + 'updated_at' + ) + + SlackIntegration.by_team(installation.team_id).id_not_in(installation.id).each_batch do |batch| + batch_ids = batch.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord + batch.update_all(updatable_attributes) + + ::Integrations::SlackWorkspace::IntegrationApiScope.update_scopes(batch_ids, installation.slack_api_scopes) + end + end + end +end diff --git a/app/views/profiles/slacks/edit.html.haml b/app/views/profiles/slacks/edit.html.haml new file mode 100644 index 00000000000..20274735650 --- /dev/null +++ b/app/views/profiles/slacks/edit.html.haml @@ -0,0 +1,6 @@ +- add_to_breadcrumbs _('Profile'), profile_path +- @hide_top_links = true +- @content_class = 'limit-container-width' +- page_title s_('SlackIntegration|GitLab for Slack') + +.js-gitlab-slack-application{ data: gitlab_slack_application_data(@projects) } diff --git a/app/views/projects/settings/slacks/edit.html.haml b/app/views/projects/settings/slacks/edit.html.haml new file mode 100644 index 00000000000..867b90655e3 --- /dev/null +++ b/app/views/projects/settings/slacks/edit.html.haml @@ -0,0 +1,20 @@ +- page_title _('Edit Slack integration') + +.row.gl-mt-3.gl-mb-3 + .col-lg-3 + %h4.gl-mt-0 + = s_('Integrations|Edit project alias') + + %p= s_('Integrations|You can use this alias in your Slack commands') + .col-lg-9 + = form_errors(@slack_integration) + = form_for(@slack_integration, url: project_settings_slack_path(@project), method: :put, html: { class: 'gl-show-field-errors js-integration-settings-form'}) do |form| + .form-group.row + = form.label :alias, s_('Integrations|Enter your alias'), class: 'col-form-label' + .col-sm-10 + = form.text_field :alias, class: 'form-control', placeholder: @slack_integration.alias, required: true + + .footer-block.row-content-block + = form.submit _('Save changes'), pajamas_button: true +   + = link_to _('Cancel'), edit_project_settings_integration_path(@project, @service), class: 'btn gl-button btn-cancel' diff --git a/config/routes/profile.rb b/config/routes/profile.rb index f42f6a9037d..73c8d63b8ec 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -39,6 +39,13 @@ resource :profile, only: [:show, :update] do put :reset end end + + resource :slack, only: [:edit] do + member do + get :slack_link + end + end + resource :preferences, only: [:show, :update] resources :comment_templates, only: [:index, :show], action: :index diff --git a/config/routes/project.rb b/config/routes/project.rb index f296143dca8..bf73f461629 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -144,6 +144,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + resource :slack, only: [:destroy, :edit, :update] do + get :slack_auth + end + resource :repository, only: [:show, :update], controller: :repository do # TODO: Removed this "create_deploy_token" route after change was made in app/helpers/ci_variables_helper.rb:14 # See MR comment for more detail: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27059#note_311585356 diff --git a/config/webpack.config.js b/config/webpack.config.js index 4b90fe5e108..39052d29287 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -224,6 +224,7 @@ const alias = { test_fixtures_static: path.join(ROOT_PATH, 'spec/frontend/fixtures/static'), test_helpers: path.join(ROOT_PATH, 'spec/frontend_integration/test_helpers'), public: path.join(ROOT_PATH, 'public'), + storybook_addons: path.resolve(ROOT_PATH, 'storybook/config/addons'), }; if (IS_EE) { diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index 758d75ee294..41e71cbac6a 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -1747,7 +1747,7 @@ If the above steps are **not successful**, proceed through the next steps: If different operating systems or different operating system versions are deployed across Geo sites, you should perform a locale data compatibility check before setting up Geo. -Geo uses PostgreSQL and Streaming Replication to replicate data across Geo sites. PostgreSQL uses locale data provided by the operating system's C library for sorting text. If the locale data in the C library is incompatible across Geo sites, erroneous query results that lead to [incorrect behavior on secondary sites](https://gitlab.com/gitlab-org/gitlab/-/issues/360723). +Geo uses PostgreSQL and Streaming Replication to replicate data across Geo sites. PostgreSQL uses locale data provided by the operating system's C library for sorting text. If the locale data in the C library is incompatible across Geo sites, it can cause erroneous query results that lead to [incorrect behavior on secondary sites](https://gitlab.com/gitlab-org/gitlab/-/issues/360723). For example, Ubuntu 18.04 (and earlier) and RHEL/Centos7 (and earlier) are incompatible with their later releases. See the [PostgreSQL wiki for more details](https://wiki.postgresql.org/wiki/Locale_data_changes). diff --git a/doc/administration/silent_mode/index.md b/doc/administration/silent_mode/index.md index d51a06045a5..e6bfe5b58ca 100644 --- a/doc/administration/silent_mode/index.md +++ b/doc/administration/silent_mode/index.md @@ -65,9 +65,9 @@ This section documents the current behavior of GitLab when Silent Mode is enable Incoming emails still raise issues, but the users who sent the emails to [Service Desk](../../user/project/service_desk.md) are not notified of issue creation or comments on their issues. -### Project and group webhooks +### Webhooks -Project and group webhooks are suppressed. The relevant Sidekiq jobs fail 4 times and then disappear, while Silent Mode is enabled. [Issue 393639](https://gitlab.com/gitlab-org/gitlab/-/issues/393639) discusses preventing the Sidekiq jobs from running in the first place. +[Project and group webhooks](../../user/project/integrations/webhooks.md), and [system hooks](../system_hooks.md) are suppressed. The relevant Sidekiq jobs fail 4 times and then disappear, while Silent Mode is enabled. [Issue 393639](https://gitlab.com/gitlab-org/gitlab/-/issues/393639) discusses preventing the Sidekiq jobs from running in the first place. Triggering webhook tests via the UI results in HTTP status 500 responses. diff --git a/doc/api/users.md b/doc/api/users.md index a69bae1c2cf..a148f780f84 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -2219,13 +2219,14 @@ Returns: - `403 Forbidden` if not authenticated as an administrator. - `404 User Not Found` if user cannot be found. -## Create a runner **(FREE SELF)** +## Create a runner **(FREE)** Creates a runner linked to the current user. Prerequisites: - You must be an administrator or have the Owner role of the target namespace or project. +- For `instance_type`, you must be an administrator of the GitLab instance. Be sure to copy or save the `token` in the response, the value cannot be retrieved again. diff --git a/doc/development/fe_guide/storybook.md b/doc/development/fe_guide/storybook.md index 44169f110c7..eaa8f8b4068 100644 --- a/doc/development/fe_guide/storybook.md +++ b/doc/development/fe_guide/storybook.md @@ -78,12 +78,29 @@ a starting point. You can also use the GitLab API Access panel in the Storybook UI to set the GitLab instance URL and access token. -### Using REST API +### Set up API access in your stories + +You should apply the `withGitLabAPIAccess` decorator to the stories that will consume GitLab’s APIs. This decorator +will display a badge indicating that the story won't work without providing the API access parameters: + +```javascript +import { withGitLabAPIAccess } from 'storybook_addons/gitlab_api_access'; +import Api from '~/api'; +import { ContentEditor } from './index'; + +export default { + component: ContentEditor, + title: 'ce/content_editor/content_editor', + decorators: [withGitLabAPIAccess], +}; +``` + +#### Using REST API The Storybook sets up `~/lib/utils/axios_utils` in `storybook/config/preview.js`. Components that use the REST API should work out of the box as long as you provide a valid GitLab instance URL and access token. -### Using GraphQL +#### Using GraphQL To write a story for a component that uses the GraphQL API, use the `createVueApollo` method provided in the Story context. @@ -91,6 +108,7 @@ the Story context. ```javascript import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { withGitLabAPIAccess } from 'storybook_addons/gitlab_api_access'; import WorkspacesList from './list.vue'; Vue.use(VueApollo); @@ -99,6 +117,9 @@ const Template = (_, { argTypes, createVueApollo }) => { return { components: { WorkspacesList }, apolloProvider: createVueApollo(), + provide: { + emptyStateSvgPath: '', + }, props: Object.keys(argTypes), template: '', }; @@ -107,10 +128,10 @@ const Template = (_, { argTypes, createVueApollo }) => { export default { component: WorkspacesList, title: 'ee/remote_development/workspaces_list', + decorators: [withGitLabAPIAccess], }; export const Default = Template.bind({}); Default.args = {}; - ``` diff --git a/doc/user/application_security/vulnerability_report/img/vulnerability_list_table_v13_9.png b/doc/user/application_security/vulnerability_report/img/vulnerability_list_table_v13_9.png deleted file mode 100644 index bfde7cd6c80..00000000000 Binary files a/doc/user/application_security/vulnerability_report/img/vulnerability_list_table_v13_9.png and /dev/null differ diff --git a/doc/user/application_security/vulnerability_report/index.md b/doc/user/application_security/vulnerability_report/index.md index e6c710d1326..9cc723da97d 100644 --- a/doc/user/application_security/vulnerability_report/index.md +++ b/doc/user/application_security/vulnerability_report/index.md @@ -31,6 +31,11 @@ in that row: - False positive **{false-positive}**: The scanner determined this vulnerability to be a false positive. +To open an issue created for a vulnerability, hover over the **Activity** entry, then select the link. +The issue icon (**{issues}**) indicates the issue's status. If Jira issue support is enabled, the +issue link found in the **Activity** entry links out to the issue in Jira. Unlike GitLab issues, the +status of a Jira issue is not shown in the GitLab UI. + ![Example project-level Vulnerability Report](img/project_level_vulnerability_report_v14_5.png) ## Project-level Vulnerability Report @@ -57,7 +62,6 @@ From the Vulnerability Report you can: - [Filter the list of vulnerabilities](#filter-the-list-of-vulnerabilities). - [View more details about a vulnerability](#view-details-of-a-vulnerability). - [View vulnerable source location](#view-vulnerable-source-location) (if available). -- [View an issue raised for a vulnerability](#view-issues-raised-for-a-vulnerability). - [Change the status of vulnerabilities](#change-status-of-vulnerabilities). - [Export details of vulnerabilities](#export-vulnerability-details). - [Sort vulnerabilities by date](#sort-vulnerabilities-by-date-detected). @@ -151,15 +155,6 @@ in the default branch. To view the relevant file, select the filename in the vulnerability's details. -## View issues raised for a vulnerability - -The **Activity** column indicates the number of issues that have been created for the vulnerability. -Hover over an **Activity** entry and select a link go to that issue. The status of whether the issue is open or closed also displays in the hover menu. - -![Display attached issues](img/vulnerability_list_table_v13_9.png) - -If Jira issue support is enabled, the issue link found in the Activity entry links out to the issue in Jira. Unlike GitLab issues, the status of whether a Jira issue is Open or Closed does not display in the GitLab UI. - ## Change status of vulnerabilities > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292636) in GitLab 13.10, all statuses became selectable. diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 8cde6e6a16b..7ec9f72e0b2 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -20,8 +20,6 @@ module API helpers do # EE::API::Projects would override this method def apply_filters(projects) - projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled] - projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled] projects = projects.with_statistics if params[:statistics] projects = projects.joins(:statistics) if params[:order_by].include?('project_statistics') # rubocop: disable CodeReuse/ActiveRecord projects = projects.created_by(current_user).imported.with_import_state if params[:imported] diff --git a/spec/controllers/profiles/slacks_controller_spec.rb b/spec/controllers/profiles/slacks_controller_spec.rb new file mode 100644 index 00000000000..3c47ad1285b --- /dev/null +++ b/spec/controllers/profiles/slacks_controller_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Profiles::SlacksController, feature_category: :integrations do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + + allow(subject).to receive(:current_user).and_return(user) + end + + describe 'GET edit' do + before do + get :edit + end + + it 'renders' do + expect(response).to render_template :edit + end + + it 'assigns projects' do + expect(assigns[:projects]).to eq [] + end + + it 'assigns disabled_projects' do + expect(assigns[:disabled_projects]).to eq [] + end + end + + describe 'GET slack_link' do + let_it_be(:project) { create(:project) } + + context 'when user is not a maintainer of the project' do + before do + project.add_developer(user) + end + + it 'renders 404' do + get :slack_link, params: { project_id: project.id } + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to be_blank + end + end + + context 'when user is a maintainer of the project' do + before do + project.add_maintainer(user) + end + + it 'renders slack link' do + allow(controller).to receive(:add_to_slack_link).and_return('mock_redirect_link') + + get :slack_link, params: { project_id: project.id } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'add_to_slack_link' => 'mock_redirect_link' }) + end + end + end +end diff --git a/spec/controllers/projects/settings/slacks_controller_spec.rb b/spec/controllers/projects/settings/slacks_controller_spec.rb new file mode 100644 index 00000000000..d7eee7c1e86 --- /dev/null +++ b/spec/controllers/projects/settings/slacks_controller_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Settings::SlacksController, feature_category: :integrations do + let_it_be_with_refind(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + def redirect_url(project) + edit_project_settings_integration_path( + project, + Integrations::GitlabSlackApplication.to_param + ) + end + + describe 'GET slack_auth' do + def stub_service(result) + service = double + expect(service).to receive(:execute).and_return(result) + expect(Projects::SlackApplicationInstallService) + .to receive(:new).with(project, user, anything).and_return(service) + end + + context 'when valid CSRF token is provided' do + before do + allow(controller).to receive(:check_oauth_state).and_return(true) + end + + it 'calls service and redirects with no alerts if result is successful' do + stub_service(status: :success) + + get :slack_auth, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(redirect_url(project)) + expect(flash[:alert]).to be_nil + expect(session[:slack_install_success]).to be(true) + end + + it 'calls service and redirects with the alert if there is error' do + stub_service(status: :error, message: 'error') + + get :slack_auth, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(redirect_url(project)) + expect(flash[:alert]).to eq('error') + end + end + + context 'when no CSRF token is provided' do + it 'returns 403' do + get :slack_auth, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when there was an OAuth error' do + it 'redirects with an alert' do + get :slack_auth, params: { namespace_id: project.namespace, project_id: project, error: 'access_denied' } + + expect(flash[:alert]).to eq('Access denied') + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(redirect_url(project)) + end + end + end + + describe 'POST update' do + let_it_be(:integration) { create(:gitlab_slack_application_integration, project: project) } + + let(:params) do + { namespace_id: project.namespace, project_id: project, slack_integration: { alias: new_alias } } + end + + context 'when alias is valid' do + let(:new_alias) { 'foo' } + + it 'updates the record' do + expect do + post :update, params: params + end.to change { integration.reload.slack_integration.alias }.to(new_alias) + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(redirect_url(project)) + end + end + + context 'when alias is invalid' do + let(:new_alias) { '' } + + it 'does not update the record' do + expect do + post :update, params: params + end.not_to change { integration.reload.slack_integration.alias } + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('projects/settings/slacks/edit') + end + end + end + + describe 'DELETE destroy' do + it 'destroys the record' do + create(:gitlab_slack_application_integration, project: project) + + expect do + delete :destroy, params: { namespace_id: project.namespace, project_id: project } + end.to change { project.gitlab_slack_application_integration.reload.slack_integration }.to(nil) + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(redirect_url(project)) + end + end +end diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb index 4f1e6c86fea..ac4f882f872 100644 --- a/spec/helpers/integrations_helper_spec.rb +++ b/spec/helpers/integrations_helper_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper' -RSpec.describe IntegrationsHelper do +RSpec.describe IntegrationsHelper, feature_category: :integrations do + let_it_be_with_refind(:project) { create(:project) } + shared_examples 'is defined for each integration event' do Integration.available_integration_names.each do |integration| events = Integration.integration_name_to_model(integration).new.configurable_events @@ -84,13 +86,60 @@ RSpec.describe IntegrationsHelper do ] end + let(:slack_app_fields) do + [ + :upgrade_slack_url, + :should_upgrade_slack + ] + end + subject { helper.integration_form_data(integration) } - context 'with Slack integration' do - let(:integration) { build(:integrations_slack) } + context 'with a GitLab for Slack App integration' do + let(:integration) { build(:gitlab_slack_application_integration, project: project) } + + let(:redirect_url) do + "http://test.host/#{project.full_path}/-/settings/slack/slack_auth" + end + + before do + allow(helper).to receive(:slack_auth_project_settings_slack_url).and_return(redirect_url) + end + + it { is_expected.to include(*fields, *slack_app_fields) } + it { is_expected.not_to include(*jira_fields) } + + it 'includes app upgrade URL' do + stub_application_setting(slack_app_id: 'MOCK_APP_ID') + + expect(subject[:upgrade_slack_url]).to start_with( + [ + Projects::SlackApplicationInstallService::SLACK_AUTHORIZE_URL, + '?client_id=MOCK_APP_ID', + "&redirect_uri=#{CGI.escape(redirect_url)}" + ].join + ) + end + + it 'includes the flag to upgrade Slack app, set to true' do + expect(subject[:should_upgrade_slack]).to eq 'true' + end + + context 'when the integration includes all necessary scopes' do + let(:integration) { create(:gitlab_slack_application_integration, :all_features_supported, project: project) } + + it 'includes the flag to upgrade Slack app, set to false' do + expect(subject[:should_upgrade_slack]).to eq 'false' + end + end + end + + context 'with Jenkins integration' do + let(:integration) { build(:jenkins_integration) } it { is_expected.to include(*fields) } it { is_expected.not_to include(*jira_fields) } + it { is_expected.not_to include(*slack_app_fields) } specify do expect(subject[:reset_path]).to eq(helper.scoped_reset_integration_path(integration)) @@ -101,10 +150,11 @@ RSpec.describe IntegrationsHelper do end end - context 'Jira service' do + context 'with Jira integration' do let(:integration) { build(:jira_integration) } it { is_expected.to include(*fields, *jira_fields) } + it { is_expected.not_to include(*slack_app_fields) } end end @@ -151,6 +201,66 @@ RSpec.describe IntegrationsHelper do end end + describe '#add_to_slack_link' do + let(:slack_link) { helper.add_to_slack_link(project, 'A12345') } + let(:query) { Rack::Utils.parse_query(URI.parse(slack_link).query) } + + before do + allow(helper).to receive(:form_authenticity_token).and_return('a token') + allow(helper).to receive(:slack_auth_project_settings_slack_url).and_return('http://redirect') + end + + it 'returns the endpoint URL with all needed params' do + expect(slack_link).to start_with(Projects::SlackApplicationInstallService::SLACK_AUTHORIZE_URL) + expect(slack_link).to include('&state=a+token') + + expect(query).to include( + 'scope' => 'commands,chat:write,chat:write.public', + 'client_id' => 'A12345', + 'redirect_uri' => 'http://redirect', + 'state' => 'a token' + ) + end + end + + describe '#gitlab_slack_application_data' do + let_it_be(:projects) { create_list(:project, 3) } + + def relation + Project.id_in(projects.pluck(:id)).inc_routes + end + + before do + allow(helper).to receive(:current_user).and_return(build(:user)) + allow(helper).to receive(:new_session_path).and_return('http://session-path') + end + + it 'includes the required keys' do + additions = helper.gitlab_slack_application_data(relation) + + expect(additions.keys).to include( + :projects, + :sign_in_path, + :is_signed_in, + :slack_link_path, + :gitlab_logo_path, + :slack_logo_path + ) + end + + it 'does not suffer from N+1 performance issues' do + baseline = ActiveRecord::QueryRecorder.new { helper.gitlab_slack_application_data(relation.limit(1)) } + + expect do + helper.gitlab_slack_application_data(relation) + end.not_to exceed_query_limit(baseline) + end + + it 'serializes nil projects without error' do + expect(helper.gitlab_slack_application_data(nil)).to include(projects: '[]') + end + end + describe '#integration_issue_type' do using RSpec::Parameterized::TableSyntax let_it_be(:issue) { create(:issue) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0e3382b4c6f..d4c9df936d6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -8117,4 +8117,48 @@ RSpec.describe User, feature_category: :user_profile do end end end + + describe '#arkose_global_score' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + context 'when the user has an arkose global risk score' do + before do + create(:abuse_trust_score, user: user1, score: 12.0, source: :arkose_global_score) + create(:abuse_trust_score, user: user1, score: 24.0, source: :arkose_global_score) + end + + it 'returns the latest score' do + expect(user1.arkose_global_score).to be(24.0) + end + end + + context 'when the user does not have an arkose global risk score' do + it 'defaults to zero' do + expect(user2.arkose_global_score).to be(0.0) + end + end + end + + describe '#arkose_custom_score' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + context 'when the user has an arkose custom risk score' do + before do + create(:abuse_trust_score, user: user1, score: 12.0, source: :arkose_custom_score) + create(:abuse_trust_score, user: user1, score: 24.0, source: :arkose_custom_score) + end + + it 'returns the latest score' do + expect(user1.arkose_custom_score).to be(24.0) + end + end + + context 'when the user does not have an arkose custom risk score' do + it 'defaults to zero' do + expect(user2.arkose_custom_score).to be(0.0) + end + end + end end diff --git a/spec/services/projects/slack_application_install_service_spec.rb b/spec/services/projects/slack_application_install_service_spec.rb new file mode 100644 index 00000000000..9502562a7d4 --- /dev/null +++ b/spec/services/projects/slack_application_install_service_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::SlackApplicationInstallService, feature_category: :integrations do + let_it_be(:user) { create(:user) } + let_it_be_with_refind(:project) { create(:project) } + + let(:integration) { project.gitlab_slack_application_integration } + let(:installation) { integration.slack_integration } + + let(:slack_app_id) { 'A12345' } + let(:slack_app_secret) { 'secret' } + let(:oauth_code) { 'code' } + let(:params) { { code: oauth_code } } + let(:exchange_url) { described_class::SLACK_EXCHANGE_TOKEN_URL } + let(:redirect_url) { Gitlab::Routing.url_helpers.slack_auth_project_settings_slack_url(project) } + + subject(:service) { described_class.new(project, user, params) } + + before do + stub_application_setting(slack_app_id: slack_app_id, slack_app_secret: slack_app_secret) + + query = { + client_id: slack_app_id, + client_secret: slack_app_secret, + code: oauth_code, + redirect_uri: redirect_url + } + + stub_request(:get, exchange_url) + .with(query: query) + .to_return(body: response.to_json, headers: { 'Content-Type' => 'application/json' }) + end + + context 'when Slack responds with an error' do + let(:response) do + { + ok: false, + error: 'something is wrong' + } + end + + it 'returns error result' do + result = service.execute + + expect(result).to eq(message: 'Slack: something is wrong', status: :error) + end + end + + context 'when Slack responds with an access token' do + let_it_be(:team_id) { 'T11111' } + let_it_be(:team_name) { 'Team name' } + let_it_be(:user_id) { 'U11111' } + let_it_be(:bot_user_id) { 'U99999' } + let_it_be(:bot_access_token) { 'token-XXXXX' } + + let(:response) do + { + ok: true, + app_id: 'A12345', + authed_user: { id: user_id }, + token_type: 'bot', + access_token: bot_access_token, + bot_user_id: bot_user_id, + team: { id: team_id, name: 'Team name' }, + enterprise: { is_enterprise_install: false }, + scope: 'chat:a,chat:b,chat:c' + } + end + + shared_examples 'success response' do + it 'returns success result and creates all needed records' do + result = service.execute + + expect(result).to eq(status: :success) + expect(integration).to be_present + expect(installation).to be_present + expect(installation).to have_attributes( + integration_id: integration.id, + team_id: team_id, + team_name: team_name, + alias: project.full_path, + user_id: user_id, + bot_user_id: bot_user_id, + bot_access_token: bot_access_token, + authorized_scope_names: contain_exactly('chat:a', 'chat:b', 'chat:c') + ) + end + end + + it_behaves_like 'success response' + + context 'when integration record already exists' do + before do + project.create_gitlab_slack_application_integration! + end + + it_behaves_like 'success response' + + context 'when installation record already exists' do + before do + integration.create_slack_integration!( + team_id: 'old value', + team_name: 'old value', + alias: 'old value', + user_id: 'old value', + bot_user_id: 'old value', + bot_access_token: 'old value' + ) + end + + it_behaves_like 'success response' + end + end + + context 'when the team has other Slack installation records' do + let_it_be_with_reload(:other_installation) { create(:slack_integration, team_id: team_id) } + let_it_be_with_reload(:other_legacy_installation) { create(:slack_integration, :legacy, team_id: team_id) } + let_it_be_with_reload(:legacy_installation_for_other_team) { create(:slack_integration, :legacy) } + + it_behaves_like 'success response' + + it 'updates related legacy records' do + travel_to(1.minute.from_now) do + expected_attributes = { + 'user_id' => user_id, + 'bot_user_id' => bot_user_id, + 'bot_access_token' => bot_access_token, + 'updated_at' => Time.current, + 'authorized_scope_names' => %w[chat:a chat:b chat:c] + } + + service.execute + + expect(other_installation).to have_attributes(expected_attributes) + expect(other_legacy_installation).to have_attributes(expected_attributes) + expect(legacy_installation_for_other_team).not_to have_attributes(expected_attributes) + end + end + end + end +end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 4a2740c28b7..972b1890f58 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -109,7 +109,6 @@ - './ee/spec/controllers/profiles/billings_controller_spec.rb' - './ee/spec/controllers/profiles_controller_spec.rb' - './ee/spec/controllers/profiles/keys_controller_spec.rb' -- './ee/spec/controllers/profiles/slacks_controller_spec.rb' - './ee/spec/controllers/profiles/usage_quotas_controller_spec.rb' - './ee/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb' - './ee/spec/controllers/projects/analytics/issues_analytics_controller_spec.rb' @@ -157,7 +156,6 @@ - './ee/spec/controllers/projects/settings/integrations_controller_spec.rb' - './ee/spec/controllers/projects/settings/operations_controller_spec.rb' - './ee/spec/controllers/projects/settings/repository_controller_spec.rb' -- './ee/spec/controllers/projects/settings/slacks_controller_spec.rb' - './ee/spec/controllers/projects/subscriptions_controller_spec.rb' - './ee/spec/controllers/projects/vulnerability_feedback_controller_spec.rb' - './ee/spec/controllers/registrations/company_controller_spec.rb' @@ -2872,7 +2870,6 @@ - './ee/spec/services/projects/protect_default_branch_service_spec.rb' - './ee/spec/services/projects/restore_service_spec.rb' - './ee/spec/services/projects/setup_ci_cd_spec.rb' -- './ee/spec/services/projects/slack_application_install_service_spec.rb' - './ee/spec/services/projects/transfer_service_spec.rb' - './ee/spec/services/projects/update_mirror_service_spec.rb' - './ee/spec/services/projects/update_service_spec.rb' diff --git a/storybook/config/addons/gitlab_api_access/index.js b/storybook/config/addons/gitlab_api_access/index.js new file mode 100644 index 00000000000..44fc390e8ea --- /dev/null +++ b/storybook/config/addons/gitlab_api_access/index.js @@ -0,0 +1 @@ +export { withGitLabAPIAccess } from './preview'; diff --git a/storybook/config/addons/gitlab_api_access/preview.js b/storybook/config/addons/gitlab_api_access/preview.js index 6daeedd0179..54977b2e973 100644 --- a/storybook/config/addons/gitlab_api_access/preview.js +++ b/storybook/config/addons/gitlab_api_access/preview.js @@ -1,8 +1,9 @@ import { addons } from '@storybook/addons'; +import { GlBadge } from '@gitlab/ui'; import { FORCE_REMOUNT } from '@storybook/core-events'; +import VueApollo from 'vue-apollo'; import axios from '~/lib/utils/axios_utils'; import createDefaultClient from '~/lib/graphql'; -import VueApollo from 'vue-apollo'; import { GITLAB_API_ACCESS_UPDATE_EVENT } from './constants'; /** @@ -66,8 +67,16 @@ export const withGitLabAPIAccess = (story, context) => { return { components: { story, + GlBadge, }, - template: '', + template: ` +
+
+ Requires API access +
+ +
+ `, }; }; diff --git a/storybook/config/preview.js b/storybook/config/preview.js index 70c8041bb3c..fc04623071a 100644 --- a/storybook/config/preview.js +++ b/storybook/config/preview.js @@ -2,7 +2,7 @@ import './gon'; import Vue from 'vue'; import translateMixin from '~/vue_shared/translate'; -import { withGitLabAPIAccess, initializeGitLabAPIAccess } from './addons/gitlab_api_access/preview'; +import { initializeGitLabAPIAccess } from './addons/gitlab_api_access/preview'; const stylesheetsRequireCtx = require.context( '../../app/assets/stylesheets', @@ -17,5 +17,3 @@ translateMixin(Vue); stylesheetsRequireCtx('./application.scss'); stylesheetsRequireCtx('./application_utilities.scss'); stylesheetsRequireCtx('./highlight/themes/white.scss'); - -export const decorators = [withGitLabAPIAccess];