diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md index 1fe9715d33..10817235be 100644 --- a/CHANGELOG-EE.md +++ b/CHANGELOG-EE.md @@ -1,5 +1,159 @@ Please view this file on the master branch, on stable branches it's out of date. +## 13.1.0 (2020-06-22) + +### Security (1 change) + +- Ensure passwords and access tokens don't appear in SCIM errors. + +### Removed (2 changes) + +- Revert Add scanner name. !33442 +- Stop recording connection pool metrics for load balancing hosts. !33749 + +### Fixed (35 changes, 2 of them are from the community) + +- Stop recording JSON parser errors due to third party license scanning integrations. !26944 +- Improve Requirements Management empty state. !30716 (Marcin Sedlak-Jakubowski) +- Display warning for issues moved from service desk on frontend. !31803 +- Add truncation for the environment dropdown. !32267 +- Add negated params filter to epics search. !32296 +- Clean up serialized objects in audit_events. !32434 +- Prevent last Group Managed Account owner with access from accidental unlinking. !32473 +- Fix issues with editing epic on Boards sidebar. !32503 (Eulyeon Ko) +- Dependency List shows only vulnerabilities from Gemnasium analyzer instead of all Dependency Scanning reports. !32560 +- Ensure users can unlink Group SAML when the group has SAML disabled. !32655 +- Fix Elasticsearch query error for ES 7.7. !32813 +- Display epic issues with null relative position. !33105 +- Fix description diff delete button not hidden after clicked. !33127 +- Fix Elasticsearch illegal_argument_exception when searching for large files. !33176 +- Support nuget dependencies in the API. !33389 +- Fix GraphQL query to fetch vulnerable projects of subgroups. !33410 +- Preserve order when applying scoped labels by title. !33420 +- Revert breaking changes to allow conan_sources.tgz and conan_export.tgz files in the Conan repository. !33435 +- Improve regex for geo auth keys checker. !33447 +- Fix confidentiality note on epic's comment field to display the correct warning text. !33486 +- Geo - Does not sync files on Object Storage when syncing object storage is enabled, but the Object Storage is disabled for the data type. !33561 +- Fix creating merge requests with approval rules. !33582 +- Specify group url in notification email. !33613 +- Fix column overlap on long texts. !33614 +- Geo - Does not try to unlink file when it not possible to get the file path while cleaning orphaned registries. !33658 +- Geo: Fix synchronization disabled on primary UI. !33760 +- Change the linkType argument for issueLinks query to enum. !33828 +- Allow admins to see project despite of ip restriction set on a group level. !34086 +- Return empty scans when head_pipeline is missing. !34193 +- Render security dashboard for projects without pipeline available. !34219 +- Include Epics in Roadmap whose parents are not part of Roadmap. !34243 +- Geo - Fix Gitlab::Geo::Replication::BaseTransfer for orphaned registries. !34292 +- Fix errors when pushing with an expired license. !34458 +- Enable active class on group packages sidebar navigation items. !34518 +- Fix creating/updating issues with epic_iid parameter on API. !34641 + +### Changed (18 changes) + +- Update header for security config page and change to GlTable. !31471 +- Display expiring subscription banner on more pages. !31497 +- Remove license management from CI/CD settings. !31553 +- Elasticsearch integration started using aliases instead of using indices directly. !32107 +- Geo Form Validations. !32263 +- Move Group-level IP address restriction feature to GitLab Premium. !32396 +- Add link to Node URL. !32471 +- Display Project and Subgroup milestones on Roadmap. !32496 +- Enable CI Minutes-related Feature Flags by default. !32581 +- Remove partial word matching from code search. !32771 +- Add authentication information to usage ping. !32790 +- Add tooltip with description to unknown severity badge icon. !33131 +- Improve the clarity of the MR approvals tooltip UI. !33329 +- Change approved to merged in Contribution Analytics. !33535 +- Update merge train settings checkbox text. !34073 +- Analytics Insights page: Load charts individually, instead of waiting for all data to load before displaying the page. !34290 +- Add polling to metrics widget. !34314 +- Added CI_HAS_OPEN_REQUIREMENTS environment variable. !34419 + +### Performance (11 changes) + +- Fix N+1 queries for Elastic Web Search projects scope. !30346 +- Fix N+1 queries for Elastic Search merge_requests scope. !30546 +- Improve performance of Advanced Search API (Issues scope) by preloading more associations. !30778 +- Update index_ci_builds_on_name_and_security_type_eq_ci_build index to support secret-detection. !31785 +- Fix N+1 queries for Elastic Search projects scope. !32688 +- Fix N+1 queries for Elastic Search milestones scope. !33327 +- Geo - Make registry table SSOT for LFS objects. !33432 +- Harden operations_dashboard_default_dashboard usage query. !33952 +- Harden users_created usage data queries. !33956 +- Harden security ci build job queries. !33966 +- Save composer.json to package metadata. !34494 + +### Added (54 changes, 3 of them are from the community) + +- Add list limit metric to API. !27324 +- Implement Go module proxy MVC (package manager for Go). !27746 (Ethan Reesor) +- Improve Vulnerability Management with Standalone Vulnerabilities. !28212 +- Add viewer and linker for go.mod and go.sum. !28262 (Ethan Reesor @firelizzard) +- Add usage statistics for modsecurity total packets/anomalous packets. !28535 +- REST API membership responses for group owner enqueries include group managed account emails. !30584 +- Add table to Issues Analytics. !30603 +- Add ability to pause and resume Elasticsearch indexing. !30621 +- Show the status of stand-alone secrets analyzer on the configuration page. !31167 +- Support transferring and displaying image uploads on Status Page. !31269 +- Add Pipeline.securityReportSummary to GraphQL. !31550 +- Create test reports table. !31643 +- Show usage graph for each storage type. !31649 +- Add requirements filtering on author username and search by title. !31857 +- Add ability to download patch from vulnerability page. !32000 +- Add callout for user count threshold. !32404 +- Make group/namespace name in email a link to group overview page. !32461 +- Add ability to select Epic while creating a New Issue. !32572 +- Expose test reports on GraphQL. !32599 +- Allow approval rules to be reset to project defaults. !32657 +- Save setting for auto-fix feature. !32690 +- Geo - Make Geo::RegistryConsistencyWorker clean up unused registries. !32695 +- Add policy for auto_fix. !32783 +- Send email notifications on Issue Iteration change. !32817 +- Add support for bulk editing health status and epic on issues. !32875 +- Add quick actions for iterations in issues. !32904 +- Add Elasticsearch to Sidekiq ServerMetrics. !32937 +- Added CI parser for requirements reports. !33031 +- Add state events to burnup chart data. !33048 +- Introduce `userNotesCount` field for VulnerabilityType on GraphQL API. !33058 +- Add project audit events API. !33155 (Julien Millau) +- Introduce `issueLinks` field for VulnerabilityType on GraphQL API. !33173 +- Add Elasticsearch metrics in Rack middleware. !33233 +- Adds NuGet project and license URL to the package details page. !33268 +- Adds a new Dependencies tab for NuGet packages on the package details page. !33303 +- Allow to specify multiple domains when restricting group membership by email domain. !33498 +- Add MR approval stats to group contribution analytics. !33601 +- Create sticky section in security dashboard layout. !33651 +- Add Network Policy Management to the Threat Monitoring page. !33667 +- Show secret_detection in report types. !33682 +- Adds NuGet package icon to package details page. !33701 +- Ability to make PAT expiration optional in self managed instances. !33783 +- Add secret detection for GraphQL API. !33797 +- Show test report status badge on Requirements list. !33848 +- Persist user preferences for boards. !33892 +- Fixes inconsistent package title icon colors. !33933 +- Retry failed vulnerability export background jobs. !33986 +- Add MR note to standalone vulnerability page. !34146 +- Show issue link on security dashboard when vulnerability has an issue. !34157 +- Geo Package Files - Sync Counts. !34205 +- Upgrade to `license_scanning` report v2.1. !34224 +- Display Saved User Lists by Feature Flags. !34294 +- Add csv export button to group security dashboard. !34374 +- Alert Users That Lists Are Modified By API Only. !34559 + +### Other (9 changes, 2 of them are from the community) + +- Update deprecated Vue 3 slot syntax in ee/app/assets/javascripts/vue_shared/security_reports/components/modal.vue. !31966 (Gilang Gumilar) +- Added DB index on confidential epics column. !32443 +- Allow ci minutes reset service to continue in case of failure. !32867 +- Remove unused index for vulnerabiliy confidence levels. !33149 +- Update vulnerabilities badge design in Dependency List. !33417 +- Add geo_primary_replication_events to usage data. !33424 +- Add new status page attributes to usage ping. !33790 +- Remove optimized_elasticsearch_indexes_project feature flag. !33965 +- Relocate Go models. !34338 (Ethan Reesor (@firelizzard)) + + ## 13.0.6 (2020-06-10) ### Security (1 change) diff --git a/CHANGELOG.md b/CHANGELOG.md index 196f184f92..94d635c30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 13.1.1 (2020-06-23) + +### Fixed (4 changes) + +- Fix missing templating vars set from URL in metrics dashboard. !34668 +- Fix edit status dropdown overflow. !34847 +- Load user before logging git http-requests. !34923 +- Do not mask key comments for DeployKeys. !35014 + +### Added (1 change) + +- Periodically recompute project authorizations. !34071 + + ## 13.1.0 (2020-06-22) ### Removed (4 changes, 2 of them are from the community) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 9e6fba6c2c..21b80e995f 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -203182ffe94da165d4ff81332b1b3fff9771e631 +13.1.1 diff --git a/Gemfile b/Gemfile index 5477ee3d18..33d4ebb7f2 100644 --- a/Gemfile +++ b/Gemfile @@ -416,7 +416,7 @@ end gem 'octokit', '~> 4.15' # https://gitlab.com/gitlab-org/gitlab/issues/207207 -gem 'gitlab-mail_room', '~> 0.0.4', require: 'mail_room' +gem 'gitlab-mail_room', '~> 0.0.6', require: 'mail_room' gem 'email_reply_trimmer', '~> 0.1' gem 'html2text' diff --git a/Gemfile.lock b/Gemfile.lock index 319c4df3f9..2e3537e223 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -390,7 +390,7 @@ GEM opentracing (~> 0.4) redis (> 3.0.0, < 5.0.0) gitlab-license (1.0.0) - gitlab-mail_room (0.0.4) + gitlab-mail_room (0.0.6) gitlab-markup (1.7.1) gitlab-net-dns (0.9.1) gitlab-puma (4.3.3.gitlab.2) @@ -1241,7 +1241,7 @@ DEPENDENCIES gitlab-chronic (~> 0.10.5) gitlab-labkit (= 0.12.0) gitlab-license (~> 1.0) - gitlab-mail_room (~> 0.0.4) + gitlab-mail_room (~> 0.0.6) gitlab-markup (~> 1.7.1) gitlab-net-dns (~> 0.9.1) gitlab-puma (~> 4.3.3.gitlab.2) diff --git a/VERSION b/VERSION index e6ba351366..21b80e995f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -13.1.0 +13.1.1 diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 058fab5f4f..5795e75628 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -2,8 +2,8 @@ import { slugify } from '~/lib/utils/text_utility'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { parseTemplatingVariables } from './variable_mapping'; import { NOT_IN_DB_PREFIX, linkTypes } from '../constants'; +import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping'; import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants'; import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range'; import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility'; @@ -289,7 +289,7 @@ export const mapToDashboardViewModel = ({ }) => { return { dashboard, - variables: parseTemplatingVariables(templating), + variables: mergeURLVariables(parseTemplatingVariables(templating)), links: links.map(mapLinksToViewModel), panelGroups: panel_groups.map(mapToPanelGroupViewModel), }; diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js index 66b9899f67..c0a8150063 100644 --- a/app/assets/javascripts/monitoring/stores/variable_mapping.js +++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js @@ -1,4 +1,5 @@ import { isString } from 'lodash'; +import { templatingVariablesFromUrl } from '../utils'; import { VARIABLE_TYPES } from '../constants'; /** @@ -164,4 +165,39 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) => return acc; }, {}); +/** + * Custom variables are defined in the dashboard yml file + * and their values can be passed through the URL. + * + * On component load, this method merges variables data + * from the yml file with URL data to store in the Vuex store. + * Not all params coming from the URL need to be stored. Only + * the ones that have a corresponding variable defined in the + * yml file. + * + * This ensures that there is always a single source of truth + * for variables + * + * This method can be improved further. See the below issue + * https://gitlab.com/gitlab-org/gitlab/-/issues/217713 + * + * @param {Object} varsFromYML template variables from yml file + * @returns {Object} + */ +export const mergeURLVariables = (varsFromYML = {}) => { + const varsFromURL = templatingVariablesFromUrl(); + const variables = {}; + Object.keys(varsFromYML).forEach(key => { + if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) { + variables[key] = { + ...varsFromYML[key], + value: varsFromURL[key], + }; + } else { + variables[key] = varsFromYML[key]; + } + }); + return variables; +}; + export default {}; diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 95d544bd6d..4d2927a066 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -170,11 +170,10 @@ export const convertVariablesForURL = variables => * begin with a constant prefix so that it doesn't collide with * other URL params. * - * @param {String} New URL + * @param {String} search URL * @returns {Object} The custom variables defined by the user in the URL */ - -export const getPromCustomVariablesFromUrl = (search = window.location.search) => { +export const templatingVariablesFromUrl = (search = window.location.search) => { const params = queryToObject(search); // pick the params with variable prefix const paramsWithVars = pickBy(params, (val, key) => key.startsWith(VARIABLE_PREFIX)); @@ -353,39 +352,4 @@ export const barChartsDataParser = (data = []) => {}, ); -/** - * Custom variables are defined in the dashboard yml file - * and their values can be passed through the URL. - * - * On component load, this method merges variables data - * from the yml file with URL data to store in the Vuex store. - * Not all params coming from the URL need to be stored. Only - * the ones that have a corresponding variable defined in the - * yml file. - * - * This ensures that there is always a single source of truth - * for variables - * - * This method can be improved further. See the below issue - * https://gitlab.com/gitlab-org/gitlab/-/issues/217713 - * - * @param {Object} varsFromYML template variables from yml file - * @returns {Object} - */ -export const mergeURLVariables = (varsFromYML = {}) => { - const varsFromURL = getPromCustomVariablesFromUrl(); - const variables = {}; - Object.keys(varsFromYML).forEach(key => { - if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) { - variables[key] = { - ...varsFromYML[key], - value: varsFromURL[key], - }; - } else { - variables[key] = varsFromYML[key]; - } - }); - return variables; -}; - export default {}; diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb index d03daa406c..e02955433b 100644 --- a/app/controllers/repositories/git_http_client_controller.rb +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -18,8 +18,7 @@ module Repositories skip_around_action :set_session_storage skip_before_action :verify_authenticity_token - before_action :parse_repo_path - before_action :authenticate_user + prepend_before_action :authenticate_user, :parse_repo_path private diff --git a/app/services/authorized_project_update/periodic_recalculate_service.rb b/app/services/authorized_project_update/periodic_recalculate_service.rb new file mode 100644 index 0000000000..91c0f50e5e --- /dev/null +++ b/app/services/authorized_project_update/periodic_recalculate_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class PeriodicRecalculateService + BATCH_SIZE = 480 + DELAY_INTERVAL = 30.seconds.to_i + + def execute + # Using this approach (instead of eg. User.each_batch) keeps the arguments + # the same for AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker + # even if the user list changes, so we can deduplicate these jobs. + (1..User.maximum(:id)).each_slice(BATCH_SIZE).with_index do |batch, index| + delay = DELAY_INTERVAL * index + AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker.perform_in(delay, *batch.minmax) + end + end + end +end diff --git a/app/services/authorized_project_update/recalculate_for_user_range_service.rb b/app/services/authorized_project_update/recalculate_for_user_range_service.rb new file mode 100644 index 0000000000..14b0f5d611 --- /dev/null +++ b/app/services/authorized_project_update/recalculate_for_user_range_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class RecalculateForUserRangeService + def initialize(start_user_id, end_user_id) + @start_user_id = start_user_id + @end_user_id = end_user_id + end + + def execute + User.where(id: start_user_id..end_user_id).select(:id).find_each do |user| # rubocop: disable CodeReuse/ActiveRecord + Users::RefreshAuthorizedProjectsService.new(user).execute + end + end + + private + + attr_reader :start_user_id, :end_user_id + end +end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 0e7a4821bd..621266f00e 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -85,8 +85,6 @@ module Users # remove - The IDs of the authorization rows to remove. # add - Rows to insert in the form `[user id, project id, access level]` def update_authorizations(remove = [], add = []) - return if remove.empty? && add.empty? - User.transaction do user.remove_project_authorizations(remove) unless remove.empty? ProjectAuthorization.insert_authorizations(add) unless add.empty? diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 0699be0f4c..3baa216681 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -11,6 +11,14 @@ :weight: 1 :idempotent: true :tags: [] +- :name: authorized_project_update:authorized_project_update_user_refresh_over_user_range + :feature_category: :authentication_and_authorization + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency :feature_category: :authentication_and_authorization :has_external_dependencies: @@ -99,6 +107,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: cronjob:authorized_project_update_periodic_recalculate + :feature_category: :source_code_management + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:ci_archive_traces_cron :feature_category: :continuous_integration :has_external_dependencies: diff --git a/app/workers/authorized_project_update/periodic_recalculate_worker.rb b/app/workers/authorized_project_update/periodic_recalculate_worker.rb new file mode 100644 index 0000000000..0d1ad67d7b --- /dev/null +++ b/app/workers/authorized_project_update/periodic_recalculate_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class PeriodicRecalculateWorker + include ApplicationWorker + # This worker does not perform work scoped to a context + include CronjobQueue # rubocop:disable Scalability/CronWorkerContext + + feature_category :source_code_management + urgency :low + + idempotent! + + def perform + if ::Feature.enabled?(:periodic_project_authorization_recalculation, default_enabled: true) + AuthorizedProjectUpdate::PeriodicRecalculateService.new.execute + end + end + end +end diff --git a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb new file mode 100644 index 0000000000..336b1c5443 --- /dev/null +++ b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class UserRefreshOverUserRangeWorker + include ApplicationWorker + + feature_category :authentication_and_authorization + urgency :low + queue_namespace :authorized_project_update + deduplicate :until_executing, including_scheduled: true + + idempotent! + + def perform(start_user_id, end_user_id) + if ::Feature.enabled?(:periodic_project_authorization_recalculation, default_enabled: true) + AuthorizedProjectUpdate::RecalculateForUserRangeService.new(start_user_id, end_user_id).execute + end + end + end +end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 9d9f24183d..0afd43634c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -499,6 +499,9 @@ Settings.cron_jobs['x509_issuer_crl_check_worker']['job_class'] = 'X509IssuerCrl Settings.cron_jobs['users_create_statistics_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *' Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker' +Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker']['cron'] ||= '45 1 * * 6' +Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker']['job_class'] = 'AuthorizedProjectUpdate::PeriodicRecalculateWorker' Gitlab.ee do Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({}) diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 5ee11c553a..44802214d7 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -491,7 +491,10 @@ introduced by [#25381](https://gitlab.com/gitlab-org/gitlab/issues/25381). ### Batch Suggestions -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1 as an [alpha feature](https://about.gitlab.com/handbook/product/#alpha). +> - It's deployed behind a feature flag, disabled by default. +> - It's disabled on GitLab.com. +> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-batch-suggestions). You can apply multiple suggestions at once to reduce the number of commits added to your branch to address your reviewers' requests. @@ -512,6 +515,27 @@ to your branch to address your reviewers' requests. ![A code change suggestion displayed, with the button to apply the batch of suggestions highlighted.](img/apply_batch_of_suggestions_v13_1.jpg "Apply a batch of suggestions") +#### Enable or disable Batch Suggestions + +Batch Suggestions is +deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) +can enable it for your instance. + +To enable it: + +```ruby +# Instance-wide +Feature.enable(:batched_suggestions) +``` + +To disable it: + +```ruby +# Instance-wide +Feature.disable(:batched_suggestions) +``` + ## Start a thread by replying to a standard comment > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9 diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index ccaea61ae4..a5fa3cf373 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -80,8 +80,9 @@ The default syntax theme is White, and you can choose among 5 different themes: [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in 13.0, the theme you choose also applies to the [Web IDE](../project/web_ide/index.md)'s code editor and [Snippets](../snippets.md). -The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808), -which applies to the entire Web IDE screen. +The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and +the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), +which apply to the entire Web IDE screen. ## Behavior diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index 981c2a7c34..ac397592a3 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -181,17 +181,23 @@ so that everyone involved can participate in the discussion. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13049) in GitLab 13.1. -Discussion threads can be resolved on Designs. You can mark a thread as resolved -or unresolved by clicking the **Resolve thread** icon at the first comment of the -discussion. +Discussion threads can be resolved on Designs. -![Resolve thread icon](img/resolve_design-discussion_icon_v13_1.png) +There are two ways to resolve/unresolve a Design thread: -Pinned comments can also be resolved or unresolved in their threads. -When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve -the thread once published. +1. You can mark a thread as resolved or unresolved by clicking the checkmark icon for **Resolve thread** in the top-right corner of the first comment of the discussion: -![Resolve checkbox](img/resolve_design-discussion_checkbox_v13_1.png) + ![Resolve thread icon](img/resolve_design-discussion_icon_v13_1.png) + +1. Design threads can also be resolved or unresolved in their threads by using a checkbox. + When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve + the thread once published: + + ![Resolve checkbox](img/resolve_design-discussion_checkbox_v13_1.png) + +Note that your resolved comment pins will disappear from the Design to free up space for new discussions. +However, if you need to revisit or find a resolved discussion, all of your resolved threads will be +available in the **Resolved Comment** area at the bottom of the right sidebar. ## Referring to designs in Markdown diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md index e2e498b605..15c3bd5522 100644 --- a/doc/user/project/static_site_editor/index.md +++ b/doc/user/project/static_site_editor/index.md @@ -7,6 +7,8 @@ description: "The static site editor enables users to edit content on static web > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10. > - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0. +> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1. +> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1. DANGER: **Danger:** In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282) @@ -78,6 +80,11 @@ Anyone satisfying the [requirements](#requirements) will be able to edit the content of the pages without prior knowledge of Git nor of your site's codebase. +NOTE: **Note:** +From GitLab 13.1 onwards, the YAML front matter of Markdown files is hidden on the +WYSIWYG editor to avoid unintended changes. To edit it, use the Markdown editing mode, the regular +GitLab file editor, or the Web IDE. + ### Use the Static Site Editor to edit your content For instance, suppose you are a recently hired technical writer at a large diff --git a/doc/user/project/web_ide/img/dark_theme_v13.0.png b/doc/user/project/web_ide/img/dark_theme_v13.0.png deleted file mode 100644 index 6034bc52c0..0000000000 Binary files a/doc/user/project/web_ide/img/dark_theme_v13.0.png and /dev/null differ diff --git a/doc/user/project/web_ide/img/dark_theme_v13_0.png b/doc/user/project/web_ide/img/dark_theme_v13_0.png new file mode 100644 index 0000000000..f199936390 Binary files /dev/null and b/doc/user/project/web_ide/img/dark_theme_v13_0.png differ diff --git a/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png b/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png new file mode 100644 index 0000000000..ccb9cf6f12 Binary files /dev/null and b/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png differ diff --git a/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png b/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png deleted file mode 100644 index f3c4aa142a..0000000000 Binary files a/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png and /dev/null differ diff --git a/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png b/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png new file mode 100644 index 0000000000..adf6d3c6b0 Binary files /dev/null and b/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png differ diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 0ddc9762bc..ce20f2308e 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -45,17 +45,19 @@ Single file editing is based on the [Ace Editor](https://ace.c9.io). ### Themes -> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab 13.0. +> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0. +> - Full Solarized Dark Theme [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219228) in GitLab 13.1. All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor. You can pick a theme from your [profile preferences](../../profile/preferences.md). -The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808), -which applies to the entire Web IDE screen. +The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and +the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), +which apply to the entire Web IDE screen. -| Solarized Light Theme | Dark Theme | -|---------------------------------------------------------------|-----------------------------------------| -| ![Solarized Light Theme](img/solarized_light_theme_v13.0.png) | ![Dark Theme](img/dark_theme_v13.0.png) | +| Solarized Light Theme | Solarized Dark Theme | Dark Theme | +|---------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------| +| ![Solarized Light Theme](img/solarized_light_theme_v13_0.png) | ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) | ## Configure the Web IDE diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 11340e91aa..3259b61536 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -25,8 +25,7 @@ module API get "deploy_keys" do authenticated_as_admin! - deploy_keys = DeployKey.all.preload_users - present paginate(deploy_keys), with: Entities::SSHKey + present paginate(DeployKey.all), with: Entities::DeployKey end params do @@ -43,7 +42,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get ":id/deploy_keys" do - keys = user_project.deploy_keys_projects.preload(deploy_key: [:user]) + keys = user_project.deploy_keys_projects.preload(:deploy_key) present paginate(keys), with: Entities::DeployKeysProject end @@ -105,7 +104,7 @@ module API # rubocop: enable CodeReuse/ActiveRecord desc 'Update an existing deploy key for a project' do - success Entities::SSHKey + success Entities::DeployKey end params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' @@ -140,7 +139,7 @@ module API desc 'Enable a deploy key for a project' do detail 'This feature was added in GitLab 8.11' - success Entities::SSHKey + success Entities::DeployKey end params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' @@ -150,7 +149,7 @@ module API current_user, declared_params).execute if key - present key, with: Entities::SSHKey + present key, with: Entities::DeployKey else not_found!('Deploy Key') end diff --git a/lib/api/entities/deploy_key.rb b/lib/api/entities/deploy_key.rb new file mode 100644 index 0000000000..ed922c24ed --- /dev/null +++ b/lib/api/entities/deploy_key.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class DeployKey < Entities::SSHKey + expose :key + end + end +end diff --git a/lib/api/entities/deploy_key_with_user.rb b/lib/api/entities/deploy_key_with_user.rb index 31024dc391..a8f6440b67 100644 --- a/lib/api/entities/deploy_key_with_user.rb +++ b/lib/api/entities/deploy_key_with_user.rb @@ -2,7 +2,8 @@ module API module Entities - class DeployKeyWithUser < Entities::SSHKeyWithUser + class DeployKeyWithUser < Entities::DeployKey + expose :user, using: Entities::UserPublic expose :deploy_keys_projects end end diff --git a/lib/api/entities/deploy_keys_project.rb b/lib/api/entities/deploy_keys_project.rb index 6472545916..12a86fbdf8 100644 --- a/lib/api/entities/deploy_keys_project.rb +++ b/lib/api/entities/deploy_keys_project.rb @@ -3,7 +3,7 @@ module API module Entities class DeployKeysProject < Grape::Entity - expose :deploy_key, merge: true, using: Entities::SSHKey + expose :deploy_key, merge: true, using: Entities::DeployKey expose :can_push end end diff --git a/package.json b/package.json index 99614c0acc..790850c68c 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "@babel/plugin-syntax-import-meta": "^7.10.1", "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", - "@gitlab/svgs": "1.139.0", - "@gitlab/ui": "16.12.1", + "@gitlab/svgs": "1.140.0", + "@gitlab/ui": "17.0.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", "@sentry/browser": "^5.10.2", diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index aafb933df3..c938df8cf4 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -60,10 +60,21 @@ RSpec.describe Repositories::GitHttpController do get :info_refs, params: params end + + include_context 'parsed logs' do + it 'adds user info to the logs' do + get :info_refs, params: params + + expect(log_data).to include('username' => user.username, + 'user_id' => user.id, + 'meta.user' => user.username) + end + end end context 'with exceptions' do before do + allow(controller).to receive(:authenticate_user).and_return(true) allow(controller).to receive(:verify_workhorse_api!).and_return(true) end diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index 3a70bda51d..2dea40585f 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -9,6 +9,7 @@ import { convertToGrafanaTimeRange, addDashboardMetaDataToLink, } from '~/monitoring/stores/utils'; +import * as urlUtils from '~/lib/utils/url_utility'; import { annotationsData } from '../mock_data'; import { NOT_IN_DB_PREFIX } from '~/monitoring/constants'; @@ -398,6 +399,118 @@ describe('mapToDashboardViewModel', () => { }); }); }); + + describe('templating variables mapping', () => { + beforeEach(() => { + jest.spyOn(urlUtils, 'queryToObject'); + }); + + afterEach(() => { + urlUtils.queryToObject.mockRestore(); + }); + + it('sets variables as-is from yml file if URL has no variables', () => { + const response = { + dashboard: 'Dashboard Name', + links: [], + templating: { + variables: { + pod: 'kubernetes', + pod_2: 'kubernetes-2', + }, + }, + }; + + urlUtils.queryToObject.mockReturnValueOnce(); + + expect(mapToDashboardViewModel(response)).toMatchObject({ + dashboard: 'Dashboard Name', + links: [], + variables: { + pod: { + label: 'pod', + type: 'text', + value: 'kubernetes', + }, + pod_2: { + label: 'pod_2', + type: 'text', + value: 'kubernetes-2', + }, + }, + }); + }); + + it('sets variables as-is from yml file if URL has no matching variables', () => { + const response = { + dashboard: 'Dashboard Name', + links: [], + templating: { + variables: { + pod: 'kubernetes', + pod_2: 'kubernetes-2', + }, + }, + }; + + urlUtils.queryToObject.mockReturnValueOnce({ + 'var-environment': 'POD', + }); + + expect(mapToDashboardViewModel(response)).toMatchObject({ + dashboard: 'Dashboard Name', + links: [], + variables: { + pod: { + label: 'pod', + type: 'text', + value: 'kubernetes', + }, + pod_2: { + label: 'pod_2', + type: 'text', + value: 'kubernetes-2', + }, + }, + }); + }); + + it('merges variables from URL with the ones from yml file', () => { + const response = { + dashboard: 'Dashboard Name', + links: [], + templating: { + variables: { + pod: 'kubernetes', + pod_2: 'kubernetes-2', + }, + }, + }; + + urlUtils.queryToObject.mockReturnValueOnce({ + 'var-environment': 'POD', + 'var-pod': 'POD1', + 'var-pod_2': 'POD2', + }); + + expect(mapToDashboardViewModel(response)).toMatchObject({ + dashboard: 'Dashboard Name', + links: [], + variables: { + pod: { + label: 'pod', + type: 'text', + value: 'POD1', + }, + pod_2: { + label: 'pod_2', + type: 'text', + value: 'POD2', + }, + }, + }); + }); + }); }); describe('normalizeQueryResult', () => { diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js index c44bb95716..5164ed1b54 100644 --- a/spec/frontend/monitoring/store/variable_mapping_spec.js +++ b/spec/frontend/monitoring/store/variable_mapping_spec.js @@ -1,4 +1,5 @@ -import { parseTemplatingVariables } from '~/monitoring/stores/variable_mapping'; +import { parseTemplatingVariables, mergeURLVariables } from '~/monitoring/stores/variable_mapping'; +import * as urlUtils from '~/lib/utils/url_utility'; import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data'; describe('parseTemplatingVariables', () => { @@ -21,3 +22,73 @@ describe('parseTemplatingVariables', () => { expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected); }); }); + +describe('mergeURLVariables', () => { + beforeEach(() => { + jest.spyOn(urlUtils, 'queryToObject'); + }); + + afterEach(() => { + urlUtils.queryToObject.mockRestore(); + }); + + it('returns empty object if variables are not defined in yml or URL', () => { + urlUtils.queryToObject.mockReturnValueOnce({}); + + expect(mergeURLVariables({})).toEqual({}); + }); + + it('returns empty object if variables are defined in URL but not in yml', () => { + urlUtils.queryToObject.mockReturnValueOnce({ + 'var-env': 'one', + 'var-instance': 'localhost', + }); + + expect(mergeURLVariables({})).toEqual({}); + }); + + it('returns yml variables if variables defined in yml but not in the URL', () => { + urlUtils.queryToObject.mockReturnValueOnce({}); + + const params = { + env: 'one', + instance: 'localhost', + }; + + expect(mergeURLVariables(params)).toEqual(params); + }); + + it('returns yml variables if variables defined in URL do not match with yml variables', () => { + const urlParams = { + 'var-env': 'one', + 'var-instance': 'localhost', + }; + const ymlParams = { + pod: { value: 'one' }, + service: { value: 'database' }, + }; + urlUtils.queryToObject.mockReturnValueOnce(urlParams); + + expect(mergeURLVariables(ymlParams)).toEqual(ymlParams); + }); + + it('returns merged yml and URL variables if there is some match', () => { + const urlParams = { + 'var-env': 'one', + 'var-instance': 'localhost:8080', + }; + const ymlParams = { + instance: { value: 'localhost' }, + service: { value: 'database' }, + }; + + const merged = { + instance: { value: 'localhost:8080' }, + service: { value: 'database' }, + }; + + urlUtils.queryToObject.mockReturnValueOnce(urlParams); + + expect(mergeURLVariables(ymlParams)).toEqual(merged); + }); +}); diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index aa5a4459a7..039cf275ee 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -169,8 +169,8 @@ describe('monitoring/utils', () => { }); }); - describe('getPromCustomVariablesFromUrl', () => { - const { getPromCustomVariablesFromUrl } = monitoringUtils; + describe('templatingVariablesFromUrl', () => { + const { templatingVariablesFromUrl } = monitoringUtils; beforeEach(() => { jest.spyOn(urlUtils, 'queryToObject'); @@ -195,7 +195,7 @@ describe('monitoring/utils', () => { 'var-pod': 'POD', }); - expect(getPromCustomVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' })); + expect(templatingVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' })); }); it('returns an empty object when no custom variables are present', () => { @@ -203,7 +203,7 @@ describe('monitoring/utils', () => { dashboard: '.gitlab/dashboards/custom_dashboard.yml', }); - expect(getPromCustomVariablesFromUrl()).toStrictEqual({}); + expect(templatingVariablesFromUrl()).toStrictEqual({}); }); }); @@ -427,76 +427,6 @@ describe('monitoring/utils', () => { }); }); - describe('mergeURLVariables', () => { - beforeEach(() => { - jest.spyOn(urlUtils, 'queryToObject'); - }); - - afterEach(() => { - urlUtils.queryToObject.mockRestore(); - }); - - it('returns empty object if variables are not defined in yml or URL', () => { - urlUtils.queryToObject.mockReturnValueOnce({}); - - expect(monitoringUtils.mergeURLVariables({})).toEqual({}); - }); - - it('returns empty object if variables are defined in URL but not in yml', () => { - urlUtils.queryToObject.mockReturnValueOnce({ - 'var-env': 'one', - 'var-instance': 'localhost', - }); - - expect(monitoringUtils.mergeURLVariables({})).toEqual({}); - }); - - it('returns yml variables if variables defined in yml but not in the URL', () => { - urlUtils.queryToObject.mockReturnValueOnce({}); - - const params = { - env: 'one', - instance: 'localhost', - }; - - expect(monitoringUtils.mergeURLVariables(params)).toEqual(params); - }); - - it('returns yml variables if variables defined in URL do not match with yml variables', () => { - const urlParams = { - 'var-env': 'one', - 'var-instance': 'localhost', - }; - const ymlParams = { - pod: { value: 'one' }, - service: { value: 'database' }, - }; - urlUtils.queryToObject.mockReturnValueOnce(urlParams); - - expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(ymlParams); - }); - - it('returns merged yml and URL variables if there is some match', () => { - const urlParams = { - 'var-env': 'one', - 'var-instance': 'localhost:8080', - }; - const ymlParams = { - instance: { value: 'localhost' }, - service: { value: 'database' }, - }; - - const merged = { - instance: { value: 'localhost:8080' }, - service: { value: 'database' }, - }; - - urlUtils.queryToObject.mockReturnValueOnce(urlParams); - - expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(merged); - }); - }); - describe('convertVariablesForURL', () => { it.each` input | expected diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb index f283ac100a..9e5eab4fc6 100644 --- a/spec/initializers/lograge_spec.rb +++ b/spec/initializers/lograge_spec.rb @@ -99,6 +99,8 @@ describe 'lograge', type: :request do end context 'with a log subscriber' do + include_context 'parsed logs' + let(:subscriber) { Lograge::LogSubscribers::ActionController.new } let(:event) do @@ -119,16 +121,6 @@ describe 'lograge', type: :request do ) end - let(:log_output) { StringIO.new } - let(:logger) do - Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } - end - let(:log_data) { Gitlab::Json.parse(log_output.string) } - - before do - Lograge.logger = logger - end - describe 'with an exception' do let(:exception) { RuntimeError.new('bad request') } let(:backtrace) { caller } diff --git a/spec/lib/api/entities/deploy_key_spec.rb b/spec/lib/api/entities/deploy_key_spec.rb new file mode 100644 index 0000000000..704dabae63 --- /dev/null +++ b/spec/lib/api/entities/deploy_key_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::DeployKey do + describe '#as_json' do + subject { entity.as_json } + + let(:deploy_key) { create(:deploy_key, public: true) } + let(:entity) { described_class.new(deploy_key) } + + it 'includes basic fields', :aggregate_failures do + is_expected.to include( + id: deploy_key.id, + title: deploy_key.title, + created_at: deploy_key.created_at, + expires_at: deploy_key.expires_at, + key: deploy_key.key + ) + end + end +end diff --git a/spec/lib/api/entities/deploy_keys_project_spec.rb b/spec/lib/api/entities/deploy_keys_project_spec.rb new file mode 100644 index 0000000000..a357467d7c --- /dev/null +++ b/spec/lib/api/entities/deploy_keys_project_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::DeployKeysProject do + describe '#as_json' do + subject { entity.as_json } + + let(:deploy_keys_project) { create(:deploy_keys_project, :write_access) } + let(:entity) { described_class.new(deploy_keys_project) } + + it 'includes basic fields', :aggregate_failures do + deploy_key = deploy_keys_project.deploy_key + + is_expected.to include( + id: deploy_key.id, + title: deploy_key.title, + created_at: deploy_key.created_at, + expires_at: deploy_key.expires_at, + key: deploy_key.key, + can_push: deploy_keys_project.can_push + ) + end + end +end diff --git a/spec/lib/api/entities/ssh_key_spec.rb b/spec/lib/api/entities/ssh_key_spec.rb new file mode 100644 index 0000000000..25a0fecfb7 --- /dev/null +++ b/spec/lib/api/entities/ssh_key_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::SSHKey do + describe '#as_json' do + subject { entity.as_json } + + let(:key) { create(:key, user: create(:user)) } + let(:entity) { described_class.new(key) } + + it 'includes basic fields', :aggregate_failures do + is_expected.to include( + id: key.id, + title: key.title, + created_at: key.created_at, + expires_at: key.expires_at, + key: key.publishable_key + ) + end + end +end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 1baa18b53c..e8cc6bc71a 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -8,7 +8,7 @@ describe API::DeployKeys do let(:admin) { create(:admin) } let(:project) { create(:project, creator_id: user.id) } let(:project2) { create(:project, creator_id: user.id) } - let(:deploy_key) { create(:deploy_key, public: true, user: user) } + let(:deploy_key) { create(:deploy_key, public: true) } let!(:deploy_keys_project) do create(:deploy_keys_project, project: project, deploy_key: deploy_key) @@ -40,32 +40,6 @@ describe API::DeployKeys do expect(json_response).to be_an Array expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) end - - it 'returns all deploy keys with comments replaced with'\ - 'a simple identifier of username + hostname' do - get api('/deploy_keys', admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - - keys = json_response.map { |key_detail| key_detail['key'] } - expect(keys).to all(include("#{user.name} (#{Gitlab.config.gitlab.host}")) - end - - context 'N+1 queries' do - before do - get api('/deploy_keys', admin) - end - - it 'avoids N+1 queries', :request_store do - control_count = ActiveRecord::QueryRecorder.new { get api('/deploy_keys', admin) }.count - - create_list(:deploy_key, 2, public: true, user: create(:user)) - - expect { get api('/deploy_keys', admin) }.not_to exceed_query_limit(control_count) - end - end end end @@ -82,25 +56,6 @@ describe API::DeployKeys do expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) end - - context 'N+1 queries' do - before do - get api("/projects/#{project.id}/deploy_keys", admin) - end - - it 'avoids N+1 queries', :request_store do - control_count = ActiveRecord::QueryRecorder.new do - get api("/projects/#{project.id}/deploy_keys", admin) - end.count - - deploy_key = create(:deploy_key, user: create(:user)) - create(:deploy_keys_project, project: project, deploy_key: deploy_key) - - expect do - get api("/projects/#{project.id}/deploy_keys", admin) - end.not_to exceed_query_limit(control_count) - end - end end describe 'GET /projects/:id/deploy_keys/:key_id' do @@ -111,13 +66,6 @@ describe API::DeployKeys do expect(json_response['title']).to eq(deploy_key.title) end - it 'exposes key comment as a simple identifier of username + hostname' do - get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['key']).to include("#{deploy_key.user_name} (#{Gitlab.config.gitlab.host})") - end - it 'returns 404 Not Found with invalid ID' do get api("/projects/#{project.id}/deploy_keys/404", admin) diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index d860179f0a..617587e2fa 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -3,19 +3,14 @@ require 'spec_helper' describe JwtController do + include_context 'parsed logs' + let(:service) { double(execute: {}) } let(:service_class) { double(new: service) } let(:service_name) { 'test' } let(:parameters) { { service: service_name } } - let(:log_output) { StringIO.new } - let(:logger) do - Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } - end - let(:log_data) { Gitlab::Json.parse(log_output.string) } before do - Lograge.logger = logger - stub_const('JwtController::SERVICES', service_name => service_class) end diff --git a/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb new file mode 100644 index 0000000000..020056da36 --- /dev/null +++ b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::PeriodicRecalculateService do + subject(:service) { described_class.new } + + describe '#execute' do + let(:batch_size) { 2 } + + let_it_be(:users) { create_list(:user, 4) } + + before do + stub_const('AuthorizedProjectUpdate::PeriodicRecalculateService::BATCH_SIZE', batch_size) + + User.delete([users[1], users[2]]) + end + + it 'calls AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker' do + (1..User.maximum(:id)).each_slice(batch_size).with_index do |batch, index| + delay = AuthorizedProjectUpdate::PeriodicRecalculateService::DELAY_INTERVAL * index + + expect(AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker).to( + receive(:perform_in).with(delay, *batch.minmax)) + end + + service.execute + end + end +end diff --git a/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb b/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb new file mode 100644 index 0000000000..28cbda6f4f --- /dev/null +++ b/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::RecalculateForUserRangeService do + describe '#execute' do + let_it_be(:users) { create_list(:user, 2) } + + it 'calls Users::RefreshAuthorizedProjectsService' do + users.each do |user| + expect(Users::RefreshAuthorizedProjectsService).to( + receive(:new).with(user).and_call_original) + end + + range = users.map(&:id).minmax + described_class.new(*range).execute + end + end +end diff --git a/spec/support/shared_contexts/controllers/logging_shared_context.rb b/spec/support/shared_contexts/controllers/logging_shared_context.rb new file mode 100644 index 0000000000..986a96f3a8 --- /dev/null +++ b/spec/support/shared_contexts/controllers/logging_shared_context.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# This context replaces the logger and exposes the `log_data` variable for +# inspection +RSpec.shared_context 'parsed logs' do + let(:logger) do + Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } + end + + let(:log_output) { StringIO.new } + let(:log_data) { Gitlab::Json.parse(log_output.string) } + + around do |example| + initial_logger = Lograge.logger + Lograge.logger = logger + + example.run + + Lograge.logger = initial_logger + end +end diff --git a/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb b/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb new file mode 100644 index 0000000000..fcd073953b --- /dev/null +++ b/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::PeriodicRecalculateWorker do + describe '#perform' do + it 'calls AuthorizedProjectUpdate::PeriodicRecalculateService' do + expect_next_instance_of(AuthorizedProjectUpdate::PeriodicRecalculateService) do |service| + expect(service).to receive(:execute) + end + + subject.perform + end + + context 'feature flag :periodic_project_authorization_recalculation is disabled' do + before do + stub_feature_flags(periodic_project_authorization_recalculation: false) + end + + it 'does not call AuthorizedProjectUpdate::PeriodicRecalculateService' do + expect(AuthorizedProjectUpdate::PeriodicRecalculateService).not_to receive(:new) + + subject.perform + end + end + end +end diff --git a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb new file mode 100644 index 0000000000..5d1c405dfd --- /dev/null +++ b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker do + let(:start_user_id) { 42 } + let(:end_user_id) { 4242 } + + describe '#perform' do + it 'calls AuthorizedProjectUpdate::RecalculateForUserRangeService' do + expect_next_instance_of(AuthorizedProjectUpdate::RecalculateForUserRangeService) do |service| + expect(service).to receive(:execute) + end + + subject.perform(start_user_id, end_user_id) + end + + context 'feature flag :periodic_project_authorization_recalculation is disabled' do + before do + stub_feature_flags(periodic_project_authorization_recalculation: false) + end + + it 'does not call AuthorizedProjectUpdate::RecalculateForUserRangeService' do + expect(AuthorizedProjectUpdate::RecalculateForUserRangeService).not_to receive(:new) + + subject.perform(start_user_id, end_user_id) + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 3b3766f540..79aed65f8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -835,15 +835,15 @@ eslint-plugin-vue "^6.2.1" vue-eslint-parser "^7.0.0" -"@gitlab/svgs@1.139.0": - version "1.139.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.139.0.tgz#8a4874e76000e2dd7d3ed3a8967d62bed47d7ea7" - integrity sha512-o1KAmQLYL727HodlPHkmj+d+Kdw8OIgHzlKmmPYMzeE+As2l1oz6CTilca56KqXGklOgrouC9P2puMwyX8e/6g== +"@gitlab/svgs@1.140.0": + version "1.140.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.140.0.tgz#593f1f65b0df57c3399fcfb9f472f59aa64da074" + integrity sha512-6gANJGi2QkpvOgFTMcY3SIwEqhO69i6R3jU4BSskkVziwDdAWxGonln22a4Iu//Iv0NrsFDpAA0jIVfnJzw0iA== -"@gitlab/ui@16.12.1": - version "16.12.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-16.12.1.tgz#4d6865308596b09e36961210df7a8a489aaadb6d" - integrity sha512-jF6/I71Q0mjHetIRDO8O4VO2KIGWKL/yH2Mdb/CqQKaEasgnc/YpuyHGCsBXqDPxCjRbXPeKp0EywICQx4agZA== +"@gitlab/ui@17.0.1": + version "17.0.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.0.1.tgz#daf036dfdc095f94123c80c3fb1ab5fe4dcbf95b" + integrity sha512-JSUGruV6oploADF0Sc0BBY43Des3utU9iWCnR8BAmttKFXFFNUKwTf908yZPGJtfnVyjJkVioOCOYkvUZ0jngg== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0"