diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-27 18:10:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-27 18:10:15 +0300 |
commit | 9979d2afd66e12d938ea55372dcf2d8105b5eaca (patch) | |
tree | 88df8f6a2f2ffba2fa1318dee83f58d6fe76d40d | |
parent | fdf32113c3924f7faec91101282fc28ec42fc869 (diff) |
Add latest changes from gitlab-org/gitlab@master
78 files changed, 1370 insertions, 391 deletions
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index edd083de8d9..04c7e3c4403 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -1258,7 +1258,6 @@ RSpec/MissingFeatureCategory: - 'ee/spec/presenters/ci/minutes/usage_presenter_spec.rb' - 'ee/spec/presenters/ci/pipeline_presenter_spec.rb' - 'ee/spec/presenters/dast/site_profile_presenter_spec.rb' - - 'ee/spec/presenters/ee/blob_presenter_spec.rb' - 'ee/spec/presenters/ee/clusters/cluster_presenter_spec.rb' - 'ee/spec/presenters/ee/instance_clusterable_presenter_spec.rb' - 'ee/spec/presenters/ee/issue_presenter_spec.rb' @@ -3335,7 +3334,6 @@ RSpec/MissingFeatureCategory: - 'spec/lib/gitlab/conflict/file_spec.rb' - 'spec/lib/gitlab/consul/internal_spec.rb' - 'spec/lib/gitlab/container_repository/tags/cache_spec.rb' - - 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb' - 'spec/lib/gitlab/counters/buffered_counter_spec.rb' - 'spec/lib/gitlab/counters/legacy_counter_spec.rb' - 'spec/lib/gitlab/cross_project_access/check_collection_spec.rb' @@ -5122,7 +5120,6 @@ RSpec/MissingFeatureCategory: - 'spec/policies/work_item_policy_spec.rb' - 'spec/presenters/alert_management/alert_presenter_spec.rb' - 'spec/presenters/award_emoji_presenter_spec.rb' - - 'spec/presenters/blob_presenter_spec.rb' - 'spec/presenters/blobs/notebook_presenter_spec.rb' - 'spec/presenters/blobs/unfold_presenter_spec.rb' - 'spec/presenters/ci/bridge_presenter_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 709793282de..f21e9b1b461 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -6eb139660ed3ebc5921f565672a00c71973fc620 +b3ea2f8bb802758d667f642b1228d31990559343 @@ -407,7 +407,7 @@ group :development, :test do gem 'database_cleaner', '~> 1.7.0' gem 'factory_bot_rails', '~> 6.2.0' - gem 'rspec-rails', '~> 6.0.1' + gem 'rspec-rails', '~> 6.0.3' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.11.0' diff --git a/Gemfile.checksum b/Gemfile.checksum index 9617337b5db..39cf6469b23 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -525,13 +525,13 @@ {"name":"rqrcode-rails3","version":"0.1.7","platform":"ruby","checksum":"6f0582f26485123e5ed6f2a8a2871f00d86d353e0f58c8429a5a13212bcf48c4"}, {"name":"rspec","version":"3.12.0","platform":"ruby","checksum":"ccc41799a43509dc0be84070e3f0410ac95cbd480ae7b6c245543eb64162399c"}, {"name":"rspec-benchmark","version":"0.6.0","platform":"ruby","checksum":"1014adb57ec2599a2455c63884229f367a2fff6a63a77fd68ce5d804c83dd6cf"}, -{"name":"rspec-core","version":"3.12.0","platform":"ruby","checksum":"c466f4137966526e177d2156ca45c249eeecc7ed519b23ae2fb80c4675406bc5"}, -{"name":"rspec-expectations","version":"3.12.2","platform":"ruby","checksum":"8652db70b25ae3378b7274477a906b6ad1833a7b7cfbb001a03f49dd1c1d6a0d"}, +{"name":"rspec-core","version":"3.12.2","platform":"ruby","checksum":"155b54480f28e2b2813185077fe435c2d663031616360ed3b179a9d6a55d2551"}, +{"name":"rspec-expectations","version":"3.12.3","platform":"ruby","checksum":"093d18e2e7e0a2c619ef8f7343d442fc6c0793fb7897d56f16f26c8a9d244416"}, {"name":"rspec-mocks","version":"3.12.6","platform":"ruby","checksum":"de51a4148ba2ce6f1c1646a2a03e9df2f52da9a42b164f2e7467b2cbe37e07bf"}, {"name":"rspec-parameterized","version":"1.0.0","platform":"ruby","checksum":"9c07b043c72afbd23dd9a1dd48c06f46bc2fb1a6d875c6703e254932ba28b386"}, {"name":"rspec-parameterized-core","version":"1.0.0","platform":"ruby","checksum":"287b494985e79821160af63aba4f91db8dbfa9a21cb200db34ba38f40e16ccc1"}, {"name":"rspec-parameterized-table_syntax","version":"1.0.0","platform":"ruby","checksum":"d7df951eff9c5dd367ca7d5f9ae4853bb7ab7941f9d5b35bba361d112704988c"}, -{"name":"rspec-rails","version":"6.0.1","platform":"ruby","checksum":"016c8ebd5b38ce5cbce949de2f5b28f2bde7bb78d4de26940516713597b26e34"}, +{"name":"rspec-rails","version":"6.0.3","platform":"ruby","checksum":"6d1812cfaf18dba5a08d7e30c85149b24a220fae064853a96e451376be6fd820"}, {"name":"rspec-retry","version":"0.6.2","platform":"ruby","checksum":"6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858"}, {"name":"rspec-support","version":"3.12.0","platform":"ruby","checksum":"dd4d44b247ff679b95b5607ac5641d197a5f9b1d33f916123cb98fc5f917c58b"}, {"name":"rspec_junit_formatter","version":"0.6.0","platform":"ruby","checksum":"40dde674e6ae4e6cc0ff560da25497677e34fefd2338cc467a8972f602b62b15"}, diff --git a/Gemfile.lock b/Gemfile.lock index 740779f811c..e5a459b0a9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1348,9 +1348,9 @@ GEM benchmark-perf (~> 0.6) benchmark-trend (~> 0.4) rspec (>= 3.0) - rspec-core (3.12.0) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-mocks (3.12.6) @@ -1367,14 +1367,14 @@ GEM rspec-parameterized-table_syntax (1.0.0) binding_of_caller rspec-parameterized-core (< 2) - rspec-rails (6.0.1) + rspec-rails (6.0.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.11) - rspec-expectations (~> 3.11) - rspec-mocks (~> 3.11) - rspec-support (~> 3.11) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.12.0) @@ -1968,7 +1968,7 @@ DEPENDENCIES rqrcode-rails3 (~> 0.1.7) rspec-benchmark (~> 0.6.0) rspec-parameterized (~> 1.0) - rspec-rails (~> 6.0.1) + rspec-rails (~> 6.0.3) rspec-retry (~> 0.6.2) rspec_flaky! rspec_junit_formatter diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue index d15c8e6e703..85b3c994e02 100644 --- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue +++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue @@ -32,7 +32,6 @@ export default { i18n: { emptyField: __('Never'), expired: __('Expired'), - header: __('Active %{accessTokenTypePlural} (%{totalAccessTokens})'), modalMessage: __( 'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.', ), @@ -45,7 +44,6 @@ export default { 'initialActiveAccessTokens', 'noActiveTokensMessage', 'showRole', - 'information', ], data() { return { @@ -74,12 +72,6 @@ export default { return FIELDS.filter(({ key }) => !ignoredFields.includes(key)); }, - header() { - return sprintf(this.$options.i18n.header, { - accessTokenTypePlural: this.accessTokenTypePlural, - totalAccessTokens: this.activeAccessTokens.length, - }); - }, modalMessage() { return sprintf(this.$options.i18n.modalMessage, { accessTokenType: this.accessTokenType, @@ -114,65 +106,66 @@ export default { <template> <dom-element-listener :selector="$options.FORM_SELECTOR" @[$options.EVENT_SUCCESS]="onSuccess"> - <div class="gl-pt-6"> - <h5>{{ header }}</h5> - - <p v-if="information" data-testid="information-section"> - {{ information }} - </p> - - <gl-table - data-testid="active-tokens" - :empty-text="noActiveTokensMessage" - :fields="filteredFields" - :items="activeAccessTokens" - :per-page="$options.PAGE_SIZE" - :current-page="currentPage" - :sort-compare="sortingChanged" - show-empty + <div> + <div + class="gl-new-card-body gl-px-0 gl-overflow-hidden gl-bg-gray-10 gl-border-l gl-border-r gl-border-b gl-rounded-bottom-base gl-mb-5 gl-md-mb-0" > - <template #cell(createdAt)="{ item: { createdAt } }"> - <user-date :date="createdAt" /> - </template> + <gl-table + data-testid="active-tokens" + :empty-text="noActiveTokensMessage" + :fields="filteredFields" + :items="activeAccessTokens" + :per-page="$options.PAGE_SIZE" + :current-page="currentPage" + :sort-compare="sortingChanged" + show-empty + stacked="sm" + > + <template #cell(createdAt)="{ item: { createdAt } }"> + <user-date :date="createdAt" /> + </template> - <template #head(lastUsedAt)="{ label }"> - <span>{{ label }}</span> - <gl-link :href="$options.lastUsedHelpLink" - ><gl-icon name="question-o" /><span class="gl-sr-only">{{ - s__('AccessTokens|The last time a token was used') - }}</span></gl-link - > - </template> + <template #head(lastUsedAt)="{ label }"> + <span>{{ label }}</span> + <gl-link :href="$options.lastUsedHelpLink" + ><gl-icon name="question-o" /><span class="gl-sr-only">{{ + s__('AccessTokens|The last time a token was used') + }}</span></gl-link + > + </template> - <template #cell(lastUsedAt)="{ item: { lastUsedAt } }"> - <time-ago-tooltip v-if="lastUsedAt" :time="lastUsedAt" /> - <template v-else> {{ $options.i18n.emptyField }}</template> - </template> + <template #cell(lastUsedAt)="{ item: { lastUsedAt } }"> + <time-ago-tooltip v-if="lastUsedAt" :time="lastUsedAt" /> + <template v-else> {{ $options.i18n.emptyField }}</template> + </template> - <template #cell(expiresAt)="{ item: { expiresAt, expired, expiresSoon } }"> - <template v-if="expiresAt"> - <span v-if="expired" class="text-danger">{{ $options.i18n.expired }}</span> - <time-ago-tooltip v-else :class="{ 'text-warning': expiresSoon }" :time="expiresAt" /> + <template #cell(expiresAt)="{ item: { expiresAt, expired, expiresSoon } }"> + <template v-if="expiresAt"> + <span v-if="expired" class="text-danger">{{ $options.i18n.expired }}</span> + <time-ago-tooltip v-else :class="{ 'text-warning': expiresSoon }" :time="expiresAt" /> + </template> + <span v-else v-gl-tooltip :title="$options.i18n.tokenValidity">{{ + $options.i18n.emptyField + }}</span> </template> - <span v-else v-gl-tooltip :title="$options.i18n.tokenValidity">{{ - $options.i18n.emptyField - }}</span> - </template> - <template #cell(action)="{ item: { revokePath } }"> - <gl-button - v-if="revokePath" - category="tertiary" - :aria-label="$options.i18n.revokeButton" - :data-confirm="modalMessage" - data-confirm-btn-variant="danger" - data-qa-selector="revoke_button" - data-method="put" - :href="revokePath" - icon="remove" - /> - </template> - </gl-table> + <template #cell(action)="{ item: { revokePath } }"> + <gl-button + v-if="revokePath" + category="tertiary" + :title="$options.i18n.revokeButton" + :aria-label="$options.i18n.revokeButton" + :data-confirm="modalMessage" + data-confirm-btn-variant="danger" + data-qa-selector="revoke_button" + data-method="put" + :href="revokePath" + icon="remove" + class="has-tooltip" + /> + </template> + </gl-table> + </div> <gl-pagination v-if="showPagination" v-model="currentPage" @@ -183,6 +176,7 @@ export default { :label-next-page="__('Go to next page')" :label-prev-page="__('Go to previous page')" align="center" + class="gl-mt-5" /> </div> </dom-element-listener> diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue index dfac423b65e..4b51b4333aa 100644 --- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue +++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue @@ -93,6 +93,12 @@ export default { this.form.querySelectorAll('input[type=checkbox]').forEach((el) => { el.checked = false; }); + document.querySelectorAll('.js-token-card').forEach((el) => { + el.querySelector('.js-add-new-token-form').style.display = ''; + el.querySelector('.js-toggle-button').style.display = 'block'; + el.querySelector('.js-token-count').innerText = + parseInt(el.querySelector('.js-token-count').innerText, 10) + 1; + }); }, }, }; @@ -105,7 +111,12 @@ export default { @[$options.EVENT_SUCCESS]="onSuccess" > <div ref="container" data-testid="access-token-section" data-qa-selector="access_token_section"> - <template v-if="newToken"> + <gl-alert + v-if="newToken" + variant="success" + data-testid="success-message" + @dismiss="newToken = null" + > <input-copy-toggle-visibility :copy-button-title="copyButtonTitle" :label="label" @@ -113,16 +124,22 @@ export default { :value="newToken" :form-input-group-props="formInputGroupProps" readonly + size="lg" + class="gl-mb-0" > <template #description> {{ $options.i18n.description }} </template> </input-copy-toggle-visibility> - <hr /> - </template> + </gl-alert> <template v-if="errors"> - <gl-alert :title="alertDangerTitle" variant="danger" @dismiss="errors = null"> + <gl-alert + :title="alertDangerTitle" + variant="danger" + data-testid="error-message" + @dismiss="errors = null" + > <ul class="gl-m-0"> <li v-for="error in errors" :key="error"> {{ error }} diff --git a/app/assets/javascripts/access_tokens/components/token.vue b/app/assets/javascripts/access_tokens/components/token.vue index 09263d8e0ea..3b25e331499 100644 --- a/app/assets/javascripts/access_tokens/components/token.vue +++ b/app/assets/javascripts/access_tokens/components/token.vue @@ -20,6 +20,11 @@ export default { type: String, required: true, }, + size: { + type: String, + required: false, + default: '', + }, }, computed: { formInputGroupProps() { @@ -40,6 +45,7 @@ export default { :value="token" :copy-button-title="copyButtonTitle" readonly + :size="size" > <template #description> <slot name="input-description"></slot> diff --git a/app/assets/javascripts/access_tokens/components/tokens_app.vue b/app/assets/javascripts/access_tokens/components/tokens_app.vue index 88119ed8a84..af26bf85941 100644 --- a/app/assets/javascripts/access_tokens/components/tokens_app.vue +++ b/app/assets/javascripts/access_tokens/components/tokens_app.vue @@ -88,6 +88,7 @@ export default { :input-label="$options.i18n[tokenType].label" :copy-button-title="$options.i18n[tokenType].copyButtonTitle" :data-testid="$options.htmlAttributes[tokenType].containerTestId" + size="md" > <template #title> <div class="settings-sticky-header"> diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js index 510f118bbb5..4e0acaa74da 100644 --- a/app/assets/javascripts/access_tokens/index.js +++ b/app/assets/javascripts/access_tokens/index.js @@ -20,7 +20,6 @@ export const initAccessTokenTableApp = () => { const { accessTokenType, accessTokenTypePlural, - information, initialActiveAccessTokens: initialActiveAccessTokensJson, noActiveTokensMessage: noTokensMessage, } = el.dataset; @@ -39,7 +38,6 @@ export const initAccessTokenTableApp = () => { provide: { accessTokenType, accessTokenTypePlural, - information, initialActiveAccessTokens, noActiveTokensMessage, showRole, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue index c38c253564a..68d81e6b60a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue @@ -74,9 +74,8 @@ export default { </script> <template> - <div class="gl-display-flex gl-align-items-flex-start"> + <div v-if="tertiaryButtons.length" class="gl-display-flex gl-align-items-flex-start"> <gl-dropdown - v-if="tertiaryButtons.length" v-gl-tooltip :title="__('Options')" :text="dropdownLabel" @@ -102,33 +101,31 @@ export default { {{ btn.text }} </gl-dropdown-item> </gl-dropdown> - <template v-if="tertiaryButtons.length"> - <gl-button - v-for="(btn, index) in tertiaryButtons" - :id="btn.id" - :key="index" - v-gl-tooltip.hover - :title="setTooltip(btn)" - :href="btn.href" - :target="btn.target" - :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]" - :data-clipboard-text="btn.dataClipboardText" - :data-qa-selector="actionButtonQaSelector(btn)" - :data-method="btn.dataMethod" - :icon="btn.icon" - :data-testid="btn.testId || 'extension-actions-button'" - :variant="btn.variant || 'confirm'" - :loading="btn.loading" - :disabled="btn.loading" - category="tertiary" - size="small" - class="gl-display-none gl-md-display-block gl-float-left" - @click="onClickAction(btn)" - > - <template v-if="btn.text"> - {{ btn.text }} - </template> - </gl-button> - </template> + <gl-button + v-for="(btn, index) in tertiaryButtons" + :id="btn.id" + :key="index" + v-gl-tooltip.hover + :title="setTooltip(btn)" + :href="btn.href" + :target="btn.target" + :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]" + :data-clipboard-text="btn.dataClipboardText" + :data-qa-selector="actionButtonQaSelector(btn)" + :data-method="btn.dataMethod" + :icon="btn.icon" + :data-testid="btn.testId || 'extension-actions-button'" + :variant="btn.variant || 'confirm'" + :loading="btn.loading" + :disabled="btn.loading" + category="tertiary" + size="small" + class="gl-display-none gl-md-display-block gl-float-left" + @click="onClickAction(btn)" + > + <template v-if="btn.text"> + {{ btn.text }} + </template> + </gl-button> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue index ec979861283..1a1152446ce 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue @@ -73,10 +73,16 @@ export default { <template #body> <div class="gl-w-full gl-display-flex" :class="{ 'gl-flex-direction-column': level === 1 }"> <div class="gl-display-flex gl-flex-grow-1"> - <div class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column"> - <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p> - <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> - <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p> + <div class="gl-display-flex gl-flex-grow-1 gl-align-items-baseline"> + <div> + <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p> + <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> + <p + v-if="data.supportingText" + v-safe-html="generatedSupportingText" + class="gl-mb-0" + ></p> + </div> <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> {{ data.badge.text }} </gl-badge> diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue index c371c1aeff9..eca5803318a 100644 --- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue +++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue @@ -65,6 +65,11 @@ export default { return {}; }, }, + size: { + type: String, + required: false, + default: '', + }, }, data() { if (!this.readonly && !this.value) { @@ -151,6 +156,7 @@ export default { <gl-form-input ref="input" :readonly="readonly" + :size="size" class="gl-font-monospace! gl-cursor-default!" v-bind="formInputGroupProps" :value="value" diff --git a/app/assets/stylesheets/framework/new_card.scss b/app/assets/stylesheets/framework/new_card.scss index 411f5300120..a1d7761bd88 100644 --- a/app/assets/stylesheets/framework/new_card.scss +++ b/app/assets/stylesheets/framework/new_card.scss @@ -91,7 +91,9 @@ @include gl-border-gray-100; @include gl-rounded-base; } +} +.gl-new-card-body { // Table adjustments @mixin new-card-table-adjustments { tbody > tr { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 6aca2b5646c..ad792a6ee50 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -76,14 +76,10 @@ } } -.settings-section { - @include gl-pt-6; - - &::after { - content: ''; - display: block; - @include gl-pb-5; - } +.settings-section::after { + content: ''; + display: block; + @include gl-pb-7; } .settings-section, @@ -91,6 +87,11 @@ @include gl-pt-0; } +// Fix for sticky header when there is no search bar. +.flash-container + .settings-section { + @include gl-pt-3; +} + .settings-section ~ .settings-section { @include gl-pt-6; } diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb index 22f1a81b95b..80f7153cd7a 100644 --- a/app/controllers/repositories/lfs_storage_controller.rb +++ b/app/controllers/repositories/lfs_storage_controller.rb @@ -18,10 +18,7 @@ module Repositories def download lfs_object = LfsObject.find_by_oid(oid) - unless lfs_object && lfs_object.file.exists? - render_lfs_not_found - return - end + return render_lfs_not_found unless lfs_object&.file&.exists? send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" }) end diff --git a/app/graphql/types/custom_emoji_type.rb b/app/graphql/types/custom_emoji_type.rb index 379a0c44d67..b02cd56e6df 100644 --- a/app/graphql/types/custom_emoji_type.rb +++ b/app/graphql/types/custom_emoji_type.rb @@ -7,6 +7,10 @@ module Types authorize :read_custom_emoji + connection_type_class(Types::CountableConnectionType) + + expose_permissions Types::PermissionTypes::CustomEmoji + field :id, ::Types::GlobalIDType[::CustomEmoji], null: false, description: 'ID of the emoji.' @@ -23,5 +27,9 @@ module Types field :external, GraphQL::Types::Boolean, null: false, description: 'Whether the emoji is an external link.' + + field :created_at, Types::TimeType, + null: false, + description: 'Timestamp of when the custom emoji was created.' end end diff --git a/app/graphql/types/permission_types/group.rb b/app/graphql/types/permission_types/group.rb index 6a1031e2532..fbd1140babc 100644 --- a/app/graphql/types/permission_types/group.rb +++ b/app/graphql/types/permission_types/group.rb @@ -5,7 +5,7 @@ module Types class Group < BasePermissionType graphql_name 'GroupPermissions' - abilities :read_group, :create_projects + abilities :read_group, :create_projects, :create_custom_emoji end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index ded4b06a028..d7aa66588d3 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -29,7 +29,8 @@ class Commit delegate :project, to: :repository, allow_nil: true MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH - COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze + MAX_SHA_LENGTH = Gitlab::Git::Commit::MAX_SHA_LENGTH + COMMIT_SHA_PATTERN = Gitlab::Git::Commit::SHA_PATTERN.freeze EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/.freeze # Used by GFM to match and present link extensions on node texts and hrefs. LINK_EXTENSION_PATTERN = /(patch)/.freeze diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index c6e507e4b6c..d882a185464 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -31,9 +31,8 @@ class CommitRange REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/.freeze PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/.freeze - # In text references, the beginning and ending refs can only be SHAs - # between 7 and 40 hex characters. - STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/.freeze + # In text references, the beginning and ending refs can only be valid SHAs. + STRICT_PATTERN = /#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\.{2,3}#{Gitlab::Git::Commit::RAW_SHA_PATTERN}/.freeze def self.reference_prefix '@' diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb index bc12d210334..f58ba2b3eaf 100644 --- a/app/presenters/blob_presenter.rb +++ b/app/presenters/blob_presenter.rb @@ -6,6 +6,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated include DiffHelper include TreeHelper include ChecksCollaboration + include Gitlab::Utils::StrongMemoize presents ::Blob, as: :blob @@ -149,6 +150,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated end def external_storage_url + return lfs_object_url if lfs_blob? return unless static_objects_external_storage_enabled? external_storage_url_or_path(url_helpers.project_raw_url(project, ref_qualified_path), project) @@ -164,6 +166,25 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated private + def lfs_object_url + return unless lfs_object.present? + + if LfsObjectUploader.proxy_download_enabled? || lfs_object.file.file_storage? + "#{project.http_url_to_repo}/gitlab-lfs/objects/#{lfs_object.oid}" + else + lfs_object.file.url(content_type: "application/octet-stream") + end + end + + def lfs_object + project.lfs_objects.find_by_oid(blob.lfs_oid) if lfs_blob? + end + strong_memoize_attr :lfs_object + + def lfs_blob? + blob.stored_externally? && blob.lfs_pointer? + end + def path_params if ref_type.present? [project, ref_qualified_path, { ref_type: ref_type }] diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml index 0208b8ad836..a844b6846a9 100644 --- a/app/views/admin/impersonation_tokens/index.html.haml +++ b/app/views/admin/impersonation_tokens/index.html.haml @@ -6,11 +6,24 @@ = render 'admin/users/head' -.row.gl-mt-3 - .col-lg-12 - #js-new-access-token-app{ data: { access_token_type: type } } +#js-new-access-token-app{ data: { access_token_type: type } } - = render 'shared/access_tokens/form', += render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c| + - c.with_header do + .gl-new-card-title-wrapper.gl-flex-direction-column + %h3.gl-new-card-title + = _('Impersonation tokens') + .gl-new-card-count + = sprite_icon('token', css_class: 'gl-mr-2') + %span.js-token-count= @active_impersonation_tokens.size + .gl-new-card-description + = _("To see all the user's personal access tokens you must impersonate them first.") + .gl-new-card-actions + = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do + = _('Add new token') + - c.with_body do + .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form + = render 'shared/access_tokens/form', ajax: true, type: type, title: _('Add an impersonation token'), @@ -20,4 +33,4 @@ scopes: @scopes, help_path: help_page_path('api/rest/index', anchor: 'impersonation-tokens') - #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_impersonation_tokens.to_json, information: _("To see all the user's personal access tokens you must impersonate them first.") } } +#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_impersonation_tokens.to_json } } diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml index ac3be429461..927162d2284 100644 --- a/app/views/groups/settings/access_tokens/index.html.haml +++ b/app/views/groups/settings/access_tokens/index.html.haml @@ -25,18 +25,31 @@ #js-new-access-token-app{ data: { access_token_type: type } } - - if current_user.can?(:create_resource_access_tokens, @group) - = render 'shared/access_tokens/form', - ajax: true, - type: type, - path: group_settings_access_tokens_path(@group), - resource: @group, - token: @resource_access_token, - scopes: @scopes, - access_levels: GroupMember.access_level_roles, - default_access_level: Gitlab::Access::GUEST, - prefix: :resource_access_token, - help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token') + = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c| + - c.with_header do + .gl-new-card-title-wrapper + %h3.gl-new-card-title + = _('Active group access tokens') + .gl-new-card-count + = sprite_icon('token', css_class: 'gl-mr-2') + %span.js-token-count= @active_access_tokens.size + - if current_user.can?(:create_resource_access_tokens, @group) + .gl-new-card-actions + = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do + = _('Add new token') + - c.with_body do + - if current_user.can?(:create_resource_access_tokens, @group) + .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form + = render 'shared/access_tokens/form', + ajax: true, + type: type, + path: group_settings_access_tokens_path(@group), + resource: @group, + token: @resource_access_token, + scopes: @scopes, + access_levels: GroupMember.access_level_roles, + default_access_level: Gitlab::Access::GUEST, + prefix: :resource_access_token, + help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token') - #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true - } } + #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true } } diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 5020f6cbb22..461f0165501 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -16,13 +16,26 @@ #js-new-access-token-app{ data: { access_token_type: type } } - = render 'shared/access_tokens/form', - ajax: true, - type: type, - path: profile_personal_access_tokens_path, - token: @personal_access_token, - scopes: @scopes, - help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes') + = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c| + - c.with_header do + .gl-new-card-title-wrapper + %h3.gl-new-card-title + = _('Active personal access tokens') + .gl-new-card-count + = sprite_icon('token', css_class: 'gl-mr-2') + %span.js-token-count= @active_access_tokens.size + .gl-new-card-actions + = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do + = _('Add new token') + - c.with_body do + .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form + = render 'shared/access_tokens/form', + ajax: true, + type: type, + path: profile_personal_access_tokens_path, + token: @personal_access_token, + scopes: @scopes, + help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes') #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } } diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml index e4af6d59cad..53ae5ba26f8 100644 --- a/app/views/projects/settings/access_tokens/index.html.haml +++ b/app/views/projects/settings/access_tokens/index.html.haml @@ -4,9 +4,11 @@ - type_plural = _('project access tokens') - @force_desktop_expanded_sidebar = true -.gl-mt-5.js-search-settings-section - %h4.gl-my-0 - = page_title +.settings-section.js-search-settings-section + .settings-sticky-header + .settings-sticky-header-inner + %h4.gl-my-0 + = page_title %p.gl-text-secondary - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/project_access_tokens') } - if current_user.can?(:create_resource_access_tokens, @project) @@ -23,9 +25,21 @@ #js-new-access-token-app{ data: { access_token_type: type } } - - if current_user.can?(:create_resource_access_tokens, @project) - = render_if_exists 'projects/settings/access_tokens/form', - type: type + = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c| + - c.with_header do + .gl-new-card-title-wrapper + %h3.gl-new-card-title + = _('Active project access tokens') + .gl-new-card-count + = sprite_icon('token', css_class: 'gl-mr-2') + %span.js-token-count= @active_access_tokens.size + - if current_user.can?(:create_resource_access_tokens, @project) + .gl-new-card-actions + = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do + = _('Add new token') + - c.with_body do + - if current_user.can?(:create_resource_access_tokens, @project) + .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form + = render_if_exists 'projects/settings/access_tokens/form', type: type - #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true - } } + #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true } } diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml index a8f781d2272..d397fdf8fc3 100644 --- a/app/views/shared/access_tokens/_form.html.haml +++ b/app/views/shared/access_tokens/_form.html.haml @@ -7,7 +7,7 @@ - access_levels = local_assigns.fetch(:access_levels, false) - default_access_level = local_assigns.fetch(:default_access_level, false) -%h5.gl-font-lg.gl-mt-0 +%h4.gl-mt-0 = title = gitlab_ui_form_for token, as: prefix, url: path, method: :post, html: { id: 'js-new-access-token-form', class: 'js-requires-input' }, remote: ajax do |f| @@ -43,3 +43,5 @@ .gl-mt-3 = f.submit s_('AccessTokens|Create %{type}') % { type: type }, data: { qa_selector: 'create_token_button' }, pajamas_button: true + = render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do + = _('Cancel') diff --git a/config/routes/project.rb b/config/routes/project.rb index 0665345a24e..d0d7442857b 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -683,7 +683,7 @@ scope path: '(/-/jira)', constraints: ::Constraints::JiraEncodedUrlConstrainer.n ) } - get 'commit/:id', constraints: { id: /\h{7,40}/ }, to: redirect { |params, req| + get 'commit/:id', constraints: { id: Gitlab::Git::Commit::SHA_PATTERN }, to: redirect { |params, req| project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path( namespace: params[:namespace_id], project: params[:project_id] diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 08aa113685a..fc66cb4d6d3 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -92,7 +92,7 @@ scope format: false do end end -resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do +resources :commit, only: [:show], constraints: { id: Gitlab::Git::Commit::SHA_PATTERN } do member do get :branches get :pipelines diff --git a/db/docs/batched_background_migrations/backfill_default_branch_protection_namespace_setting.yml b/db/docs/batched_background_migrations/backfill_default_branch_protection_namespace_setting.yml new file mode 100644 index 00000000000..7a9e1cb8f5b --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_default_branch_protection_namespace_setting.yml @@ -0,0 +1,6 @@ +--- +migration_job_name: BackfillDefaultBranchProtectionNamespaceSetting +description: This migration back fills column default_branch_protection_defaults of namespace settings table +feature_category: database +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127335/ +milestone: 16.3 diff --git a/db/migrate/20230707031923_add_emails_to_x509_certificates.rb b/db/migrate/20230707031923_add_emails_to_x509_certificates.rb new file mode 100644 index 00000000000..44ea425fe72 --- /dev/null +++ b/db/migrate/20230707031923_add_emails_to_x509_certificates.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddEmailsToX509Certificates < Gitlab::Database::Migration[2.1] + def change + add_column :x509_certificates, :emails, :string, array: true, default: [], null: false + end +end diff --git a/db/post_migrate/20230724071541_queue_backfill_default_branch_protection_namespace_setting.rb b/db/post_migrate/20230724071541_queue_backfill_default_branch_protection_namespace_setting.rb new file mode 100644 index 00000000000..57debd9d0a2 --- /dev/null +++ b/db/post_migrate/20230724071541_queue_backfill_default_branch_protection_namespace_setting.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class QueueBackfillDefaultBranchProtectionNamespaceSetting < Gitlab::Database::Migration[2.1] + MIGRATION = "BackfillDefaultBranchProtectionNamespaceSetting" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 10_000 + SUB_BATCH_SIZE = 100 + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + queue_batched_background_migration( + MIGRATION, + :namespace_settings, + :namespace_id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :namespace_settings, :namespace_id, []) + end +end diff --git a/db/post_migrate/20230724085146_replace_old_fk_p_ci_builds_metadata_to_builds_v3.rb b/db/post_migrate/20230724085146_replace_old_fk_p_ci_builds_metadata_to_builds_v3.rb new file mode 100644 index 00000000000..acfed3032eb --- /dev/null +++ b/db/post_migrate/20230724085146_replace_old_fk_p_ci_builds_metadata_to_builds_v3.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class ReplaceOldFkPCiBuildsMetadataToBuildsV3 < Gitlab::Database::Migration[2.1] + include Gitlab::Database::PartitioningMigrationHelpers + + disable_ddl_transaction! + + def up + return if new_foreign_key_exists? + + with_lock_retries do + remove_foreign_key_if_exists :p_ci_builds_metadata, :ci_builds, + name: :fk_e20479742e_p, reverse_lock_order: true + + rename_constraint :p_ci_builds_metadata, :temp_fk_e20479742e_p, :fk_e20479742e_p + + Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_builds_metadata) do |partition| + rename_constraint partition.identifier, :temp_fk_e20479742e_p, :fk_e20479742e_p + end + end + end + + def down + return unless new_foreign_key_exists? + + add_concurrent_partitioned_foreign_key :p_ci_builds_metadata, :ci_builds, + name: :temp_fk_e20479742e_p, + column: [:partition_id, :build_id], + target_column: [:partition_id, :id], + on_update: :cascade, + on_delete: :cascade, + validate: true, + reverse_lock_order: true + + switch_constraint_names :p_ci_builds_metadata, :fk_e20479742e_p, :temp_fk_e20479742e_p + + Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_builds_metadata) do |partition| + switch_constraint_names partition.identifier, :fk_e20479742e_p, :temp_fk_e20479742e_p + end + end + + private + + def new_foreign_key_exists? + foreign_key_exists?(:p_ci_builds_metadata, :p_ci_builds, name: :fk_e20479742e_p) + end +end diff --git a/db/post_migrate/20230724085149_replace_old_fk_p_ci_runner_machine_builds_to_builds_v3.rb b/db/post_migrate/20230724085149_replace_old_fk_p_ci_runner_machine_builds_to_builds_v3.rb new file mode 100644 index 00000000000..d3919148240 --- /dev/null +++ b/db/post_migrate/20230724085149_replace_old_fk_p_ci_runner_machine_builds_to_builds_v3.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class ReplaceOldFkPCiRunnerMachineBuildsToBuildsV3 < Gitlab::Database::Migration[2.1] + include Gitlab::Database::PartitioningMigrationHelpers + + disable_ddl_transaction! + + def up + return if new_foreign_key_exists? + + with_lock_retries do + remove_foreign_key_if_exists :p_ci_runner_machine_builds, :ci_builds, + name: :fk_bb490f12fe_p, reverse_lock_order: true + + rename_constraint :p_ci_runner_machine_builds, :temp_fk_bb490f12fe_p, :fk_bb490f12fe_p + + Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_runner_machine_builds) do |partition| + rename_constraint partition.identifier, :temp_fk_bb490f12fe_p, :fk_bb490f12fe_p + end + end + end + + def down + return unless new_foreign_key_exists? + + add_concurrent_partitioned_foreign_key :p_ci_runner_machine_builds, :ci_builds, + name: :temp_fk_bb490f12fe_p, + column: [:partition_id, :build_id], + target_column: [:partition_id, :id], + on_update: :cascade, + on_delete: :cascade, + validate: true, + reverse_lock_order: true + + switch_constraint_names :p_ci_runner_machine_builds, :fk_bb490f12fe_p, :temp_fk_bb490f12fe_p + + Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_runner_machine_builds) do |partition| + switch_constraint_names partition.identifier, :fk_bb490f12fe_p, :temp_fk_bb490f12fe_p + end + end + + private + + def new_foreign_key_exists? + foreign_key_exists?(:p_ci_runner_machine_builds, :p_ci_builds, name: :fk_bb490f12fe_p) + end +end diff --git a/db/schema_migrations/20230707031923 b/db/schema_migrations/20230707031923 new file mode 100644 index 00000000000..af20d211b43 --- /dev/null +++ b/db/schema_migrations/20230707031923 @@ -0,0 +1 @@ +0a9e2fd92a64fbd4f2dc0d9fc866b3c5bb582c10503b0a85f1164db2e42c7cd0
\ No newline at end of file diff --git a/db/schema_migrations/20230724071541 b/db/schema_migrations/20230724071541 new file mode 100644 index 00000000000..9760046215f --- /dev/null +++ b/db/schema_migrations/20230724071541 @@ -0,0 +1 @@ +3ace146e873db96f6dcb556e701339081620aa268f013979c9062b50957d9e13
\ No newline at end of file diff --git a/db/schema_migrations/20230724085146 b/db/schema_migrations/20230724085146 new file mode 100644 index 00000000000..47b5817d794 --- /dev/null +++ b/db/schema_migrations/20230724085146 @@ -0,0 +1 @@ +3523475202ec758dc33748af5c2e85392d1ab55af4a5c7f7b76412b6723d1bf6
\ No newline at end of file diff --git a/db/schema_migrations/20230724085149 b/db/schema_migrations/20230724085149 new file mode 100644 index 00000000000..e5e1a5f2461 --- /dev/null +++ b/db/schema_migrations/20230724085149 @@ -0,0 +1 @@ +e4236ae465987e86f4a68ee552856afe054dccf5b13c5d71abb72cad186266d1
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0b8bcde15f8..80261e7b687 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -24966,7 +24966,8 @@ CREATE TABLE x509_certificates ( email character varying(255) NOT NULL, serial_number bytea NOT NULL, certificate_status smallint DEFAULT 0 NOT NULL, - x509_issuer_id bigint NOT NULL + x509_issuer_id bigint NOT NULL, + emails character varying[] DEFAULT '{}'::character varying[] NOT NULL ); CREATE SEQUENCE x509_certificates_id_seq diff --git a/doc/administration/audit_event_streaming/index.md b/doc/administration/audit_event_streaming/index.md index a8e1f0b015e..9cc70f4b701 100644 --- a/doc/administration/audit_event_streaming/index.md +++ b/doc/administration/audit_event_streaming/index.md @@ -385,9 +385,29 @@ To list streaming destinations for an instance and see the verification tokens: 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). 1. Select **Admin Area**. 1. On the left sidebar, select **Monitoring > Audit Events**. -1. On the main area, select the **Streams**. +1. On the main area, select the **Streams** tab. 1. View the verification token on the right side of each item. +### Update event filters + +> Event type filtering in the UI with a defined list of audit event types [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415013) in GitLab 16.3. + +When this feature is enabled, you can permit users to filter streamed audit events per destination. +If the feature is enabled with no filters, the destination receives all audit events. + +A streaming destination that has an event type filter set has a **filtered** (**{filter}**) label. + +To update a streaming destination's event filters: + +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. On the left sidebar, select **Monitoring > Audit Events**. +1. On the main area, select the **Streams** tab. +1. Select the stream to expand. +1. Locate the **Filter by audit event type** dropdown list. +1. Select the dropdown list and select or clear the required event types. +1. Select **Save** to update the event filters. + ### Override default content type header By default, streaming destinations use a `content-type` header of `application/x-www-form-urlencoded`. However, you diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index fdafe9200bf..4ba3a57723e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -8751,6 +8751,7 @@ The connection type for [`CustomEmoji`](#customemoji). | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="customemojiconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. | | <a id="customemojiconnectionedges"></a>`edges` | [`[CustomEmojiEdge]`](#customemojiedge) | A list of edges. | | <a id="customemojiconnectionnodes"></a>`nodes` | [`[CustomEmoji]`](#customemoji) | A list of nodes. | | <a id="customemojiconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | @@ -14336,10 +14337,22 @@ A custom emoji uploaded by user. | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="customemojicreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the custom emoji was created. | | <a id="customemojiexternal"></a>`external` | [`Boolean!`](#boolean) | Whether the emoji is an external link. | | <a id="customemojiid"></a>`id` | [`CustomEmojiID!`](#customemojiid) | ID of the emoji. | | <a id="customemojiname"></a>`name` | [`String!`](#string) | Name of the emoji. | | <a id="customemojiurl"></a>`url` | [`String!`](#string) | Link to file of the emoji. | +| <a id="customemojiuserpermissions"></a>`userPermissions` | [`CustomEmojiPermissions!`](#customemojipermissions) | Permissions for the current user on the resource. | + +### `CustomEmojiPermissions` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="customemojipermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `create_custom_emoji` on this resource. | +| <a id="customemojipermissionsdeletecustomemoji"></a>`deleteCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `delete_custom_emoji` on this resource. | +| <a id="customemojipermissionsreadcustomemoji"></a>`readCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `read_custom_emoji` on this resource. | ### `CustomerRelationsContact` @@ -17314,6 +17327,7 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction). | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="grouppermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `create_custom_emoji` on this resource. | | <a id="grouppermissionscreateprojects"></a>`createProjects` | [`Boolean!`](#boolean) | Indicates the user can perform `create_projects` on this resource. | | <a id="grouppermissionsreadgroup"></a>`readGroup` | [`Boolean!`](#boolean) | Indicates the user can perform `read_group` on this resource. | diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md index a7923cb84a0..b3e5be6c71c 100644 --- a/doc/ci/interactive_web_terminal/index.md +++ b/doc/ci/interactive_web_terminal/index.md @@ -61,7 +61,7 @@ improving this behavior. Sometimes, when a job is running, things don't go as you would expect, and it would be helpful if one can have a shell to aid debugging. When a job is running, on the right panel you can see a button `debug` that opens the terminal -for the current job. +for the current job. Only the person who started a job can debug it. ![Example of job running with terminal available](img/interactive_web_terminal_running_job.png) diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md index 4f3df3ecff8..c13e586a207 100644 --- a/doc/install/openshift_and_gitlab/index.md +++ b/doc/install/openshift_and_gitlab/index.md @@ -18,34 +18,29 @@ Running GitLab within an OpenShift cluster is officially supported using the Git [setting up GitLab on OpenShift on the GitLab Operator's documentation](https://docs.gitlab.com/charts/installation/operator.html). Some components (documented on the GitLab Operator doc) are not supported yet. -## Deploy to and integrate with OpenShift from GitLab - -Deploying custom or COTS applications on top of OpenShift from GitLab is supported using [the GitLab agent](../../user/clusters/agent/index.md). - ## Use OpenShift to run a GitLab Runner Fleet The GitLab Operator does not include the GitLab Runner. To install and manage a GitLab Runner fleet in an OpenShift cluster, use the [GitLab Runner Operator](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator). -## Unsupported GitLab features +### Deploy to and integrate with OpenShift from GitLab -### Secure +Deploying custom or COTS applications on top of OpenShift from GitLab is supported using [the GitLab agent](../../user/clusters/agent/index.md). -- [License Compliance via the `License-Scanning.gitlab-ci.yml` CI/CD template](../../user/compliance/license_compliance/index.md). [License scanning of CycloneDX files](../../user/compliance/license_scanning_of_cyclonedx_files/index.md) is supported on OpenShift. -- [Code Quality scanning](../../ci/testing/code_quality.md) -- [Operational Container Scanning](../../user/clusters/agent/vulnerabilities.md) (Note: Pipeline [Container Scanning](../../user/application_security/container_scanning/index.md) is supported) +### Unsupported GitLab features -### Docker-in-Docker +#### Docker-in-Docker When using OpenShift to run a GitLab Runner Fleet, we do not support some GitLab features given OpenShift's security model. Features requiring Docker-in-Docker might not work. For Auto DevOps, the following features are not supported yet: -- Auto Code Quality -- Auto License Compliance +- [Auto Code Quality](../../ci/testing/code_quality.md) +- [Auto License Compliance](../../user/compliance/license_compliance/index.md) ([License scanning of CycloneDX files](../../user/compliance/license_scanning_of_cyclonedx_files/index.md) is supported on OpenShift) - Auto Browser Performance Testing - Auto Build +- [Operational Container Scanning](../../user/clusters/agent/vulnerabilities.md) (Note: Pipeline [Container Scanning](../../user/application_security/container_scanning/index.md) is supported) For Auto Build, there's a [possible workaround using `kaniko`](../../ci/docker/using_kaniko.md). You can check the progress of the implementation in this [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/332560). diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index 10b1f3ff082..a3025144242 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -20,8 +20,6 @@ If you use Jira Data Center or Jira Server, use the [Jira DVCS connector](dvcs/i ## Install the GitLab for Jira Cloud app **(FREE SAAS)** -> **Add namespace** [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/331432) to **Link groups** in GitLab 16.1. - Prerequisites: - You must have [site administrator](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Make-someone-a-site-admin) access to the Jira instance. @@ -42,13 +40,14 @@ For an overview, see ## Configure the GitLab for Jira Cloud app **(FREE SAAS)** +> **Add namespace** [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/331432) to **Link groups** in GitLab 16.1. + Prerequisites: - You must have at least the Maintainer role for the GitLab group. - You must have [site administrator](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Make-someone-a-site-admin) access to the Jira instance. You can sync data from GitLab to Jira by linking the GitLab for Jira Cloud app to one or more GitLab groups. - To configure the GitLab for Jira Cloud app: 1. In Jira, on the top bar, select **Apps > Manage your apps**. diff --git a/doc/tutorials/infrastructure.md b/doc/tutorials/infrastructure.md index 688a1a1f7c7..a10e0c68c37 100644 --- a/doc/tutorials/infrastructure.md +++ b/doc/tutorials/infrastructure.md @@ -13,3 +13,5 @@ configure the infrastructure for your application. |-------|-------------|--------------------| | [Use GitOps with GitLab](https://about.gitlab.com/blog/2022/04/07/the-ultimate-guide-to-gitops-with-gitlab/) | Learn how to provision and manage a base infrastructure, connect to a Kubernetes cluster, deploy third-party applications, and carry out other infrastructure automation tasks. | | | [Set up Flux for GitOps](../user/clusters/agent/gitops/flux_tutorial.md) | Learn how to set up Flux for GitOps in a sample project. | | +| [Structure your repository for GitOps](../user/clusters/agent/gitops/example_repository_structure.md) | Learn how to organize a GitLab project for GitOps. | | +| [Deploy an OCI artifact using Flux](../user/clusters/agent/gitops/flux_oci_tutorial.md) | Learn how to package and deploy your Kubernetes manifests as an OCI artifact. | | diff --git a/doc/user/ai_features.md b/doc/user/ai_features.md index 0ae10b897ec..3dea892509e 100644 --- a/doc/user/ai_features.md +++ b/doc/user/ai_features.md @@ -230,6 +230,25 @@ Provide feedback on this experimental feature in [issue 407779](https://gitlab.c **Data usage**: When you use this feature, the text of public comments on the issue are sent to the large language model referenced above. +### Show deployment frequency forecast **(ULTIMATE SAAS)** + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10228) in GitLab 16.2 as an [Experiment](../policy/experiment-beta-support.md#experiment). + +This feature is an [Experiment](../policy/experiment-beta-support.md) on GitLab.com. + +In CI/CD Analytics, you can view a forecast of deployment frequency: + +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. +1. Select **Analyze > CI/CD analytics**. +1. Select the **Deployment frequency** tab. +1. Turn on the **Show forecast** toggle. +1. On the confirmation dialog, select **Accept testing terms**. + +The forecast is displayed as a dotted line on the chart. Data is forecasted for a duration that is half of the selected date range. +For example, if you select a 30-day range, a forecast for the following 15 days is displayed. + +Provide feedback on this experimental feature in [issue 416833](https://gitlab.com/gitlab-org/gitlab/-/issues/416833). + ## Data Usage GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function. diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index 8d66f3e48dd..2873711a2f4 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -611,6 +611,7 @@ Payload example: } }, "work_in_progress": false, + "draft": false, "assignee": { "name": "User1", "username": "user1", @@ -898,6 +899,7 @@ Payload example: "state": "opened", "blocking_discussions_resolved": true, "work_in_progress": false, + "draft": false, "first_contribution": true, "merge_status": "unchecked", "target_project_id": 14, @@ -983,6 +985,10 @@ Payload example: "previous": null, "current": 1 }, + "draft": { + "previous": true, + "current": false + } "updated_at": { "previous": "2017-09-15 16:50:55 UTC", "current":"2017-09-15 16:52:00 UTC" diff --git a/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting.rb b/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting.rb new file mode 100644 index 00000000000..8da29a61d61 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class is used to update the default_branch_protection_defaults column + # for user namespaces of the namespace_settings table. + class BackfillDefaultBranchProtectionNamespaceSetting < BatchedMigrationJob + operation_name :set_default_branch_protection_defaults + feature_category :database + + # Migration only version of `namespaces` table + class Namespace < ::ApplicationRecord + self.table_name = 'namespaces' + self.inheritance_column = :_type_disabled + + has_one :namespace_setting, + class_name: '::Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting::NamespaceSetting' + end + + # Migration only version of `namespace_settings` table + class NamespaceSetting < ::ApplicationRecord + self.table_name = 'namespace_settings' + belongs_to :namespace, + class_name: '::Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting::Namespace' + end + + # Migration only version of Gitlab::Access:BranchProtection application code. + class BranchProtection + attr_reader :level + + def initialize(level) + @level = level + end + + PROTECTION_NONE = 0 + PROTECTION_DEV_CAN_PUSH = 1 + PROTECTION_FULL = 2 + PROTECTION_DEV_CAN_MERGE = 3 + PROTECTION_DEV_CAN_INITIAL_PUSH = 4 + + DEVELOPER = 30 + MAINTAINER = 40 + + def to_hash + case level + when PROTECTION_NONE + self.class.protection_none + when PROTECTION_DEV_CAN_PUSH + self.class.protection_partial + when PROTECTION_FULL + self.class.protected_fully + when PROTECTION_DEV_CAN_MERGE + self.class.protected_against_developer_pushes + when PROTECTION_DEV_CAN_INITIAL_PUSH + self.class.protected_after_initial_push + end + end + + class << self + def protection_none + { + allowed_to_push: [{ 'access_level' => DEVELOPER }], + allowed_to_merge: [{ 'access_level' => DEVELOPER }], + allow_force_push: true + } + end + + def protection_partial + protection_none.merge(allow_force_push: false) + end + + def protected_fully + { + allowed_to_push: [{ 'access_level' => MAINTAINER }], + allowed_to_merge: [{ 'access_level' => MAINTAINER }], + allow_force_push: false + } + end + + def protected_against_developer_pushes + { + allowed_to_push: [{ 'access_level' => MAINTAINER }], + allowed_to_merge: [{ 'access_level' => DEVELOPER }], + allow_force_push: true + } + end + + def protected_after_initial_push + { + allowed_to_push: [{ 'access_level' => MAINTAINER }], + allowed_to_merge: [{ 'access_level' => DEVELOPER }], + allow_force_push: true, + developer_can_initial_push: true + } + end + end + end + + def perform + each_sub_batch do |sub_batch| + update_default_protection_branch_defaults(sub_batch) + end + end + + private + + def update_default_protection_branch_defaults(batch) + namespace_settings = NamespaceSetting.where(namespace_id: batch.pluck(:namespace_id)).includes(:namespace) + + values_list = namespace_settings.map do |namespace_setting| + level = namespace_setting.namespace.default_branch_protection.to_i + value = BranchProtection.new(level).to_hash.to_json + "(#{namespace_setting.namespace_id}, '#{value}'::jsonb)" + end.join(", ") + + sql = <<~SQL + WITH new_values (namespace_id, default_branch_protection_defaults) AS ( + VALUES + #{values_list} + ) + UPDATE namespace_settings + SET default_branch_protection_defaults = new_values.default_branch_protection_defaults + FROM new_values + WHERE namespace_settings.namespace_id = new_values.namespace_id; + SQL + + connection.execute(sql) + end + end + end +end diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb index 8be1e1716ec..ba7662cd13d 100644 --- a/lib/gitlab/checks/branch_check.rb +++ b/lib/gitlab/checks/branch_check.rb @@ -43,7 +43,7 @@ module Gitlab def prohibited_branch_checks return if deletion? - if branch_name =~ %r{\A\h{40}(/|\z)} + if branch_name =~ %r{\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}(/|\z)}o raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name] end diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb index 669c447c09b..8d1fcf4f916 100644 --- a/lib/gitlab/content_security_policy/config_loader.rb +++ b/lib/gitlab/content_security_policy/config_loader.rb @@ -3,66 +3,30 @@ module Gitlab module ContentSecurityPolicy class ConfigLoader - DIRECTIVES = %w(base_uri child_src connect_src default_src font_src - form_action frame_ancestors frame_src img_src manifest_src - media_src object_src report_uri script_src style_src worker_src).freeze - + DIRECTIVES = %w[ + base_uri child_src connect_src default_src font_src form_action + frame_ancestors frame_src img_src manifest_src media_src object_src + report_uri script_src style_src worker_src + ].freeze DEFAULT_FALLBACK_VALUE = '<default_value>' + HTTP_PORTS = [80, 443].freeze def self.default_enabled Rails.env.development? || Rails.env.test? end def self.default_directives - directives = { - 'default_src' => "'self'", - 'base_uri' => "'self'", - 'connect_src' => ContentSecurityPolicy::Directives.connect_src, - 'font_src' => "'self'", - 'form_action' => "'self' https: http:", - 'frame_ancestors' => "'self'", - 'frame_src' => ContentSecurityPolicy::Directives.frame_src, - 'img_src' => "'self' data: blob: http: https:", - 'manifest_src' => "'self'", - 'media_src' => "'self' data: blob: http: https:", - 'script_src' => ContentSecurityPolicy::Directives.script_src, - 'style_src' => ContentSecurityPolicy::Directives.style_src, - 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:", - 'object_src' => "'none'", - 'report_uri' => nil - } - - # connect_src with 'self' includes https/wss variations of the origin, - # however, safari hasn't covered this yet and we need to explicitly add - # support for websocket origins until Safari catches up with the specs - if Rails.env.development? - allow_webpack_dev_server(directives) - allow_letter_opener(directives) - allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled? - end + directives = default_directives_defaults + allow_development_tooling(directives) allow_websocket_connections(directives) - allow_cdn(directives, Settings.gitlab.cdn_host) if Settings.gitlab.cdn_host.present? - allow_zuora(directives) if Gitlab.com? - # Support for Sentry setup via configuration files will be removed in 16.0 - # in favor of Gitlab::CurrentSettings. - allow_legacy_sentry(directives) if Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn - allow_sentry(directives) if Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn) + allow_cdn(directives) + allow_zuora(directives) + allow_sentry(directives) allow_framed_gitlab_paths(directives) - allow_customersdot(directives) if ENV['CUSTOMER_PORTAL_URL'].present? - allow_review_apps(directives) if ENV['REVIEW_APPS_ENABLED'] - - # The follow section contains workarounds to patch Safari's lack of support for CSP Level 3 - # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579 - # frame-src was deprecated in CSP level 2 in favor of child-src - # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing - # However Safari seems to read child-src first so we'll just keep both equal - append_to_directive(directives, 'child_src', directives['frame_src']) - - # Safari also doesn't support worker-src and only checks child-src - # So for compatibility until it catches up to other browsers we need to - # append worker-src's content to child-src - append_to_directive(directives, 'child_src', directives['worker_src']) + allow_customersdot(directives) + allow_review_apps(directives) + csp_level_3_backport(directives) directives end @@ -87,41 +51,72 @@ module Gitlab private - def arguments_for(directive) - # In order to disable a directive, the user can explicitly - # set a falsy value like nil, false or empty string - arguments = @merged_csp_directives[directive] - return unless arguments.present? && arguments.is_a?(String) + def self.default_directives_defaults + { + 'default_src' => "'self'", + 'base_uri' => "'self'", + 'connect_src' => ContentSecurityPolicy::Directives.connect_src, + 'font_src' => "'self'", + 'form_action' => "'self' https: http:", + 'frame_ancestors' => "'self'", + 'frame_src' => ContentSecurityPolicy::Directives.frame_src, + 'img_src' => "'self' data: blob: http: https:", + 'manifest_src' => "'self'", + 'media_src' => "'self' data: blob: http: https:", + 'script_src' => ContentSecurityPolicy::Directives.script_src, + 'style_src' => ContentSecurityPolicy::Directives.style_src, + 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:", + 'object_src' => "'none'", + 'report_uri' => nil + } + end + + # connect_src with 'self' includes https/wss variations of the origin, + # however, safari hasn't covered this yet and we need to explicitly add + # support for websocket origins until Safari catches up with the specs + def self.allow_development_tooling(directives) + return unless Rails.env.development? - arguments.strip.split(' ').map(&:strip) + allow_webpack_dev_server(directives) + allow_letter_opener(directives) + allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled? + end + + def self.allow_webpack_dev_server(directives) + secure = Settings.webpack.dev_server['https'] + host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}" + http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}" + ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}" + + append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}") + end + + def self.allow_letter_opener(directives) + url = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/') + append_to_directive(directives, 'frame_src', url) + end + + def self.allow_snowplow_micro(directives) + url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s + append_to_directive(directives, 'connect_src', url) end def self.allow_websocket_connections(directives) - http_ports = [80, 443] host = Gitlab.config.gitlab.host port = Gitlab.config.gitlab.port secure = Gitlab.config.gitlab.https protocol = secure ? 'wss' : 'ws' ws_url = "#{protocol}://#{host}" - - unless http_ports.include?(port) - ws_url = "#{ws_url}:#{port}" - end + ws_url = "#{ws_url}:#{port}" unless HTTP_PORTS.include?(port) append_to_directive(directives, 'connect_src', ws_url) end - def self.allow_webpack_dev_server(directives) - secure = Settings.webpack.dev_server['https'] - host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}" - http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}" - ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}" - - append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}") - end + def self.allow_cdn(directives) + cdn_host = Settings.gitlab.cdn_host.presence + return unless cdn_host - def self.allow_cdn(directives, cdn_host) append_to_directive(directives, 'script_src', cdn_host) append_to_directive(directives, 'style_src', cdn_host) append_to_directive(directives, 'font_src', cdn_host) @@ -129,47 +124,35 @@ module Gitlab append_to_directive(directives, 'frame_src', cdn_host) end - def self.zuora_host - "https://*.zuora.com/apps/PublicHostedPageLite.do" - end - def self.allow_zuora(directives) + return unless Gitlab.com? + append_to_directive(directives, 'frame_src', zuora_host) end - def self.append_to_directive(directives, directive, text) - directives[directive] = "#{directives[directive]} #{text}".strip - end + def self.allow_sentry(directives) + allow_legacy_sentry(directives) if legacy_sentry_configured? + return unless sentry_client_side_dsn_enabled? - def self.allow_customersdot(directives) - customersdot_host = ENV['CUSTOMER_PORTAL_URL'] + sentry_uri = URI(Gitlab::CurrentSettings.sentry_clientside_dsn) - append_to_directive(directives, 'frame_src', customersdot_host) + append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}") end def self.allow_legacy_sentry(directives) # Support for Sentry setup via configuration files will be removed in 16.0 # in favor of Gitlab::CurrentSettings. - sentry_dsn = Gitlab.config.sentry.clientside_dsn - sentry_uri = URI(sentry_dsn) + sentry_uri = URI(Gitlab.config.sentry.clientside_dsn) append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}") end - def self.allow_sentry(directives) - sentry_dsn = Gitlab::CurrentSettings.sentry_clientside_dsn - sentry_uri = URI(sentry_dsn) - - append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}") - end - - def self.allow_letter_opener(directives) - append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/')) + def self.legacy_sentry_configured? + Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn end - def self.allow_snowplow_micro(directives) - url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s - append_to_directive(directives, 'connect_src', url) + def self.sentry_client_side_dsn_enabled? + Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn) end # Using 'self' in the CSP introduces several CSP bypass opportunities @@ -180,10 +163,50 @@ module Gitlab end end + def self.allow_customersdot(directives) + customersdot_host = ENV['CUSTOMER_PORTAL_URL'].presence + return unless customersdot_host + + append_to_directive(directives, 'frame_src', customersdot_host) + end + def self.allow_review_apps(directives) + return unless ENV['REVIEW_APPS_ENABLED'].presence + # Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/') end + + # The follow contains workarounds to patch Safari's lack of support for CSP Level 3 + def self.csp_level_3_backport(directives) + # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579 + # frame-src was deprecated in CSP level 2 in favor of child-src + # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing + # However Safari seems to read child-src first so we'll just keep both equal + append_to_directive(directives, 'child_src', directives['frame_src']) + + # Safari also doesn't support worker-src and only checks child-src + # So for compatibility until it catches up to other browsers we need to + # append worker-src's content to child-src + append_to_directive(directives, 'child_src', directives['worker_src']) + end + + def self.append_to_directive(directives, directive, text) + directives[directive] = "#{directives[directive]} #{text}".strip + end + + def self.zuora_host + "https://*.zuora.com/apps/PublicHostedPageLite.do" + end + + def arguments_for(directive) + # In order to disable a directive, the user can explicitly + # set a falsy value like nil, false or empty string + arguments = @merged_csp_directives[directive] + return unless arguments.is_a?(String) + + arguments.split(' ') + end end end end diff --git a/lib/gitlab/etag_caching/router/graphql.rb b/lib/gitlab/etag_caching/router/graphql.rb index 92b21a0859d..7946e768e00 100644 --- a/lib/gitlab/etag_caching/router/graphql.rb +++ b/lib/gitlab/etag_caching/router/graphql.rb @@ -14,7 +14,7 @@ module Gitlab 'continuous_integration' ], [ - %r(\Apipelines/sha/\w{7,40}\z), + %r(\Apipelines/sha/\w{#{Gitlab::Git::Commit::MIN_SHA_LENGTH},#{Gitlab::Git::Commit::MAX_SHA_LENGTH}}\z)o, 'ci_editor', 'pipeline_composition' ], diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index ef5c242e68a..4e574d6de74 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -8,7 +8,7 @@ module Gitlab # https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012 EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' BLANK_SHA = ('0' * 40).freeze - COMMIT_ID = /\A[0-9a-f]{40}\z/.freeze + COMMIT_ID = /\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}\z/.freeze TAG_REF_PREFIX = "refs/tags/" BRANCH_REF_PREFIX = "refs/heads/" diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index b104610038c..571dde6fcfc 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -12,7 +12,20 @@ module Gitlab attr_accessor :raw_commit, :head MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes + + SHA1_LENGTH = 40 + SHA256_LENGTH = 64 + MIN_SHA_LENGTH = 7 + MAX_SHA_LENGTH = SHA256_LENGTH + + RAW_SHA_PATTERN = "\\h{#{MIN_SHA_LENGTH},#{MAX_SHA_LENGTH}}".freeze + SHA_PATTERN = /#{RAW_SHA_PATTERN}/ + # Match a full SHA. Note that because this expression is not anchored it will match any SHA that is at + # least SHA1_LENGTH long. + RAW_FULL_SHA_PATTERN = "\\h{#{SHA1_LENGTH}}(?:\\h{#{SHA256_LENGTH - SHA1_LENGTH}})?".freeze + FULL_SHA_PATTERN = /#{RAW_FULL_SHA_PATTERN}/ + SERIALIZE_KEYS = [ :id, :message, :parent_ids, :authored_date, :author_name, :author_email, diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index a6ca8323a20..22af06ba09b 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -10,6 +10,7 @@ module Gitlab blocking_discussions_resolved created_at description + draft head_pipeline_id id iid @@ -55,6 +56,7 @@ module Gitlab target: merge_request.target_project.hook_attrs, last_commit: merge_request.diff_head_commit&.hook_attrs, work_in_progress: merge_request.draft?, + draft: merge_request.draft?, total_time_spent: merge_request.total_time_spent, time_change: merge_request.time_change, human_total_time_spent: merge_request.human_total_time_spent, diff --git a/lib/gitlab/x509/signature.rb b/lib/gitlab/x509/signature.rb index d101a6d2522..649e5379927 100644 --- a/lib/gitlab/x509/signature.rb +++ b/lib/gitlab/x509/signature.rb @@ -42,11 +42,13 @@ module Gitlab !verified_signature || signed_by_user.nil? - if signed_by_user.verified_emails.include?(@email.downcase) && certificate_email.casecmp?(@email) - :verified - else - :unverified + if signed_by_user.verified_emails.include?(@email.downcase) + return :verified if certificate_emails.find do |ce| + ce.casecmp?(@email) + end end + + :unverified end private @@ -173,18 +175,13 @@ module Gitlab end def certificate_email - email = nil + certificate_emails.first + end - get_certificate_extension('subjectAltName').split(',').each do |item| - if item.strip.start_with?("email") - email = item.split('email:')[1] - break - end + def certificate_emails + get_certificate_extension('subjectAltName').split(',').each.with_object([]) do |item, emails| + emails << item.split('email:')[1] if item.strip.start_with?("email") end - - return if email.nil? - - email end def x509_issuer @@ -206,6 +203,7 @@ module Gitlab subject_key_identifier: certificate_subject_key_identifier, subject: certificate_subject, email: certificate_email, + emails: certificate_emails, serial_number: cert.serial.to_i, x509_issuer_id: x509_issuer.id } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 772d563c6dc..834e89c6ee0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2638,15 +2638,21 @@ msgstr "" msgid "Active" msgstr "" -msgid "Active %{accessTokenTypePlural} (%{totalAccessTokens})" -msgstr "" - msgid "Active Sessions" msgstr "" msgid "Active chat names (%{count})" msgstr "" +msgid "Active group access tokens" +msgstr "" + +msgid "Active personal access tokens" +msgstr "" + +msgid "Active project access tokens" +msgstr "" + msgid "Activity" msgstr "" @@ -2866,6 +2872,9 @@ msgstr "" msgid "Add new key" msgstr "" +msgid "Add new token" +msgstr "" + msgid "Add new webhook" msgstr "" @@ -23585,6 +23594,9 @@ msgstr "" msgid "Impersonation has been disabled" msgstr "" +msgid "Impersonation tokens" +msgstr "" + msgid "Import" msgstr "" @@ -37793,6 +37805,9 @@ msgstr "" msgid "ProtectedEnvironment|Protected Environment (%{protected_environments_count})" msgstr "" +msgid "ProtectedEnvironment|Protected Environments" +msgstr "" + msgid "ProtectedEnvironment|Required approvals" msgstr "" @@ -37808,12 +37823,15 @@ msgstr "" msgid "ProtectedEnvironment|Select users" msgstr "" -msgid "ProtectedEnvironment|There are currently no protected environments. Protect an environment with this form." +msgid "ProtectedEnvironment|There are currently no protected environments." msgstr "" msgid "ProtectedEnvironment|Unprotect" msgstr "" +msgid "ProtectedEnvironment|Unprotect environment" +msgstr "" + msgid "ProtectedEnvironment|Users with at least the Developer role can write to unprotected environments. Are you sure you want to unprotect %{environment_name}?" msgstr "" diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb index 0350c8ab066..543dc2cc2a6 100644 --- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb +++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb @@ -19,6 +19,7 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js, feature_category: :s name = 'Hello World' visit admin_user_impersonation_tokens_path(user_id: user.username) + click_button 'Add new token' fill_in "Token name", with: name # Set date to 1st of next month diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 65fe1330be2..094855393be 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -18,6 +18,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :user_ name = 'My PAT' visit profile_personal_access_tokens_path + + click_button 'Add new token' fill_in "Token name", with: name # Set date to 1st of next month @@ -43,6 +45,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :user_ it "displays an error message" do number_tokens_before = PersonalAccessToken.count visit profile_personal_access_tokens_path + + click_button 'Add new token' fill_in "Token name", with: 'My PAT' click_on "Create personal access token" @@ -145,6 +149,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :user_ visit profile_personal_access_tokens_path({ name: name, scopes: scopes }) + click_button 'Add new token' expect(page).to have_field("Token name", with: name) expect(find("#personal_access_token_scopes_api")).to be_checked expect(find("#personal_access_token_scopes_read_user")).to be_checked diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb index 210815f341c..9025bd9052e 100644 --- a/spec/features/projects/settings/access_tokens_spec.rb +++ b/spec/features/projects/settings/access_tokens_spec.rb @@ -49,6 +49,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :use it 'shows Owner option' do visit resource_settings_access_tokens_path + click_button 'Add new token' expect(role_dropdown_options).to include('Owner') end end @@ -63,6 +64,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :use it 'does not show Owner option for a maintainer' do visit resource_settings_access_tokens_path + click_button 'Add new token' expect(role_dropdown_options).not_to include('Owner') end end @@ -81,6 +83,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :use it 'shows access token creation form and text' do visit project_settings_access_tokens_path(personal_project) + click_button 'Add new token' expect(page).to have_selector('#js-new-access-token-form') end end diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb index 978b678c334..1ca4b761788 100644 --- a/spec/features/projects/settings/user_searches_in_settings_spec.rb +++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb @@ -31,7 +31,7 @@ RSpec.describe 'User searches project settings', :js, feature_category: :groups_ visit project_settings_access_tokens_path(project) end - it_behaves_like 'can highlight results', 'Expiration date' + it_behaves_like 'can highlight results', 'Token name' end context 'in Repository page' do diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js index 2fa14810578..5236f38dc35 100644 --- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js +++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js @@ -91,24 +91,6 @@ describe('~/access_tokens/components/access_token_table_app', () => { expect(cells.at(0).text()).toBe(noTokensMessage); }); - it('should show a title indicating the amount of tokens', () => { - createComponent(); - - expect(wrapper.find('h5').text()).toBe( - sprintf(__('Active %{accessTokenTypePlural} (%{totalAccessTokens})'), { - accessTokenTypePlural, - totalAccessTokens: defaultActiveAccessTokens.length, - }), - ); - }); - - it('should render information section', () => { - const info = 'This is my information'; - createComponent({ information: info }); - - expect(wrapper.findByTestId('information-section').text()).toBe(info); - }); - describe('table headers', () => { it('should include `Action` column', () => { createComponent(); diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js index 70f77932ccf..d51ac638f0e 100644 --- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js +++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js @@ -23,6 +23,8 @@ describe('~/access_tokens/components/new_access_token_app', () => { }; const findButtonEl = () => document.querySelector('[type=submit]'); + const findGlAlertError = () => wrapper.findByTestId('error-message'); + const findGlAlertSuccess = () => wrapper.findByTestId('success-message'); const triggerSuccess = async (newToken = 'new token') => { wrapper @@ -57,7 +59,7 @@ describe('~/access_tokens/components/new_access_token_app', () => { it('should render nothing', () => { expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false); - expect(wrapper.findComponent(GlAlert).exists()).toBe(false); + expect(findGlAlertError().exists()).toBe(false); }); describe('on success', () => { @@ -65,7 +67,8 @@ describe('~/access_tokens/components/new_access_token_app', () => { const newToken = '12345'; await triggerSuccess(newToken); - expect(wrapper.findComponent(GlAlert).exists()).toBe(false); + expect(findGlAlertError().exists()).toBe(false); + expect(findGlAlertSuccess().exists()).toBe(true); const InputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility); expect(InputCopyToggleVisibilityComponent.props('value')).toBe(newToken); @@ -82,7 +85,7 @@ describe('~/access_tokens/components/new_access_token_app', () => { const newToken = '12345'; await triggerSuccess(newToken); - expect(wrapper.findComponent(GlAlert).exists()).toBe(false); + expect(findGlAlertError().exists()).toBe(false); const inputAttributes = wrapper .findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType })) @@ -135,7 +138,7 @@ describe('~/access_tokens/components/new_access_token_app', () => { expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false); - let GlAlertComponent = wrapper.findComponent(GlAlert); + let GlAlertComponent = findGlAlertError(); expect(GlAlertComponent.props('title')).toBe(__('The form contains the following errors:')); expect(GlAlertComponent.props('variant')).toBe('danger'); let itemEls = wrapper.findAll('li'); diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js index c1158e0d124..7d4d73b00b2 100644 --- a/spec/frontend/access_tokens/index_spec.js +++ b/spec/frontend/access_tokens/index_spec.js @@ -50,7 +50,6 @@ describe('access tokens', () => { initialActiveAccessTokens, // Default values - information: undefined, noActiveTokensMessage: sprintf(__('This user has no active %{accessTokenTypePlural}.'), { accessTokenTypePlural, }), @@ -59,14 +58,12 @@ describe('access tokens', () => { }); it('mounts the component and provides all values', () => { - const information = 'Additional information'; const noActiveTokensMessage = 'This group has no active access tokens.'; setHTMLFixture( `<div id="js-access-token-table-app" data-access-token-type="${accessTokenType}" data-access-token-type-plural="${accessTokenTypePlural}" data-initial-active-access-tokens=${JSON.stringify(initialActiveAccessTokens)} - data-information="${information}" data-no-active-tokens-message="${noActiveTokensMessage}" data-show-role > @@ -82,7 +79,6 @@ describe('access tokens', () => { accessTokenType, accessTokenTypePlural, initialActiveAccessTokens, - information, noActiveTokensMessage, showRole: true, }); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap index bb520cd7a47..9c5f9db9762 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap @@ -18,10 +18,12 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render <status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub> <div class=\\"gl-w-full gl-display-flex\\"> <div class=\\"gl-display-flex gl-flex-grow-1\\"> - <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\"> - <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p> - <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub> - <!----> + <div class=\\"gl-display-flex gl-flex-grow-1 gl-align-items-baseline\\"> + <div> + <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p> + <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub> + <!----> + </div> <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\"> Badge is optional. Text to be displayed inside badge </gl-badge-stub> @@ -44,10 +46,12 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render <!----> <div class=\\"gl-w-full gl-display-flex\\"> <div class=\\"gl-display-flex gl-flex-grow-1\\"> - <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\"> - <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p> - <!----> - <!----> + <div class=\\"gl-display-flex gl-flex-grow-1 gl-align-items-baseline\\"> + <div> + <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p> + <!----> + <!----> + </div> <!----> </div> <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub> diff --git a/spec/graphql/types/custom_emoji_type_spec.rb b/spec/graphql/types/custom_emoji_type_spec.rb index 7f3c99e4b63..17697321602 100644 --- a/spec/graphql/types/custom_emoji_type_spec.rb +++ b/spec/graphql/types/custom_emoji_type_spec.rb @@ -3,9 +3,20 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['CustomEmoji'] do + expected_fields = %w[ + id + name + url + external + created_at + user_permissions + ] + specify { expect(described_class.graphql_name).to eq('CustomEmoji') } specify { expect(described_class).to require_graphql_authorizations(:read_custom_emoji) } - specify { expect(described_class).to have_graphql_fields(:id, :name, :url, :external) } + specify { expect(described_class).to have_graphql_fields(*expected_fields) } + + specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::CustomEmoji) } end diff --git a/spec/lib/api/validations/validators/git_sha_spec.rb b/spec/lib/api/validations/validators/git_sha_spec.rb index ae6be52a4c7..2ae3fca7a6e 100644 --- a/spec/lib/api/validations/validators/git_sha_spec.rb +++ b/spec/lib/api/validations/validators/git_sha_spec.rb @@ -8,6 +8,7 @@ RSpec.describe API::Validations::Validators::GitSha do let(:sha) { RepoHelpers.sample_commit.id } let(:short_sha) { sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH] } let(:too_short_sha) { sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH - 1] } + let(:too_long_sha) { "a" * (Gitlab::Git::Commit::MAX_SHA_LENGTH + 1) } subject do described_class.new(['test'], {}, false, scope.new) @@ -29,7 +30,7 @@ RSpec.describe API::Validations::Validators::GitSha do context 'invalid sha' do it 'raises a validation error' do - expect_validation_error('test' => "#{sha}2") # Sha length > 40 + expect_validation_error('test' => too_long_sha) # too long SHA expect_validation_error('test' => 'somestring') expect_validation_error('test' => too_short_sha) # sha length < MIN_SHA_LENGTH (7) end diff --git a/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb b/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb new file mode 100644 index 00000000000..62c9e240b7a --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting, + schema: 20230724071541, + feature_category: :database do + let(:namespaces_table) { table(:namespaces) } + let(:namespace_settings_table) { table(:namespace_settings) } + + subject(:perform_migration) do + described_class.new( + start_id: 1, + end_id: 30, + batch_table: :namespace_settings, + batch_column: :namespace_id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection + ).perform + end + + before do + namespaces_table.create!(id: 1, name: 'group_namespace', path: 'path-1', type: 'Group', + default_branch_protection: 0) + namespaces_table.create!(id: 2, name: 'user_namespace', path: 'path-2', type: 'User', default_branch_protection: 1) + namespaces_table.create!(id: 3, name: 'user_three_namespace', path: 'path-3', type: 'User', + default_branch_protection: 2) + namespaces_table.create!(id: 4, name: 'group_four_namespace', path: 'path-4', type: 'Group', + default_branch_protection: 3) + namespaces_table.create!(id: 5, name: 'group_five_namespace', path: 'path-5', type: 'Group', + default_branch_protection: 4) + + namespace_settings_table.create!(namespace_id: 1, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 2, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 3, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 4, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 5, default_branch_protection_defaults: {}) + end + + it 'updates default_branch_protection_defaults to a correct value', :aggregate_failures do + expect(ActiveRecord::QueryRecorder.new { perform_migration }.count).to eq(16) + + expect(migrated_attribute(1)).to eq({ "allow_force_push" => true, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 30 }] }) + expect(migrated_attribute(2)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 30 }] }) + expect(migrated_attribute(3)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 40 }], + "allowed_to_push" => [{ "access_level" => 40 }] }) + expect(migrated_attribute(4)).to eq({ "allow_force_push" => true, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 40 }] }) + expect(migrated_attribute(5)).to eq({ "allow_force_push" => true, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 40 }], + "developer_can_initial_push" => true }) + end + + def migrated_attribute(namespace_id) + namespace_settings_table.find(namespace_id).default_branch_protection_defaults + end +end diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index 7ce267c535f..9f6d4c7fd36 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -32,6 +32,18 @@ RSpec.describe Gitlab::Checks::BranchCheck, feature_category: :source_code_manag expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") end + it "prohibits 64-character hexadecimal branch names" do + allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175") + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") + end + + it "prohibits 64-character hexadecimal branch names as the start of a path" do + allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175/test") + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") + end + it "doesn't prohibit a nested hexadecimal in a branch name" do allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-fix") diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb index b40829d72a0..44887a86aff 100644 --- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb +++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do +RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :shared do let(:policy) { ActionDispatch::ContentSecurityPolicy.new } let(:csp_config) do { @@ -29,7 +29,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do context 'when in production' do before do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + stub_rails_env('production') end it 'is disabled' do @@ -40,6 +40,16 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do describe '.default_directives' do let(:directives) { described_class.default_directives } + let(:child_src) { directives['child_src'] } + let(:connect_src) { directives['connect_src'] } + let(:font_src) { directives['font_src'] } + let(:frame_src) { directives['frame_src'] } + let(:img_src) { directives['img_src'] } + let(:media_src) { directives['media_src'] } + let(:report_uri) { directives['report_uri'] } + let(:script_src) { directives['script_src'] } + let(:style_src) { directives['style_src'] } + let(:worker_src) { directives['worker_src'] } it 'returns default directives' do directive_names = (described_class::DIRECTIVES - ['report_uri']) @@ -49,68 +59,167 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end expect(directives.has_key?('report_uri')).to be_truthy - expect(directives['report_uri']).to be_nil - expect(directives['child_src']).to eq("#{directives['frame_src']} #{directives['worker_src']}") + expect(report_uri).to be_nil + expect(child_src).to eq("#{frame_src} #{worker_src}") end describe 'the images-src directive' do it 'can be loaded from anywhere' do - expect(directives['img_src']).to include('http: https:') + expect(img_src).to include('http: https:') end end describe 'the media-src directive' do it 'can be loaded from anywhere' do - expect(directives['media_src']).to include('http: https:') + expect(media_src).to include('http: https:') end end - context 'adds all websocket origins to support Safari' do + describe 'Webpack dev server websocket connections' do + let(:webpack_dev_server_host) { 'webpack-dev-server.com' } + let(:webpack_dev_server_port) { '9999' } + let(:webpack_dev_server_https) { true } + + before do + stub_config_setting( + webpack: { dev_server: { + host: webpack_dev_server_host, + webpack_dev_server_port: webpack_dev_server_port, + https: webpack_dev_server_https + } } + ) + end + + context 'when in production' do + before do + stub_rails_env('production') + end + + context 'with secure domain' do + it 'does not include webpack dev server in connect-src' do + expect(connect_src).not_to include(webpack_dev_server_host) + expect(connect_src).not_to include(webpack_dev_server_port) + end + end + + context 'with insecure domain' do + let(:webpack_dev_server_https) { false } + + it 'does not include webpack dev server in connect-src' do + expect(connect_src).not_to include(webpack_dev_server_host) + expect(connect_src).not_to include(webpack_dev_server_port) + end + end + end + + context 'when in development' do + before do + stub_rails_env('development') + end + + context 'with secure domain' do + before do + stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: true) + end + + it 'includes secure websocket url for webpack dev server in connect-src' do + expect(connect_src).to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + expect(connect_src).not_to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + end + end + + context 'with insecure domain' do + before do + stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: false) + end + + it 'includes insecure websocket url for webpack dev server in connect-src' do + expect(connect_src).not_to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + expect(connect_src).to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + end + end + end + end + + describe 'Websocket connections' do it 'with insecure domain' do stub_config_setting(host: 'example.com', https: false) - expect(directives['connect_src']).to eq("'self' ws://example.com") + expect(connect_src).to eq("'self' ws://example.com") end it 'with secure domain' do stub_config_setting(host: 'example.com', https: true) - expect(directives['connect_src']).to eq("'self' wss://example.com") + expect(connect_src).to eq("'self' wss://example.com") end it 'with custom port' do stub_config_setting(host: 'example.com', port: '1234') - expect(directives['connect_src']).to eq("'self' ws://example.com:1234") + expect(connect_src).to eq("'self' ws://example.com:1234") end it 'with custom port and secure domain' do stub_config_setting(host: 'example.com', https: true, port: '1234') - expect(directives['connect_src']).to eq("'self' wss://example.com:1234") + expect(connect_src).to eq("'self' wss://example.com:1234") + end + + it 'when port is included in HTTP_PORTS' do + described_class::HTTP_PORTS.each do |port| + stub_config_setting(host: 'example.com', https: true, port: port) + expect(connect_src).to eq("'self' wss://example.com") + end end end - context 'when CDN host is defined' do + describe 'CDN connections' do before do - stub_config_setting(cdn_host: 'https://cdn.example.com') + allow(described_class).to receive(:allow_letter_opener) + allow(described_class).to receive(:allow_zuora) + allow(described_class).to receive(:allow_framed_gitlab_paths) + allow(described_class).to receive(:allow_customersdot) + allow(described_class).to receive(:csp_level_3_backport) + end + + context 'when CDN host is defined' do + let(:cdn_host) { 'https://cdn.example.com' } + + before do + stub_config_setting(cdn_host: cdn_host) + end + + it 'adds CDN host to CSP' do + expect(script_src).to include(cdn_host) + expect(style_src).to include(cdn_host) + expect(font_src).to include(cdn_host) + expect(worker_src).to include(cdn_host) + expect(frame_src).to include(cdn_host) + end end - it 'adds CDN host to CSP' do - expect(directives['script_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src + " https://cdn.example.com") - expect(directives['style_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src + " https://cdn.example.com") - expect(directives['font_src']).to eq("'self' https://cdn.example.com") - expect(directives['worker_src']).to eq('http://localhost/assets/ blob: data: https://cdn.example.com') - expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/") + context 'when CDN host is undefined' do + before do + stub_config_setting(cdn_host: nil) + end + + it 'does not include CDN host in CSP' do + expect(script_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src) + expect(style_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src) + expect(font_src).to eq("'self'") + expect(worker_src).to eq("http://localhost/assets/ blob: data:") + expect(frame_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src) + end end end describe 'Zuora directives' do context 'when on SaaS', :saas do it 'adds Zuora host to CSP' do - expect(directives['frame_src']).to include('https://*.zuora.com/apps/PublicHostedPageLite.do') + expect(frame_src).to include('https://*.zuora.com/apps/PublicHostedPageLite.do') end end context 'when is not Gitlab.com?' do it 'does not add Zuora host to CSP' do - expect(directives['frame_src']).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do') + expect(frame_src).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do') end end end @@ -131,7 +240,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds legacy sentry path to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com") + expect(connect_src).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com") end end @@ -143,7 +252,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds new sentry path to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com") + expect(connect_src).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com") end end @@ -159,11 +268,22 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'config is backwards compatible, does not add sentry path to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com") + expect(connect_src).to eq("'self' ws://gitlab.example.com") end end context 'when legacy sentry and sentry are both configured' do + let(:connect_src_expectation) do + # rubocop:disable Lint/PercentStringArray + %w[ + 'self' + ws://gitlab.example.com + dummy://legacy-sentry.example.com + dummy://sentry.example.com + ].join(' ') + # rubocop:enable Lint/PercentStringArray + end + before do allow(Gitlab.config.sentry).to receive(:enabled).and_return(true) allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(legacy_dsn) @@ -173,24 +293,57 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds both sentry paths to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com dummy://sentry.example.com") + expect(connect_src).to eq(connect_src_expectation) end end end - context 'when CUSTOMER_PORTAL_URL is set' do - let(:customer_portal_url) { 'https://customers.example.com' } + describe 'Customer portal frames' do + context 'when CUSTOMER_PORTAL_URL is set' do + let(:customer_portal_url) { 'https://customers.example.com' } + let(:frame_src_expectation) do + [ + ::Gitlab::ContentSecurityPolicy::Directives.frame_src, + 'http://localhost/admin/', + 'http://localhost/assets/', + 'http://localhost/-/speedscope/index.html', + 'http://localhost/-/sandbox/', + customer_portal_url + ].join(' ') + end - before do - stub_env('CUSTOMER_PORTAL_URL', customer_portal_url) + before do + stub_env('CUSTOMER_PORTAL_URL', customer_portal_url) + end + + it 'adds CUSTOMER_PORTAL_URL to CSP' do + expect(frame_src).to eq(frame_src_expectation) + end end - it 'adds CUSTOMER_PORTAL_URL to CSP' do - expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/ #{customer_portal_url}") + context 'when CUSTOMER_PORTAL_URL is blank' do + let(:customer_portal_url) { '' } + let(:frame_src_expectation) do + [ + ::Gitlab::ContentSecurityPolicy::Directives.frame_src, + 'http://localhost/admin/', + 'http://localhost/assets/', + 'http://localhost/-/speedscope/index.html', + 'http://localhost/-/sandbox/' + ].join(' ') + end + + before do + stub_env('CUSTOMER_PORTAL_URL', customer_portal_url) + end + + it 'adds CUSTOMER_PORTAL_URL to CSP' do + expect(frame_src).to eq(frame_src_expectation) + end end end - context 'letter_opener application URL' do + describe 'letter_opener application URL' do let(:gitlab_url) { 'http://gitlab.example.com' } let(:letter_opener_url) { "#{gitlab_url}/rails/letter_opener/" } @@ -200,21 +353,21 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do context 'when in production' do before do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + stub_rails_env('production') end it 'does not add letter_opener to CSP' do - expect(directives['frame_src']).not_to include(letter_opener_url) + expect(frame_src).not_to include(letter_opener_url) end end context 'when in development' do before do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) + stub_rails_env('development') end it 'adds letter_opener to CSP' do - expect(directives['frame_src']).to include(letter_opener_url) + expect(frame_src).to include(letter_opener_url) end end end @@ -234,7 +387,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'does not add Snowplow Micro URL to connect-src' do - expect(directives['connect_src']).not_to include(snowplow_micro_url) + expect(connect_src).not_to include(snowplow_micro_url) end end @@ -244,7 +397,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds Snowplow Micro URL with trailing slash to connect-src' do - expect(directives['connect_src']).to match(Regexp.new(snowplow_micro_url)) + expect(connect_src).to match(Regexp.new(snowplow_micro_url)) end context 'when not enabled using config' do @@ -253,7 +406,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'does not add Snowplow Micro URL to connect-src' do - expect(directives['connect_src']).not_to include(snowplow_micro_url) + expect(connect_src).not_to include(snowplow_micro_url) end end @@ -262,8 +415,18 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do stub_env('REVIEW_APPS_ENABLED', 'true') end - it 'adds gitlab-org/gitlab merge requests API endpoint to CSP' do - expect(directives['connect_src']).to include('https://gitlab.com/api/v4/projects/278964/merge_requests/') + it "includes review app's merge requests API endpoint in the CSP" do + expect(connect_src).to include('https://gitlab.com/api/v4/projects/278964/merge_requests/') + end + end + + context 'when REVIEW_APPS_ENABLED is blank' do + before do + stub_env('REVIEW_APPS_ENABLED', '') + end + + it "does not include review app's merge requests API endpoint in the CSP" do + expect(connect_src).not_to include('https://gitlab.com/api/v4/projects/278964/merge_requests/') end end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 3b1dbf7d602..5c4be1003c3 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -691,6 +691,100 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do end end + describe 'SHA patterns' do + shared_examples 'a SHA-matching pattern' do + let(:expected_match) { sha } + + shared_examples 'a match' do + it 'matches the pattern' do + expect(value).to match(pattern) + expect(pattern.match(value).to_a).to eq([expected_match]) + end + end + + shared_examples 'no match' do + it 'does not match the pattern' do + expect(value).not_to match(pattern) + end + end + + shared_examples 'a SHA pattern' do + context "with too short value" do + let(:value) { sha[0, described_class::MIN_SHA_LENGTH - 1] } + + it_behaves_like 'no match' + end + + context "with full length" do + let(:value) { sha } + + it_behaves_like 'a match' + end + + context "with exceeeding length" do + let(:value) { sha + sha } + + # This case is not exactly pretty for SHA1 as we would still match the full SHA256 length. It's arguable what + # the correct behaviour would be, but without starting to distinguish SHA1 and SHA256 hashes this is the best + # we can do. + let(:expected_match) { (sha + sha)[0, described_class::MAX_SHA_LENGTH] } + + it_behaves_like 'a match' + end + + context "with embedded SHA" do + let(:value) { "xxx#{sha}xxx" } + + it_behaves_like 'a match' + end + end + + context 'abbreviated SHA pattern' do + let(:pattern) { described_class::SHA_PATTERN } + + context "with minimum length" do + let(:value) { sha[0, described_class::MIN_SHA_LENGTH] } + let(:expected_match) { value } + + it_behaves_like 'a match' + end + + context "with medium length" do + let(:value) { sha[0, described_class::MIN_SHA_LENGTH + 20] } + let(:expected_match) { value } + + it_behaves_like 'a match' + end + + it_behaves_like 'a SHA pattern' + end + + context 'full SHA pattern' do + let(:pattern) { described_class::FULL_SHA_PATTERN } + + context 'with abbreviated length' do + let(:value) { sha[0, described_class::SHA1_LENGTH - 1] } + + it_behaves_like 'no match' + end + + it_behaves_like 'a SHA pattern' + end + end + + context 'SHA1' do + let(:sha) { "5716ca5987cbf97d6bb54920bea6adde242d87e6" } + + it_behaves_like 'a SHA-matching pattern' + end + + context 'SHA256' do + let(:sha) { "a52e146ac2ab2d0efbb768ab8ebd1e98a6055764c81fe424fbae4522f5b4cb92" } + + it_behaves_like 'a SHA-matching pattern' + end + end + def sample_commit_hash { author_email: "dmitriy.zaporozhets@gmail.com", diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index f9a6c25b786..1818693974e 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -39,6 +39,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do title updated_at updated_by_id + draft ].freeze expect(safe_attribute_keys).to match_array(expected_safe_attribute_keys) @@ -66,6 +67,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do url last_commit work_in_progress + draft total_time_spent time_change human_total_time_spent diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb index d119a4e2b9d..e0823aa8153 100644 --- a/spec/lib/gitlab/x509/signature_spec.rb +++ b/spec/lib/gitlab/x509/signature_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Gitlab::X509::Signature do it 'returns a verified signature if email does match' do expect(signature.x509_certificate).to have_attributes(certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) expect(signature.verified_signature).to be_truthy expect(signature.verification_status).to eq(:verified) @@ -55,6 +56,27 @@ RSpec.describe Gitlab::X509::Signature do expect(signature.verification_status).to eq(:verified) end + context 'when the certificate contains multiple emails' do + before do + allow_any_instance_of(described_class).to receive(:get_certificate_extension).and_call_original + + allow_any_instance_of(described_class).to receive(:get_certificate_extension) + .with('subjectAltName') + .and_return("email:gitlab2@example.com, othername:<unsupported>, email:#{X509Helpers::User1.certificate_email}") + end + + context 'and the email matches one of them' do + it 'returns a verified signature' do + expect(signature.x509_certificate).to have_attributes(certificate_attributes.except(:email, :emails)) + expect(signature.x509_certificate.email).to eq('gitlab2@example.com') + expect(signature.x509_certificate.emails).to contain_exactly('gitlab2@example.com', X509Helpers::User1.certificate_email) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) + expect(signature.verified_signature).to be_truthy + expect(signature.verification_status).to eq(:verified) + end + end + end + context "if the email matches but isn't confirmed" do let!(:user) { create(:user, :unconfirmed, email: X509Helpers::User1.certificate_email) } @@ -106,6 +128,7 @@ RSpec.describe Gitlab::X509::Signature do subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier, subject: X509Helpers::User1.certificate_subject, email: X509Helpers::User1.certificate_email, + emails: [X509Helpers::User1.certificate_email], serial_number: X509Helpers::User1.certificate_serial } end @@ -248,15 +271,31 @@ RSpec.describe Gitlab::X509::Signature do .and_return("email:gitlab@example.com, othername:<unsupported>") end - it 'extracts email' do - signature = described_class.new( + let(:signature) do + described_class.new( X509Helpers::User1.signed_commit_signature, X509Helpers::User1.signed_commit_base_data, 'gitlab@example.com', X509Helpers::User1.signed_commit_time ) + end + it 'extracts email' do expect(signature.x509_certificate.email).to eq("gitlab@example.com") + expect(signature.x509_certificate.emails).to contain_exactly("gitlab@example.com") + end + + context 'when there are multiple emails' do + before do + allow_any_instance_of(described_class).to receive(:get_certificate_extension) + .with('subjectAltName') + .and_return("email:gitlab@example.com, othername:<unsupported>, email:gitlab2@example.com") + end + + it 'extracts all the emails' do + expect(signature.x509_certificate.email).to eq("gitlab@example.com") + expect(signature.x509_certificate.emails).to contain_exactly("gitlab@example.com", "gitlab2@example.com") + end end end @@ -311,6 +350,7 @@ RSpec.describe Gitlab::X509::Signature do subject_key_identifier: X509Helpers::User1.tag_certificate_subject_key_identifier, subject: X509Helpers::User1.certificate_subject, email: X509Helpers::User1.certificate_email, + emails: [X509Helpers::User1.certificate_email], serial_number: X509Helpers::User1.tag_certificate_serial } end diff --git a/spec/migrations/20230724071541_queue_backfill_default_branch_protection_namespace_setting_spec.rb b/spec/migrations/20230724071541_queue_backfill_default_branch_protection_namespace_setting_spec.rb new file mode 100644 index 00000000000..5ba8c6b853c --- /dev/null +++ b/spec/migrations/20230724071541_queue_backfill_default_branch_protection_namespace_setting_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillDefaultBranchProtectionNamespaceSetting, feature_category: :database do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :namespace_settings, + column_name: :namespace_id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index dd3d4f1865c..7ab43611108 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -831,7 +831,8 @@ eos expect(described_class.valid_hash?('a' * 6)).to be false expect(described_class.valid_hash?('a' * 7)).to be true expect(described_class.valid_hash?('a' * 40)).to be true - expect(described_class.valid_hash?('a' * 41)).to be false + expect(described_class.valid_hash?('a' * 64)).to be true + expect(described_class.valid_hash?('a' * 65)).to be false end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 7df49fbe548..a6766035c1e 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -180,7 +180,9 @@ RSpec.describe ProjectTeam, feature_category: :groups_and_projects do subject(:import) { target_project.team.import(source_project, current_user) } - it { is_expected.to match(imported_members) } + it 'matches the imported members', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/419394' do + is_expected.to match(imported_members) + end it 'target project includes source member with the same access' do import diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb index 150c7bd5f3e..00a2c218466 100644 --- a/spec/presenters/blob_presenter_spec.rb +++ b/spec/presenters/blob_presenter_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -RSpec.describe BlobPresenter do - let_it_be(:project) { create(:project, :repository) } +RSpec.describe BlobPresenter, feature_category: :source_code_management do + let_it_be(:project) { create(:project, :repository, lfs: true) } let_it_be(:user) { project.first_owner } let(:repository) { project.repository } @@ -228,6 +228,83 @@ RSpec.describe BlobPresenter do it { expect(presenter.code_owners).to match_array([]) } end + describe '#external_storage_url' do + let(:static_objects_external_storage_url) { nil } + let(:raw_path) { Rails.application.routes.url_helpers.project_raw_path(project, File.join(ref, path)) } + let(:token) { '' } + let(:cdn_fronted_url) do + "#{static_objects_external_storage_url}#{raw_path}#{token}" + end + + subject { presenter.external_storage_url } + + before do + stub_application_setting(static_objects_external_storage_url: static_objects_external_storage_url) + end + + context 'when blob is an lfs pointer' do + let_it_be(:blob) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } + let_it_be(:lfs_object) { project.lfs_objects.find_by_oid(blob.lfs_oid) } + + let(:lfs_object_url) { lfs_object.file.url(content_type: "application/octet-stream") } + let(:lfs_object_proxy_url) { "#{project.http_url_to_repo}/gitlab-lfs/objects/#{lfs_object.oid}" } + let(:proxy_download) { true } + let(:lfs_config) do + Gitlab.config.lfs.deep_merge( + 'enabled' => true, + 'object_store' => { + 'remote_directory' => 'lfs-objects', + 'enabled' => true, + 'proxy_download' => proxy_download, + 'connection' => { + 'endpoint' => 'http:127.0.0.1:9000', + 'path_style' => true + } + } + ) + end + + before do + stub_lfs_setting(lfs_config) + stub_lfs_object_storage(proxy_download: proxy_download) + end + + context 'when direct download is enabled' do + let(:proxy_download) { false } + + it { is_expected.to eq(lfs_object_url) } + end + + context 'when proxy download is enabled' do + it { is_expected.to eq(lfs_object_proxy_url) } + end + end + + context 'when static_objects_external_storage_enabled?' do + let(:static_objects_external_storage_url) { 'https://cdn.gitlab.com' } + + context 'and project is private' do + let(:token) { "?token=#{user.static_object_token}" } + + it { is_expected.to eq(cdn_fronted_url) } + end + + context 'and project is public' do + let(:token) { '' } + + before do + allow(project).to receive(:public?).and_return(true) + end + + it { is_expected.to eq(cdn_fronted_url) } + end + end + + context 'when not static_objects_external_storage_enabled?' do + it { is_expected.to be_nil } + end + end + describe '#ide_edit_path' do it { expect(presenter.ide_edit_path).to eq("/-/ide/project/#{project.full_path}/edit/HEAD/-/files/ruby/regex.rb") } end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index c78adc2dcef..abc42d11c63 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -380,13 +380,14 @@ RSpec.describe 'project routing' do it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/hooks/hook_logs/1', '/gitlab/gitlabhq/-/hooks/hook_logs/1' end - # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/} + # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: Gitlab::Git::Commit::SHA_PATTERN, project_id: /[^\/]+/} describe Projects::CommitController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq/-/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd') expect(get('/gitlab/gitlabhq/-/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff') expect(get('/gitlab/gitlabhq/-/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch') expect(get('/gitlab/gitlabhq/-/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') + expect(get('/gitlab/gitlabhq/-/commit/6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321') end it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commit/4246fbd", "/gitlab/gitlabhq/-/commit/4246fbd" @@ -652,6 +653,10 @@ RSpec.describe 'project routing' do it 'to #show' do expect(get('/gitlab/gitlabhq/-/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable') expect(get('/gitlab/gitlabhq/-/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') + expect(get('/gitlab/gitlabhq/-/compare/257cc5642cb1a054f08cc83f2d943e56fd3ebe99...5716ca5987cbf97d6bb54920bea6adde242d87e6')) + .to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', to: '5716ca5987cbf97d6bb54920bea6adde242d87e6') + expect(get('/gitlab/gitlabhq/-/compare/47d6aca82756ff2e61e53520bfdf1faa6c86d933be4854eb34840c57d12e0c85...a52e146ac2ab2d0efbb768ab8ebd1e98a6055764c81fe424fbae4522f5b4cb92')) + .to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: '47d6aca82756ff2e61e53520bfdf1faa6c86d933be4854eb34840c57d12e0c85', to: 'a52e146ac2ab2d0efbb768ab8ebd1e98a6055764c81fe424fbae4522f5b4cb92') end it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare', '/gitlab/gitlabhq/-/compare' diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb index 7beed9c7755..dc282bf0a68 100644 --- a/spec/support/helpers/filter_spec_helper.rb +++ b/spec/support/helpers/filter_spec_helper.rb @@ -94,9 +94,9 @@ module FilterSpecHelper when /\A(.+)?[^\d]\d+\z/ # Integer-based reference with optional project prefix reference.gsub(/\d+\z/) { |i| i.to_i + 10_000 } - when /\A(.+@)?(\h{7,40}\z)/ + when /\A(.+@)?(#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\z)/o # SHA-based reference with optional prefix - reference.gsub(/\h{7,40}\z/) { |v| v.reverse } + reference.gsub(/#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\z/o) { |v| v.reverse } else reference.gsub(/\w+\z/) { |v| v.reverse } end diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb index 3c78869ffaa..34e3ba95b0d 100644 --- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb +++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb @@ -15,6 +15,8 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type| name = 'My access token' visit resource_settings_access_tokens_path + + click_button 'Add new token' fill_in 'Token name', with: name # Set date to 1st of next month |