diff options
49 files changed, 580 insertions, 102 deletions
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index bda581f5a4d..86d80d88737 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -306,7 +306,7 @@ export default () => { const issueBoardsModal = document.getElementById('js-add-issues-btn'); - if (issueBoardsModal) { + if (issueBoardsModal && gon.features.addIssuesButton) { // eslint-disable-next-line no-new new Vue({ el: issueBoardsModal, diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index bbfa865905a..32366c5068f 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -33,7 +33,14 @@ export default { mixins: [glFeatureFlagsMixin()], computed: { ...mapGetters(['currentKey', 'propsSource', 'isDisabled']), - ...mapState(['defaultState', 'override', 'isSaving', 'isTesting', 'isResetting']), + ...mapState([ + 'defaultState', + 'customState', + 'override', + 'isSaving', + 'isTesting', + 'isResetting', + ]), isEditable() { return this.propsSource.editable; }, @@ -42,8 +49,8 @@ export default { }, isInstanceOrGroupLevel() { return ( - this.propsSource.integrationLevel === integrationLevels.INSTANCE || - this.propsSource.integrationLevel === integrationLevels.GROUP + this.customState.integrationLevel === integrationLevels.INSTANCE || + this.customState.integrationLevel === integrationLevels.GROUP ); }, showJiraIssuesFields() { diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index f099372d6db..90ffa258884 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -4,7 +4,7 @@ import ActionComponent from './action_component.vue'; import JobNameComponent from './job_name_component.vue'; import { sprintf } from '~/locale'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; -import { accessors } from './accessors'; +import { accessValue } from './accessors'; import { REST } from './constants'; /** @@ -79,10 +79,10 @@ export default { return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; }, detailsPath() { - return this.status[accessors[this.dataMethod].detailsPath]; + return accessValue(this.dataMethod, 'detailsPath', this.status); }, hasDetails() { - return this.status[accessors[this.dataMethod].hasDetails]; + return accessValue(this.dataMethod, 'hasDetails', this.status); }, status() { return this.job && this.job.status ? this.job.status : {}; diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 5e2d79758e1..830ad219574 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -5,7 +5,7 @@ import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; import { GRAPHQL } from './constants'; -import { accessors } from './accessors'; +import { accessValue } from './accessors'; export default { components: { @@ -39,7 +39,6 @@ export default { default: () => ({}), }, }, - accessors, titleClasses: [ 'gl-font-weight-bold', 'gl-pipeline-job-width', @@ -56,8 +55,8 @@ export default { }, }, methods: { - getAccessor(property) { - return accessors[GRAPHQL][property]; + getGroupId(group) { + return accessValue(GRAPHQL, 'groupId', group); }, groupId(group) { return `ci-badge-${escape(group.name)}`; @@ -87,7 +86,7 @@ export default { <div v-for="group in groups" :id="groupId(group)" - :key="group[getAccessor('groupId')]" + :key="getGroupId(group)" data-testid="stage-column-group" class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width" > diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 741609c908a..af7c0d0ec3f 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -229,6 +229,7 @@ export default { v-if="pipeline.cancelable" :loading="isCanceling" :disabled="isCanceling" + class="gl-ml-3" variant="danger" data-testid="cancelPipeline" @click="cancelPipeline()" diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue index df7d9b56aed..a07c57c42cb 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue @@ -30,6 +30,10 @@ export default { required: false, default: '', }, + customEmailEnabled: { + type: Boolean, + required: false, + }, selectedTemplate: { type: String, required: false, @@ -140,6 +144,7 @@ export default { :is-enabled="isEnabled" :incoming-email="incomingEmail" :custom-email="updatedCustomEmail" + :custom-email-enabled="customEmailEnabled" :initial-selected-template="selectedTemplate" :initial-outgoing-name="outgoingName" :initial-project-key="projectKey" diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index 5d120fd0b3f..2896cb491b5 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -31,6 +31,10 @@ export default { required: false, default: '', }, + customEmailEnabled: { + type: Boolean, + required: false, + }, initialSelectedTemplate: { type: String, required: false, @@ -69,7 +73,7 @@ export default { return [''].concat(this.templates); }, hasProjectKeySupport() { - return Boolean(this.glFeatures.serviceDeskCustomAddress); + return Boolean(this.customEmailEnabled); }, email() { return this.customEmail || this.incomingEmail; diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js index c73163788ef..8f9828dd73d 100644 --- a/app/assets/javascripts/projects/settings_service_desk/index.js +++ b/app/assets/javascripts/projects/settings_service_desk/index.js @@ -18,6 +18,7 @@ export default () => { endpoint: dataset.endpoint, incomingEmail: dataset.incomingEmail, customEmail: dataset.customEmail, + customEmailEnabled: parseBoolean(dataset.customEmailEnabled), selectedTemplate: dataset.selectedTemplate, outgoingName: dataset.outgoingName, projectKey: dataset.projectKey, @@ -31,6 +32,7 @@ export default () => { endpoint: this.endpoint, incomingEmail: this.incomingEmail, customEmail: this.customEmail, + customEmailEnabled: this.customEmailEnabled, selectedTemplate: this.selectedTemplate, outgoingName: this.outgoingName, projectKey: this.projectKey, diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss index 493a9093361..7b424882ffa 100644 --- a/app/assets/stylesheets/page_bundles/pipeline.scss +++ b/app/assets/stylesheets/page_bundles/pipeline.scss @@ -129,17 +129,6 @@ overflow: auto; } -// Move to Gitlab UI -.gl-font-weight-100 { - font-weight: 100; -} - -.gl-active-text-decoration-none:active, -.gl-focus-text-decoration-none:focus, -.gl-hover-text-decoration-none:hover { - text-decoration: none; -} - // These are single-value classes to use with utility-class style CSS // but to still access this variable. Do not add other styles. .gl-pipeline-min-h { @@ -150,10 +139,6 @@ width: 186px; } -.gl-pipeline-title-width { - width: 176px; -} - .gl-build-content { @include build-content(); } diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 0d92127910d..51c9bf3699a 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -7,6 +7,9 @@ class Projects::BoardsController < Projects::ApplicationController before_action :check_issues_available! before_action :authorize_read_board!, only: [:index, :show] before_action :assign_endpoint_vars + before_action do + push_frontend_feature_flag(:add_issues_button) + end feature_category :boards diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 161d59fdd85..f4c81ef65ab 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -35,7 +35,6 @@ class ProjectsController < Projects::ApplicationController before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export] before_action only: [:edit] do - push_frontend_feature_flag(:service_desk_custom_address, @project) push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true) end diff --git a/app/models/project.rb b/app/models/project.rb index 96863de6e4c..69032b3d762 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2507,8 +2507,7 @@ class Project < ApplicationRecord end def service_desk_custom_address - return unless ::Gitlab::ServiceDeskEmail.enabled? - return unless ::Feature.enabled?(:service_desk_custom_address, self) + return unless service_desk_custom_address_enabled? key = service_desk_setting&.project_key return unless key.present? @@ -2516,6 +2515,10 @@ class Project < ApplicationRecord ::Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}") end + def service_desk_custom_address_enabled? + ::Gitlab::ServiceDeskEmail.enabled? && ::Feature.enabled?(:service_desk_custom_address, self) + end + def root_namespace if namespace.has_parent? namespace.root_ancestor diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml index 7c08955983a..3b2b3a2ba67 100644 --- a/app/views/projects/_service_desk_settings.html.haml +++ b/app/views/projects/_service_desk_settings.html.haml @@ -12,6 +12,7 @@ enabled: "#{@project.service_desk_enabled}", incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled), custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled), + custom_email_enabled: "#{@project.service_desk_custom_address_enabled?}", selected_template: "#{@project.service_desk_setting&.issue_template_key}", outgoing_name: "#{@project.service_desk_setting&.outgoing_name}", project_key: "#{@project.service_desk_setting&.project_key}", diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml index 255ec9995db..50daa400e6c 100644 --- a/app/views/shared/access_tokens/_table.html.haml +++ b/app/views/shared/access_tokens/_table.html.haml @@ -42,7 +42,7 @@ = _('In %{time_to_now}') % { time_to_now: distance_of_time_in_words_to_now(token.expires_at) } - else %span.token-never-expires-label= _('Never') - %td= token.scopes.present? ? token.scopes.join(', ') : html_escape_once(_('<no scopes selected>')).html_safe + %td= token.scopes.present? ? token.scopes.join(', ') : _('no scopes selected') %td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: 'btn btn-danger float-right qa-revoke-button', data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } } - else .settings-message.text-center diff --git a/app/views/shared/deploy_tokens/_table.html.haml b/app/views/shared/deploy_tokens/_table.html.haml index ad73442807e..361471af0ad 100644 --- a/app/views/shared/deploy_tokens/_table.html.haml +++ b/app/views/shared/deploy_tokens/_table.html.haml @@ -23,7 +23,7 @@ In #{distance_of_time_in_words_to_now(token.expires_at)} - else %span.token-never-expires-label= _('Never') - %td= token.scopes.present? ? token.scopes.join(", ") : html_escape_once(_('<no scopes selected>')).html_safe + %td= token.scopes.present? ? token.scopes.join(', ') : _('no scopes selected') %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger float-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"} = render 'shared/deploy_tokens/revoke_modal', token: token, group_or_project: group_or_project - else diff --git a/changelogs/unreleased/247476-remove-angle-brackets-in-no-scopes.yml b/changelogs/unreleased/247476-remove-angle-brackets-in-no-scopes.yml new file mode 100644 index 00000000000..bb13a2fc782 --- /dev/null +++ b/changelogs/unreleased/247476-remove-angle-brackets-in-no-scopes.yml @@ -0,0 +1,5 @@ +--- +title: Remove brackets in no scopes selected message in access and deploy tokens lists +merge_request: 47628 +author: +type: changed diff --git a/changelogs/unreleased/284070-issue-list-dropdown-not-shown-in-boards-add-issue-modal.yml b/changelogs/unreleased/284070-issue-list-dropdown-not-shown-in-boards-add-issue-modal.yml new file mode 100644 index 00000000000..858a7dac2e3 --- /dev/null +++ b/changelogs/unreleased/284070-issue-list-dropdown-not-shown-in-boards-add-issue-modal.yml @@ -0,0 +1,5 @@ +--- +title: Remove `Add Issues` button and a related modal +merge_request: 47898 +author: +type: changed diff --git a/changelogs/unreleased/fix-default-category-for-dropdowns-gitlab-ui-integration-test.yml b/changelogs/unreleased/fix-default-category-for-dropdowns-gitlab-ui-integration-test.yml new file mode 100644 index 00000000000..a15b3d57583 --- /dev/null +++ b/changelogs/unreleased/fix-default-category-for-dropdowns-gitlab-ui-integration-test.yml @@ -0,0 +1,5 @@ +--- +title: Fix styling of various dropdowns +merge_request: 48800 +author: +type: fixed diff --git a/changelogs/unreleased/justin_ho-fix-confirmation-modal-showing-on-project-integrations.yml b/changelogs/unreleased/justin_ho-fix-confirmation-modal-showing-on-project-integrations.yml new file mode 100644 index 00000000000..2fa121dc15d --- /dev/null +++ b/changelogs/unreleased/justin_ho-fix-confirmation-modal-showing-on-project-integrations.yml @@ -0,0 +1,5 @@ +--- +title: Fix confirmation modal showing on project integration +merge_request: 48720 +author: +type: fixed diff --git a/changelogs/unreleased/tle-fix-button-spacing-on-pipeline-page.yml b/changelogs/unreleased/tle-fix-button-spacing-on-pipeline-page.yml new file mode 100644 index 00000000000..85b7f8e6701 --- /dev/null +++ b/changelogs/unreleased/tle-fix-button-spacing-on-pipeline-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix spacing between buttons on pipeline header +merge_request: 48660 +author: +type: fixed diff --git a/config/feature_flags/development/add_issues_button.yml b/config/feature_flags/development/add_issues_button.yml new file mode 100644 index 00000000000..cc4727a29c4 --- /dev/null +++ b/config/feature_flags/development/add_issues_button.yml @@ -0,0 +1,8 @@ +--- +name: add_issues_button +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47898 +rollout_issue_url: +milestone: '13.6' +type: development +group: group::project management +default_enabled: false diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8822e57e153..948724bdf76 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -1042,6 +1042,10 @@ production: &base shared: # path: /mnt/gitlab # Default: shared + # Encrypted Settings configuration + encrypted_settings: + # path: /mnt/gitlab/encrypted_settings # Default: shared/encrypted_settings + # Gitaly settings gitaly: # Path to the directory containing Gitaly client executables. diff --git a/config/initializers/01_secret_token.rb b/config/initializers/01_secret_token.rb index 5949f463457..d7e725477eb 100644 --- a/config/initializers/01_secret_token.rb +++ b/config/initializers/01_secret_token.rb @@ -34,6 +34,9 @@ def create_tokens openid_connect_signing_key: generate_new_rsa_private_key } + # encrypted_settings_key_base is optional for now + defaults[:encrypted_settings_key_base] = generate_new_secure_token if ENV['GITLAB_GENERATE_ENCRYPTED_SETTINGS_KEY_BASE'] + missing_secrets = set_missing_keys(defaults) write_secrets_yml(missing_secrets) unless missing_secrets.empty? diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d0c4cccd874..f2315b2a431 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -3,6 +3,13 @@ require_relative '../object_store_settings' require_relative '../smime_signature_settings' # Default settings +Settings['shared'] ||= Settingslogic.new({}) +Settings.shared['path'] = Settings.absolute(Settings.shared['path'] || "shared") + +Settings['encrypted_settings'] ||= Settingslogic.new({}) +Settings.encrypted_settings['path'] ||= File.join(Settings.shared['path'], "encrypted_settings") +Settings.encrypted_settings['path'] = Settings.absolute(Settings.encrypted_settings['path']) + Settings['ldap'] ||= Settingslogic.new({}) Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil? Settings.ldap['prevent_ldap_sign_in'] = false if Settings.ldap['prevent_ldap_sign_in'].blank? @@ -140,9 +147,6 @@ if Gitlab.ee? && Rails.env.test? && !saml_provider_enabled Settings.omniauth.providers << Settingslogic.new({ 'name' => 'group_saml' }) end -Settings['shared'] ||= Settingslogic.new({}) -Settings.shared['path'] = Settings.absolute(Settings.shared['path'] || "shared") - Settings['issues_tracker'] ||= {} # diff --git a/config/settings.rb b/config/settings.rb index c681fa32491..3369f2a4480 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -152,6 +152,14 @@ class Settings < Settingslogic Gitlab::Application.secrets.db_key_base end + def encrypted(path) + Gitlab::EncryptedConfiguration.new( + content_path: path, + base_key: Gitlab::Application.secrets.encrypted_settings_key_base, + previous_keys: Gitlab::Application.secrets.rotated_encrypted_settings_key_base || [] + ) + end + def load_dynamic_cron_schedules! cron_jobs['gitlab_usage_ping_worker']['cron'] ||= cron_for_usage_ping end diff --git a/doc/development/application_secrets.md b/doc/development/application_secrets.md index f1e01af7702..06c2c8bf8b5 100644 --- a/doc/development/application_secrets.md +++ b/doc/development/application_secrets.md @@ -16,6 +16,7 @@ This page is a development guide for application secrets. | `otp_key_base` | The base key for One Time Passwords, described in [User management](../raketasks/user_management.md#rotate-two-factor-authentication-encryption-key) | |`db_key_base` | The base key to encrypt the data for `attr_encrypted` columns | |`openid_connect_signing_key` | The singing key for OpenID Connect | +| `encrypted_settings_key_base` | The base key to encrypt settings files with | ## Where the secrets are stored diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 1a51e8ff604..ddf19dc9935 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -7,8 +7,7 @@ description: 'Writing styles, markup, formatting, and other standards for GitLab # Documentation Style Guide -This document defines the standards for GitLab's documentation content and -files. +This document defines the standards for GitLab documentation. For broader information about the documentation, see the [Documentation guidelines](../index.md). @@ -241,16 +240,16 @@ to update. Put files for a specific product area into the related folder: -| Directory | What belongs here | -|:----------------------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `doc/user/` | User related documentation. Anything that can be done within the GitLab user interface goes here, including usage of the `/admin` interface. | -| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed by using GitLab's interface exist under `doc/user/admin_area/`. | -| `doc/api/` | API related documentation. | +| Directory | What belongs here | +|:----------------------|:------------------| +| `doc/user/` | User related documentation. Anything that can be done within the GitLab user interface goes here, including usage of the `/admin` interface. | +| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. Administrator settings in the GitLab user interface are under `doc/user/admin_area/`. | +| `doc/api/` | API-related documentation. | | `doc/development/` | Documentation related to the development of GitLab, whether contributing code or documentation. Related process and style guides should go here. | -| `doc/legal/` | Legal documents about contributing to GitLab. | -| `doc/install/` | Contains instructions for installing GitLab. | -| `doc/update/` | Contains instructions for updating GitLab. | -| `doc/topics/` | Indexes per topic (`doc/topics/topic_name/index.md`): all resources for that topic. | +| `doc/legal/` | Legal documents about contributing to GitLab. | +| `doc/install/` | Contains instructions for installing GitLab. | +| `doc/update/` | Contains instructions for updating GitLab. | +| `doc/topics/` | Indexes per topic (`doc/topics/topic_name/index.md`): all resources for that topic. | ### Work with directories and files @@ -277,10 +276,10 @@ Refer to the following items when working with directories and files: Every page you would navigate under `/profile` should have its own document, for example, `account.md`, `applications.md`, or `emails.md`. - `doc/user/dashboard/` should contain all dashboard related documentation. - - `doc/user/admin_area/` should contain all admin related documentation - describing what can be achieved by accessing GitLab's admin interface - (_not to be confused with `doc/administration` where server access is - required_). + - `doc/user/admin_area/` should contain all administrator-related + documentation describing what can be achieved by accessing the GitLab + administrator interface (not to be confused with `doc/administration` where + server access is required). - Every category under `/admin/application_settings/` should have its own document located at `doc/user/admin_area/settings/`. For example, the **Visibility and Access Controls** category should have a document @@ -567,9 +566,9 @@ tenses, words, and phrases: - Avoid using the word *currently* when talking about the product or its features. The documentation describes the product as it is, and not as it will be at some indeterminate point in the future. -- Avoid the using the word *scalability* with increasing GitLab's performance - for additional users. Using the words *scale* or *scaling* in other cases is - acceptable, but references to increasing GitLab's performance for additional +- Avoid using the word scalability when talking about increasing GitLab + performance for additional users. The words scale or scaling are sometimes + acceptable, but references to increasing GitLab performance for additional users should direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) page. @@ -577,8 +576,8 @@ tenses, words, and phrases: direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) for information about configuring GitLab to have the performance needed for additional users over time. -- Don't use profanity or obscenities. Doing so may negatively affect other - users and contributors, which is contrary to GitLab's value of +- Don't use profanity or obscenities. Doing so may negatively affect other users + and contributors, which is contrary to the GitLab value of [Diversity, Inclusion, and Belonging](https://about.gitlab.com/handbook/values/#diversity-inclusion). - Avoid the use of [racially-insensitive terminology or phrases](https://www.marketplace.org/2020/06/17/tech-companies-update-language-to-avoid-offensive-terms/). For example: - Use *primary* and *secondary* for database and server relationships. @@ -982,9 +981,9 @@ Important: tutorials, presentations, StackOverflow posts, and other sources. - Do not link to `h1` headings. -Note that, with Kramdown, it is possible to add a custom ID to an HTML element -with Markdown markup, but they _do not_ work in GitLab's `/help`. Therefore, -do not use this option until further notice. +Note that with Kramdown, it's possible to add a custom ID to an HTML element +with Markdown markup, but they don't work in `/help`. Because of this, don't use +this option. ## Links @@ -1268,7 +1267,7 @@ request. ## Videos -Adding GitLab's existing YouTube video tutorials to the documentation is highly +Adding GitLab YouTube video tutorials to the documentation is highly encouraged, unless the video is outdated. Videos should not replace documentation, but complement or illustrate it. If content in a video is fundamental to a feature and its key use cases, but this is not adequately @@ -1297,7 +1296,7 @@ You can link any up-to-date video that's useful to the GitLab user. The [GitLab documentation site](https://docs.gitlab.com) supports embedded videos. -You can only embed videos from [GitLab's official YouTube account](https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg). +You can embed videos from [the official YouTube account for GitLab](https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg) only. For videos from other sources, [link](#link-to-video) them instead. In most cases, it is better to [link to video](#link-to-video) instead, because @@ -1341,9 +1340,9 @@ This is how it renders on the GitLab documentation site: > - The `figure` tag is required for semantic SEO and the `video_container` class is necessary to make sure the video is responsive and displays on different mobile devices. -> - The `<div class="video-fallback">` is a fallback necessary for GitLab's -`/help`, as GitLab's Markdown processor does not support iframes. It's hidden on -the documentation site, but will be displayed on GitLab's `/help`. +> - The `<div class="video-fallback">` is a fallback necessary for +`/help`, because the GitLab Markdown processor doesn't support iframes. It's +hidden on the documentation site, but is displayed by `/help`. ## Code blocks @@ -1659,8 +1658,8 @@ elements: To help users be aware of recent product improvements or additions, we add GitLab version information to our documentation. -The GitLab Technical Writing team determines which versions of GitLab's -documentation to display on this site based on GitLab's +The GitLab Technical Writing team determines which versions of +documentation to display on this site based on the GitLab [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#we-support-the-current-major-version-and-the-two-previous-major-versions). ### View older GitLab documentation versions diff --git a/doc/user/project/img/issue_boards_add_issues_modal_v13_6.png b/doc/user/project/img/issue_boards_add_issues_modal_v13_6.png Binary files differdeleted file mode 100644 index a138efc9c1c..00000000000 --- a/doc/user/project/img/issue_boards_add_issues_modal_v13_6.png +++ /dev/null diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index a0880af99c6..9692c6d4f49 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -433,7 +433,13 @@ To remove a list from an issue board: 1. Select **Remove list**. A confirmation dialog appears. 1. Select **OK**. -### Add issues to a list +### Add issues to a list **(CORE ONLY)** + +> - Feature flag [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47898) in GitLab 13.7. +> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default. +> - It's disabled on GitLab.com. +> - It's recommended for production use. +> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-adding-issues-to-the-list). **(CORE ONLY)** You can add issues to a list in a project issue board by clicking the **Add issues** button in the top right corner of the issue board. This opens up a modal @@ -453,7 +459,23 @@ the list by filtering by the following: - Release - Weight -![Bulk adding issues to lists](img/issue_boards_add_issues_modal_v13_6.png) +#### Enable or disable adding issues to the list **(CORE ONLY)** + +Adding issues to the list 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. + +To enable it: + +```ruby +Feature.enable(:add_issues_button) +``` + +To disable it: + +```ruby +Feature.disable(:add_issues_button) +``` ### Remove an issue from a list diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index c2cf648abee..7fa5fbb13c8 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -254,18 +254,18 @@ NOTE: **Note:** Although the Code Climate spec supports more properties, those are ignored by GitLab. -## Code Quality reports **(STARTER)** +## Code Quality reports -Once the Code Quality job has completed: +After the Code Quality job completes: -- The full list of code quality violations generated by a pipeline is shown in the - Code Quality tab of the Pipeline Details page. - Potential changes to code quality are shown directly in the merge request. The Code Quality widget in the merge request compares the reports from the base and head of the branch, then lists any violations that are resolved or created when the branch is merged. - The full JSON report is available as a [downloadable artifact](../../../ci/pipelines/job_artifacts.md#downloading-artifacts) for the `code_quality` job. +- The full list of code quality violations generated by a pipeline is shown in the + Code Quality tab of the Pipeline Details page. **(STARTER)** ### Generating an HTML report diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md index 9b3776c8878..5e43a02403a 100644 --- a/doc/user/project/releases/index.md +++ b/doc/user/project/releases/index.md @@ -79,6 +79,16 @@ To create a new release through the GitLab UI: [release notes](#release-notes-description), or [assets links](#links). 1. Click **Create release**. +### Create release from GitLab CI + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19298) in GitLab 12.7. + +You can [create a release directly from the GitLab CI pipeline](../../../ci/yaml/README.md#release) +by using a `release` node in the job definition. + +The release is created only if the job processes without error. If the Rails API returns an error +during release creation, the release job fails. + ### Schedule a future release > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/38105) in GitLab 12.1. diff --git a/lib/gitlab/encrypted_configuration.rb b/lib/gitlab/encrypted_configuration.rb new file mode 100644 index 00000000000..fe49af3ab33 --- /dev/null +++ b/lib/gitlab/encrypted_configuration.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +module Gitlab + class EncryptedConfiguration + delegate :[], :fetch, to: :config + delegate_missing_to :options + attr_reader :content_path, :key, :previous_keys + + CIPHER = "aes-256-gcm" + SALT = "GitLabEncryptedConfigSalt" + + class MissingKeyError < RuntimeError + def initialize(msg = "Missing encryption key to encrypt/decrypt file with.") + super + end + end + + class InvalidConfigError < RuntimeError + def initialize(msg = "Content was not a valid yml config file") + super + end + end + + def self.generate_key(base_key) + # Because the salt is static, we want uniqueness to be coming from the base_key + # Error if the base_key is empty or suspiciously short + raise 'Base key too small' if base_key.blank? || base_key.length < 16 + + ActiveSupport::KeyGenerator.new(base_key).generate_key(SALT, ActiveSupport::MessageEncryptor.key_len(CIPHER)) + end + + def initialize(content_path: nil, base_key: nil, previous_keys: []) + @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path } if content_path + @key = self.class.generate_key(base_key) if base_key + @previous_keys = previous_keys + end + + def active? + content_path&.exist? + end + + def read + if active? + decrypt(content_path.binread) + else + "" + end + end + + def write(contents) + # ensure contents are valid to deserialize before write + deserialize(contents) + + temp_file = Tempfile.new(File.basename(content_path), File.dirname(content_path)) + File.open(temp_file.path, 'wb') do |file| + file.write(encrypt(contents)) + end + FileUtils.mv(temp_file.path, content_path) + ensure + temp_file&.unlink + end + + def config + return @config if @config + + contents = deserialize(read) + + raise InvalidConfigError.new unless contents.is_a?(Hash) + + @config = contents.deep_symbolize_keys + end + + def change(&block) + writing(read, &block) + end + + private + + def writing(contents) + updated_contents = yield contents + + write(updated_contents) if updated_contents != contents + end + + def encrypt(contents) + handle_missing_key! + encryptor.encrypt_and_sign(contents) + end + + def decrypt(contents) + handle_missing_key! + encryptor.decrypt_and_verify(contents) + end + + def encryptor + return @encryptor if @encryptor + + @encryptor = ActiveSupport::MessageEncryptor.new(key, cipher: CIPHER) + + # Allow fallback to previous keys + @previous_keys.each do |key| + @encryptor.rotate(self.class.generate_key(key)) + end + + @encryptor + end + + def options + # Allows top level keys to be referenced using dot syntax + @options ||= ActiveSupport::InheritableOptions.new(config) + end + + def deserialize(contents) + YAML.safe_load(contents, permitted_classes: [Symbol]).presence || {} + end + + def handle_missing_key! + raise MissingKeyError.new if @key.nil? + end + end +end diff --git a/lib/tasks/gitlab/user_management.rake b/lib/tasks/gitlab/user_management.rake index 5bf3b8c806e..f47e549e795 100644 --- a/lib/tasks/gitlab/user_management.rake +++ b/lib/tasks/gitlab/user_management.rake @@ -6,8 +6,8 @@ namespace :gitlab do result = User.where(id: group.direct_and_indirect_users_with_inactive.select(:id)).update_all(projects_limit: 0, can_create_group: false) ids_count = group.direct_and_indirect_users_with_inactive.count - puts "Done".green if result == ids_count - puts "Something went wrong".red if result != ids_count + puts "Done".color(:green) if result == ids_count + puts "Something went wrong".color(:red) if result != ids_count end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9248e9b1918..d93d2dbb8a3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -896,9 +896,6 @@ msgstr "" msgid "< 1 hour" msgstr "" -msgid "<no scopes selected>" -msgstr "" - msgid "'%{data}' at %{location} does not match format: %{format}" msgstr "" @@ -32767,6 +32764,9 @@ msgstr "" msgid "no one can merge" msgstr "" +msgid "no scopes selected" +msgstr "" + msgid "none" msgstr "" diff --git a/package.json b/package.json index e33ef3ec1a3..ded5488b2b2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.175.0", - "@gitlab/ui": "24.3.0", + "@gitlab/ui": "24.3.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-3", "@rails/ujs": "^6.0.3-2", diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index ed873478fc9..6525ae653c9 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -134,4 +134,20 @@ RSpec.describe Settings do end end end + + describe '.encrypted' do + before do + allow(Gitlab::Application.secrets).to receive(:encryped_settings_key_base).and_return(SecureRandom.hex(64)) + end + + it 'defaults to using the encrypted_settings_key_base for the key' do + expect(Gitlab::EncryptedConfiguration).to receive(:new).with(hash_including(base_key: Gitlab::Application.secrets.encrypted_settings_key_base)) + Settings.encrypted('tmp/tests/test.enc') + end + + it 'returns empty encrypted config when a key has not been set' do + allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil) + expect(Settings.encrypted('tmp/tests/test.enc').read).to be_empty + end + end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index f941adca233..8d0fa3e023b 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -37,6 +37,10 @@ RSpec.describe 'Issue Boards add issue modal', :js do end context 'modal interaction' do + before do + stub_feature_flags(add_issues_button: true) + end + it 'opens modal' do click_button('Add issues') @@ -72,6 +76,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do context 'issues list' do before do + stub_feature_flags(add_issues_button: true) click_button('Add issues') wait_for_requests diff --git a/spec/features/projects/settings/service_desk_setting_spec.rb b/spec/features/projects/settings/service_desk_setting_spec.rb index 59e6f54da2f..d31913d2dcf 100644 --- a/spec/features/projects/settings/service_desk_setting_spec.rb +++ b/spec/features/projects/settings/service_desk_setting_spec.rb @@ -14,20 +14,57 @@ RSpec.describe 'Service Desk Setting', :js do allow_any_instance_of(Project).to receive(:present).with(current_user: user).and_return(presenter) allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true } allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true } - - visit edit_project_path(project) end it 'shows activation checkbox' do + visit edit_project_path(project) + expect(page).to have_selector("#service-desk-checkbox") end - it 'shows incoming email after activating' do - find("#service-desk-checkbox").click - wait_for_requests - project.reload - expect(project.service_desk_enabled).to be_truthy - expect(project.service_desk_address).to be_present - expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_address) + context 'when service_desk_email is disabled' do + before do + allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(false) + + visit edit_project_path(project) + end + + it 'shows incoming email but not project name suffix after activating' do + find("#service-desk-checkbox").click + + wait_for_requests + + project.reload + expect(project.service_desk_enabled).to be_truthy + expect(project.service_desk_address).to be_present + expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_incoming_address) + expect(page).not_to have_selector('#service-desk-project-suffix') + end + end + + context 'when service_desk_email is enabled' do + before do + allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?) { true } + allow(::Gitlab::ServiceDeskEmail).to receive(:address_for_key) { 'address-suffix@example.com' } + + visit edit_project_path(project) + end + + it 'allows setting of custom address suffix' do + find("#service-desk-checkbox").click + wait_for_requests + + project.reload + expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_incoming_address) + + page.within '#js-service-desk' do + fill_in('service-desk-project-suffix', with: 'foo') + click_button 'Save changes' + end + + wait_for_requests + + expect(find('[data-testid="incoming-email"]').value).to eq('address-suffix@example.com') + end end end diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap index 7471ef2c1b3..6f28573c808 100644 --- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap +++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap @@ -9,7 +9,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ menu-class="dropdown-menu-large" > <button - class="btn btn-danger btn-md gl-button split-content-button btn-danger-secondary" + class="btn btn-danger btn-md gl-button split-content-button" type="button" > <!----> @@ -27,7 +27,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ <button aria-expanded="false" aria-haspopup="true" - class="btn dropdown-toggle btn-danger btn-md gl-button gl-dropdown-toggle btn-danger-secondary dropdown-toggle-split" + class="btn dropdown-toggle btn-danger btn-md gl-button gl-dropdown-toggle dropdown-toggle-split" type="button" > <span diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap index 812836e02b7..8c6b446794f 100644 --- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap +++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap @@ -2,7 +2,7 @@ exports[`Design management design version dropdown component renders design version dropdown button 1`] = ` <gl-dropdown-stub - category="tertiary" + category="primary" headertext="" issueiid="" projectpath="" @@ -42,7 +42,7 @@ exports[`Design management design version dropdown component renders design vers exports[`Design management design version dropdown component renders design version list 1`] = ` <gl-dropdown-stub - category="tertiary" + category="primary" headertext="" issueiid="" projectpath="" diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap index f9845c813e2..26ff4aa657b 100644 --- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap +++ b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap @@ -47,7 +47,7 @@ exports[`Alert integration settings form default state should match the default <gl-dropdown-stub block="true" - category="tertiary" + category="primary" data-qa-selector="incident_templates_dropdown" headertext="" id="alert-integration-settings-issue-template" diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap index 751ceb3c235..c40b7c90c72 100644 --- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap +++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap @@ -86,7 +86,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <button aria-expanded="false" aria-haspopup="true" - class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary" + class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" type="button" > <!----> @@ -201,7 +201,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <button aria-expanded="false" aria-haspopup="true" - class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary" + class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" type="button" > <!----> diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index 645aca0b157..17720aeb702 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -33,7 +33,7 @@ exports[`Dashboard template matches the default snapshot 1`] = ` class="mb-2 pr-2 d-flex d-sm-block" > <gl-dropdown-stub - category="tertiary" + category="primary" class="flex-grow-1" data-qa-selector="environments_dropdown" headertext="" diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap index 30cc87242d0..324c9788309 100644 --- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap +++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap @@ -10,7 +10,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`] <!----> <gl-dropdown-stub - category="tertiary" + category="primary" headertext="" size="medium" text="rspec" diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap index 9d160eb5ba4..dd88ba9a6fb 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap @@ -2,7 +2,7 @@ exports[`SplitButton renders actionItems 1`] = ` <gl-dropdown-stub - category="tertiary" + category="primary" headertext="" menu-class="" size="medium" diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index 51a14aca4ad..ab16dbad3fc 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'create_tokens' do describe 'ensure acknowledged secrets in any installations' do let(:acknowledged_secrets) do - %w[secret_key_base otp_key_base db_key_base openid_connect_signing_key] + %w[secret_key_base otp_key_base db_key_base openid_connect_signing_key encrypted_settings_key_base rotated_encrypted_settings_key_base] end it 'does not allow to add a new secret without a proper handling' do @@ -90,6 +90,7 @@ RSpec.describe 'create_tokens' do expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base) expect(new_secrets['db_key_base']).to eq(secrets.db_key_base) expect(new_secrets['openid_connect_signing_key']).to eq(secrets.openid_connect_signing_key) + expect(new_secrets['encrypted_settings_key_base']).to eq(secrets.encrypted_settings_key_base) end create_tokens @@ -106,6 +107,7 @@ RSpec.describe 'create_tokens' do before do secrets.db_key_base = 'db_key_base' secrets.openid_connect_signing_key = 'openid_connect_signing_key' + secrets.encrypted_settings_key_base = 'encrypted_settings_key_base' allow(File).to receive(:exist?).with('.secret').and_return(true) stub_file_read('.secret', content: 'file_key') @@ -158,6 +160,7 @@ RSpec.describe 'create_tokens' do expect(secrets.otp_key_base).to eq('otp_key_base') expect(secrets.db_key_base).to eq('db_key_base') expect(secrets.openid_connect_signing_key).to eq('openid_connect_signing_key') + expect(secrets.encrypted_settings_key_base).to eq('encrypted_settings_key_base') end it 'deletes the .secret file' do @@ -208,12 +211,34 @@ RSpec.describe 'create_tokens' do create_tokens end end + + context 'when rotated_encrypted_settings_key_base does not exist' do + before do + secrets.secret_key_base = 'secret_key_base' + secrets.otp_key_base = 'otp_key_base' + secrets.openid_connect_signing_key = 'openid_connect_signing_key' + secrets.encrypted_settings_key_base = 'encrypted_settings_key_base' + end + + it 'does not warn about the missing secrets' do + expect(self).not_to receive(:warn_missing_secret).with('rotated_encrypted_settings_key_base') + + create_tokens + end + + it 'does not update secrets.yml' do + expect(File).not_to receive(:write) + + create_tokens + end + end end context 'when db_key_base is blank but exists in secrets.yml' do before do secrets.otp_key_base = 'otp_key_base' secrets.secret_key_base = 'secret_key_base' + secrets.encrypted_settings_key_base = 'encrypted_settings_key_base' yaml_secrets = secrets.to_h.stringify_keys.merge('db_key_base' => '<%= an_erb_expression %>') allow(File).to receive(:exist?).with('.secret').and_return(false) diff --git a/spec/lib/gitlab/encrypted_configuration_spec.rb b/spec/lib/gitlab/encrypted_configuration_spec.rb new file mode 100644 index 00000000000..eadc2cf71a7 --- /dev/null +++ b/spec/lib/gitlab/encrypted_configuration_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::EncryptedConfiguration do + subject(:configuration) { described_class.new } + + let!(:config_tmp_dir) { Dir.mktmpdir('config-') } + + after do + FileUtils.rm_f(config_tmp_dir) + end + + describe '#initialize' do + it 'accepts all args as optional fields' do + expect { configuration }.not_to raise_exception + + expect(configuration.key).to be_nil + expect(configuration.previous_keys).to be_empty + end + + it 'generates 32 byte key when provided a larger base key' do + configuration = described_class.new(base_key: 'A' * 64) + + expect(configuration.key.bytesize).to eq 32 + end + + it 'generates 32 byte key when provided a smaller base key' do + configuration = described_class.new(base_key: 'A' * 16) + + expect(configuration.key.bytesize).to eq 32 + end + + it 'throws an error when the base key is too small' do + expect { described_class.new(base_key: 'A' * 12) }.to raise_error 'Base key too small' + end + end + + context 'when provided a config file but no key' do + let(:config_path) { File.join(config_tmp_dir, 'credentials.yml.enc') } + + it 'throws an error when writing without a key' do + expect { described_class.new(content_path: config_path).write('test') }.to raise_error Gitlab::EncryptedConfiguration::MissingKeyError + end + + it 'throws an error when reading without a key' do + config = described_class.new(content_path: config_path) + File.write(config_path, 'test') + expect { config.read }.to raise_error Gitlab::EncryptedConfiguration::MissingKeyError + end + end + + context 'when provided key and config file' do + let(:credentials_config_path) { File.join(config_tmp_dir, 'credentials.yml.enc') } + let(:credentials_key) { SecureRandom.hex(64) } + + describe '#write' do + it 'encrypts the file using the provided key' do + encryptor = ActiveSupport::MessageEncryptor.new(Gitlab::EncryptedConfiguration.generate_key(credentials_key), cipher: 'aes-256-gcm') + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write('sample-content') + expect(encryptor.decrypt_and_verify(File.read(credentials_config_path))).to eq('sample-content') + end + end + + describe '#read' do + it 'reads yaml configuration' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + expect(config[:foo][:bar]).to be true + end + + it 'allows referencing top level keys via dot syntax' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + expect(config.foo[:bar]).to be true + end + + it 'throws a custom error when referencing an invalid key map config' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write("stringcontent") + expect { config[:foo] }.to raise_error Gitlab::EncryptedConfiguration::InvalidConfigError + end + end + + describe '#change' do + it 'changes yaml configuration' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + config.change do |unencrypted_contents| + contents = YAML.safe_load(unencrypted_contents, permitted_classes: [Symbol]) + contents.merge(beef: "stew").to_yaml + end + expect(config.foo[:bar]).to be true + expect(config.beef).to eq('stew') + end + end + + context 'when provided previous_keys for rotation' do + let(:credential_key_original) { SecureRandom.hex(64) } + let(:credential_key_latest) { SecureRandom.hex(64) } + let(:config_path_original) { File.join(config_tmp_dir, 'credentials-orig.yml.enc') } + let(:config_path_latest) { File.join(config_tmp_dir, 'credentials-latest.yml.enc') } + + def encryptor(key) + ActiveSupport::MessageEncryptor.new(Gitlab::EncryptedConfiguration.generate_key(key), cipher: 'aes-256-gcm') + end + + describe '#write' do + it 'rotates the key when provided a new key' do + config1 = described_class.new(content_path: config_path_original, base_key: credential_key_original) + config1.write('sample-content1') + + config2 = described_class.new(content_path: config_path_latest, base_key: credential_key_latest, previous_keys: [credential_key_original]) + config2.write('sample-content2') + + original_key_encryptor = encryptor(credential_key_original) # can read with the initial key + latest_key_encryptor = encryptor(credential_key_latest) # can read with the new key + both_key_encryptor = encryptor(credential_key_latest) # can read with either key + both_key_encryptor.rotate(Gitlab::EncryptedConfiguration.generate_key(credential_key_original)) + + expect(original_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1') + expect(both_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1') + expect(latest_key_encryptor.decrypt_and_verify(File.read(config_path_latest))).to eq('sample-content2') + expect(both_key_encryptor.decrypt_and_verify(File.read(config_path_latest))).to eq('sample-content2') + expect { original_key_encryptor.decrypt_and_verify(File.read(config_path_latest)) }.to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage) + end + end + + describe '#read' do + it 'supports reading using rotated config' do + described_class.new(content_path: config_path_original, base_key: credential_key_original).write({ foo: { bar: true } }.to_yaml) + + config = described_class.new(content_path: config_path_original, base_key: credential_key_latest, previous_keys: [credential_key_original]) + expect(config[:foo][:bar]).to be true + end + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f9c06b20d9c..0ca35476233 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1535,6 +1535,42 @@ RSpec.describe Project, factory_default: :keep do end end + describe '.service_desk_custom_address_enabled?' do + let_it_be(:project) { create(:project, service_desk_enabled: true) } + + subject(:address_enabled) { project.service_desk_custom_address_enabled? } + + context 'when service_desk_email is enabled' do + before do + allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(true) + end + + it 'returns true' do + expect(address_enabled).to be_truthy + end + + context 'when service_desk_custom_address flag is disabled' do + before do + stub_feature_flags(service_desk_custom_address: false) + end + + it 'returns false' do + expect(address_enabled).to be_falsey + end + end + end + + context 'when service_desk_email is disabled' do + before do + allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(false) + end + + it 'returns false when service_desk_email is disabled' do + expect(address_enabled).to be_falsey + end + end + end + describe '.find_by_service_desk_project_key' do it 'returns the correct project' do project1 = create(:project) diff --git a/yarn.lock b/yarn.lock index f7dff90667b..1d4289a7c81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -866,10 +866,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.175.0.tgz#734f341784af1cd1d62d160a17bcdfb61ff7b04d" integrity sha512-gXpc87TGSXIzfAr4QER1Qw1v3P47pBO6BXkma52blgwXVmcFNe3nhQzqsqt66wKNzrIrk3lAcB4GUyPHbPVXpg== -"@gitlab/ui@24.3.0": - version "24.3.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-24.3.0.tgz#6e3f7d6e198d12faad046ba819ffc56aa520d2ea" - integrity sha512-jfDMEEUrJUC8OoQrtpeupomfbiIX0AnQ5SYaugVwkNocRQ/dWwFf1OwbnKh3BNRNUS2581Vt1yjZb32CO8+GiA== +"@gitlab/ui@24.3.1": + version "24.3.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-24.3.1.tgz#3bc40a1e33326fa89cf25d29afa1628c0ab59684" + integrity sha512-m2/Q/vuWrDt5ZGcdbf7VB6h152VtDA+BeVZu1T8RxzQGzgCJ9XnRqb8wTvW4KOXzdbsY/iqCHPRYhZKVrnakqg== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |