diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-06 18:08:12 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-06 18:08:12 +0000 |
commit | e22c3819ad2321a0cf825877fe3b60e41268c5b3 (patch) | |
tree | fcd143b30bdd7b42d439cd0b2fc5c6c4268d8d97 | |
parent | 49b16b71778148e9f9c579bf7bf69853c780c827 (diff) | |
download | gitlab-ce-e22c3819ad2321a0cf825877fe3b60e41268c5b3.tar.gz |
Add latest changes from gitlab-org/gitlab@master
191 files changed, 2390 insertions, 647 deletions
diff --git a/README.md b/README.md index 29d5d599972..b61af1b1f42 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL/OpenSUSE -- Ruby (MRI) 2.7.7 +- Ruby (MRI) 3.0.5 - Git 2.33+ - Redis 5.0+ - PostgreSQL 12+ diff --git a/app/assets/javascripts/blame/streaming/index.js b/app/assets/javascripts/blame/streaming/index.js new file mode 100644 index 00000000000..a74e01b6423 --- /dev/null +++ b/app/assets/javascripts/blame/streaming/index.js @@ -0,0 +1,56 @@ +import { renderHtmlStreams } from '~/streaming/render_html_streams'; +import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link'; +import { createAlert } from '~/flash'; +import { __ } from '~/locale'; +import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests'; +import { toPolyfillReadable } from '~/streaming/polyfills'; + +export async function renderBlamePageStreams(firstStreamPromise) { + const element = document.querySelector('#blame-stream-container'); + + if (!element || !firstStreamPromise) return; + + const stopAnchorObserver = handleStreamedAnchorLink(element); + const { dataset } = document.querySelector('#blob-content-holder'); + const totalExtraPages = parseInt(dataset.totalExtraPages, 10); + const { pagesUrl } = dataset; + + const remainingStreams = rateLimitStreamRequests({ + factory: (index) => { + const url = new URL(pagesUrl); + // page numbers start with 1 + // the first page is already rendered in the document + // the second page is passed with the 'firstStreamPromise' + url.searchParams.set('page', index + 3); + return fetch(url).then((response) => toPolyfillReadable(response.body)); + }, + // we don't want to overload gitaly with concurrent requests + // https://gitlab.com/gitlab-org/gitlab/-/issues/391842#note_1281695095 + // using 5 as a good starting point + maxConcurrentRequests: 5, + total: totalExtraPages, + }); + + try { + await renderHtmlStreams( + [firstStreamPromise.then(toPolyfillReadable), ...remainingStreams], + element, + ); + } catch (error) { + createAlert({ + message: __('Blame could not be loaded as a single page.'), + primaryButton: { + text: __('View blame as separate pages'), + clickHandler() { + const newUrl = new URL(window.location); + newUrl.searchParams.delete('streaming'); + window.location.href = newUrl; + }, + }, + }); + throw error; + } finally { + stopAnchorObserver(); + document.querySelector('#blame-stream-loading').remove(); + } +} diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js index 1e4b9de90f2..f0fdd18c828 100644 --- a/app/assets/javascripts/pages/projects/blame/show/index.js +++ b/app/assets/javascripts/pages/projects/blame/show/index.js @@ -1,5 +1,10 @@ import initBlob from '~/pages/projects/init_blob'; import redirectToCorrectPage from '~/blame/blame_redirect'; +import { renderBlamePageStreams } from '~/blame/streaming'; -redirectToCorrectPage(); +if (new URLSearchParams(window.location.search).get('streaming')) { + renderBlamePageStreams(window.blamePageStream); +} else { + redirectToCorrectPage(); +} initBlob(); diff --git a/app/assets/javascripts/streaming/chunk_writer.js b/app/assets/javascripts/streaming/chunk_writer.js new file mode 100644 index 00000000000..4bbd0a5f843 --- /dev/null +++ b/app/assets/javascripts/streaming/chunk_writer.js @@ -0,0 +1,144 @@ +import { throttle } from 'lodash'; +import { RenderBalancer } from '~/streaming/render_balancer'; +import { + BALANCE_RATE, + HIGH_FRAME_TIME, + LOW_FRAME_TIME, + MAX_CHUNK_SIZE, + MIN_CHUNK_SIZE, + TIMEOUT, +} from '~/streaming/constants'; + +const defaultConfig = { + balanceRate: BALANCE_RATE, + minChunkSize: MIN_CHUNK_SIZE, + maxChunkSize: MAX_CHUNK_SIZE, + lowFrameTime: LOW_FRAME_TIME, + highFrameTime: HIGH_FRAME_TIME, + timeout: TIMEOUT, +}; + +function concatUint8Arrays(a, b) { + const array = new Uint8Array(a.length + b.length); + array.set(a, 0); + array.set(b, a.length); + return array; +} + +// This class is used to write chunks with a balanced size +// to avoid blocking main thread for too long. +// +// A chunk can be: +// 1. Too small +// 2. Too large +// 3. Delayed in time +// +// This class resolves all these problems by +// 1. Splitting or concatenating chunks to met the size criteria +// 2. Rendering current chunk buffer immediately if enough time has passed +// +// The size of the chunk is determined by RenderBalancer, +// It measures execution time for each chunk write and adjusts next chunk size. +export class ChunkWriter { + buffer = null; + decoder = new TextDecoder('utf-8'); + timeout = null; + + constructor(htmlStream, config) { + this.htmlStream = htmlStream; + + const { balanceRate, minChunkSize, maxChunkSize, lowFrameTime, highFrameTime, timeout } = { + ...defaultConfig, + ...config, + }; + + // ensure we still render chunks over time if the size criteria is not met + this.scheduleAccumulatorFlush = throttle(this.flushAccumulator.bind(this), timeout); + + const averageSize = Math.round((maxChunkSize + minChunkSize) / 2); + this.size = Math.max(averageSize, minChunkSize); + + this.balancer = new RenderBalancer({ + lowFrameTime, + highFrameTime, + decrease: () => { + this.size = Math.round(Math.max(this.size / balanceRate, minChunkSize)); + }, + increase: () => { + this.size = Math.round(Math.min(this.size * balanceRate, maxChunkSize)); + }, + }); + } + + write(chunk) { + this.scheduleAccumulatorFlush.cancel(); + + if (this.buffer) { + this.buffer = concatUint8Arrays(this.buffer, chunk); + } else { + this.buffer = chunk; + } + + // accumulate chunks until the size is fulfilled + if (this.size > this.buffer.length) { + this.scheduleAccumulatorFlush(); + return Promise.resolve(); + } + + return this.balancedWrite(); + } + + balancedWrite() { + let cursor = 0; + + return this.balancer.render(() => { + const chunkPart = this.buffer.subarray(cursor, cursor + this.size); + // accumulate chunks until the size is fulfilled + // this is a hot path for the last chunkPart of the chunk + if (chunkPart.length < this.size) { + this.buffer = chunkPart; + this.scheduleAccumulatorFlush(); + return false; + } + + this.writeToDom(chunkPart); + + cursor += this.size; + if (cursor >= this.buffer.length) { + this.buffer = null; + return false; + } + // continue render + return true; + }); + } + + writeToDom(chunk, stream = true) { + // stream: true allows us to split chunks with multi-part words + const decoded = this.decoder.decode(chunk, { stream }); + this.htmlStream.write(decoded); + } + + flushAccumulator() { + if (this.buffer) { + this.writeToDom(this.buffer); + this.buffer = null; + } + } + + close() { + this.scheduleAccumulatorFlush.cancel(); + if (this.buffer) { + // last chunk should have stream: false to indicate the end of the stream + this.writeToDom(this.buffer, false); + this.buffer = null; + } + this.htmlStream.close(); + } + + abort() { + this.scheduleAccumulatorFlush.cancel(); + this.buffer = null; + this.htmlStream.abort(); + } +} diff --git a/app/assets/javascripts/streaming/constants.js b/app/assets/javascripts/streaming/constants.js new file mode 100644 index 00000000000..224d93a7ac1 --- /dev/null +++ b/app/assets/javascripts/streaming/constants.js @@ -0,0 +1,9 @@ +// Lower min chunk numbers can make the page loading take incredibly long +export const MIN_CHUNK_SIZE = 128 * 1024; +export const MAX_CHUNK_SIZE = 2048 * 1024; +export const LOW_FRAME_TIME = 32; +// Tasks that take more than 50ms are considered Long +// https://web.dev/optimize-long-tasks/ +export const HIGH_FRAME_TIME = 64; +export const BALANCE_RATE = 1.2; +export const TIMEOUT = 500; diff --git a/app/assets/javascripts/streaming/handle_streamed_anchor_link.js b/app/assets/javascripts/streaming/handle_streamed_anchor_link.js new file mode 100644 index 00000000000..315dc9bb0a0 --- /dev/null +++ b/app/assets/javascripts/streaming/handle_streamed_anchor_link.js @@ -0,0 +1,26 @@ +import { throttle } from 'lodash'; +import { scrollToElement } from '~/lib/utils/common_utils'; +import LineHighlighter from '~/blob/line_highlighter'; + +const noop = () => {}; + +export function handleStreamedAnchorLink(rootElement) { + // "#L100-200" → ['L100', 'L200'] + const [anchorStart, end] = window.location.hash.substring(1).split('-'); + const anchorEnd = end ? `L${end}` : anchorStart; + if (!anchorStart || document.getElementById(anchorEnd)) return noop; + + const handler = throttle((mutationList, instance) => { + if (!document.getElementById(anchorEnd)) return; + scrollToElement(document.getElementById(anchorStart)); + // eslint-disable-next-line no-new + new LineHighlighter(); + instance.disconnect(); + }, 300); + + const observer = new MutationObserver(handler); + + observer.observe(rootElement, { childList: true, subtree: true }); + + return () => observer.disconnect(); +} diff --git a/app/assets/javascripts/streaming/html_stream.js b/app/assets/javascripts/streaming/html_stream.js new file mode 100644 index 00000000000..8182f69a607 --- /dev/null +++ b/app/assets/javascripts/streaming/html_stream.js @@ -0,0 +1,33 @@ +import { ChunkWriter } from '~/streaming/chunk_writer'; + +export class HtmlStream { + constructor(element) { + const streamDocument = document.implementation.createHTMLDocument('stream'); + + streamDocument.open(); + streamDocument.write('<streaming-element>'); + + const virtualStreamingElement = streamDocument.querySelector('streaming-element'); + element.appendChild(document.adoptNode(virtualStreamingElement)); + + this.streamDocument = streamDocument; + } + + withChunkWriter(config) { + return new ChunkWriter(this, config); + } + + write(chunk) { + // eslint-disable-next-line no-unsanitized/method + this.streamDocument.write(chunk); + } + + close() { + this.streamDocument.write('</streaming-element>'); + this.streamDocument.close(); + } + + abort() { + this.streamDocument.close(); + } +} diff --git a/app/assets/javascripts/streaming/polyfills.js b/app/assets/javascripts/streaming/polyfills.js new file mode 100644 index 00000000000..a9a044a3e99 --- /dev/null +++ b/app/assets/javascripts/streaming/polyfills.js @@ -0,0 +1,5 @@ +import { createReadableStreamWrapper } from '@mattiasbuelens/web-streams-adapter'; +import { ReadableStream as PolyfillReadableStream } from 'web-streams-polyfill'; + +// TODO: remove this when our WebStreams API reaches 100% support +export const toPolyfillReadable = createReadableStreamWrapper(PolyfillReadableStream); diff --git a/app/assets/javascripts/streaming/rate_limit_stream_requests.js b/app/assets/javascripts/streaming/rate_limit_stream_requests.js new file mode 100644 index 00000000000..04a592baa16 --- /dev/null +++ b/app/assets/javascripts/streaming/rate_limit_stream_requests.js @@ -0,0 +1,87 @@ +const consumeReadableStream = (stream) => { + return new Promise((resolve, reject) => { + stream.pipeTo( + new WritableStream({ + close: resolve, + abort: reject, + }), + ); + }); +}; + +const wait = (timeout) => + new Promise((resolve) => { + setTimeout(resolve, timeout); + }); + +// this rate-limiting approach is specific to Web Streams +// because streams only resolve when they're fully consumed +// so we need to split each stream into two pieces: +// one for the rate-limiter (wait for all the bytes to be sent) +// another for the original consumer +export const rateLimitStreamRequests = ({ + factory, + total, + maxConcurrentRequests, + immediateCount = maxConcurrentRequests, + timeout = 0, +}) => { + if (total === 0) return []; + + const unsettled = []; + + const pushUnsettled = (promise) => { + let res; + let rej; + const consume = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + unsettled.push(consume); + return promise.then((stream) => { + const [first, second] = stream.tee(); + // eslint-disable-next-line promise/no-nesting + consumeReadableStream(first) + .then(() => { + unsettled.splice(unsettled.indexOf(consume), 1); + res(); + }) + .catch(rej); + return second; + }, rej); + }; + + const immediate = Array.from({ length: Math.min(immediateCount, total) }, (_, i) => + pushUnsettled(factory(i)), + ); + + const queue = []; + const flushQueue = () => { + const promises = + unsettled.length > maxConcurrentRequests ? unsettled : [...unsettled, wait(timeout)]; + // errors are handled by the caller + // eslint-disable-next-line promise/catch-or-return + Promise.race(promises).then(() => { + const cb = queue.shift(); + cb?.(); + if (queue.length !== 0) { + // wait for stream consumer promise to be removed from unsettled + queueMicrotask(flushQueue); + } + }); + }; + + const throttled = Array.from({ length: total - immediateCount }, (_, i) => { + return new Promise((resolve, reject) => { + queue.push(() => { + pushUnsettled(factory(i + immediateCount)) + .then(resolve) + .catch(reject); + }); + }); + }); + + flushQueue(); + + return [...immediate, ...throttled]; +}; diff --git a/app/assets/javascripts/streaming/render_balancer.js b/app/assets/javascripts/streaming/render_balancer.js new file mode 100644 index 00000000000..66929ff3a54 --- /dev/null +++ b/app/assets/javascripts/streaming/render_balancer.js @@ -0,0 +1,36 @@ +export class RenderBalancer { + previousTimestamp = undefined; + + constructor({ increase, decrease, highFrameTime, lowFrameTime }) { + this.increase = increase; + this.decrease = decrease; + this.highFrameTime = highFrameTime; + this.lowFrameTime = lowFrameTime; + } + + render(fn) { + return new Promise((resolve) => { + const callback = (timestamp) => { + this.throttle(timestamp); + if (fn()) requestAnimationFrame(callback); + else resolve(); + }; + requestAnimationFrame(callback); + }); + } + + throttle(timestamp) { + const { previousTimestamp } = this; + this.previousTimestamp = timestamp; + if (previousTimestamp === undefined) return; + + const duration = Math.round(timestamp - previousTimestamp); + if (!duration) return; + + if (duration >= this.highFrameTime) { + this.decrease(); + } else if (duration < this.lowFrameTime) { + this.increase(); + } + } +} diff --git a/app/assets/javascripts/streaming/render_html_streams.js b/app/assets/javascripts/streaming/render_html_streams.js new file mode 100644 index 00000000000..7201e541777 --- /dev/null +++ b/app/assets/javascripts/streaming/render_html_streams.js @@ -0,0 +1,40 @@ +import { HtmlStream } from '~/streaming/html_stream'; + +async function pipeStreams(domWriter, streamPromises) { + try { + for await (const stream of streamPromises.slice(0, -1)) { + await stream.pipeTo(domWriter, { preventClose: true }); + } + const stream = await streamPromises[streamPromises.length - 1]; + await stream.pipeTo(domWriter); + } catch (error) { + domWriter.abort(error); + } +} + +// this function (and the rest of the pipeline) expects polyfilled streams +// do not pass native streams here unless our browser support allows for it +// TODO: remove this notice when our WebStreams API support reaches 100% +export function renderHtmlStreams(streamPromises, element, config) { + if (streamPromises.length === 0) return Promise.resolve(); + + const chunkedHtmlStream = new HtmlStream(element).withChunkWriter(config); + + return new Promise((resolve, reject) => { + const domWriter = new WritableStream({ + write(chunk) { + return chunkedHtmlStream.write(chunk); + }, + close() { + chunkedHtmlStream.close(); + resolve(); + }, + abort(error) { + chunkedHtmlStream.abort(); + reject(error); + }, + }); + + pipeStreams(domWriter, streamPromises); + }); +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 9ea5a66b3bc..b292adf9eac 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -580,3 +580,31 @@ span.idiff { padding: 0; border-radius: 0 0 $border-radius-default $border-radius-default; } + +.blame-stream-container { + border-top: 1px solid $border-color; +} + +.blame-stream-loading { + $gradient-size: 16px; + position: sticky; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + margin-top: -$gradient-size; + height: $gl-spacing-scale-10; + border-top: $gradient-size solid transparent; + background-color: $white; + box-sizing: content-box; + background-clip: content-box; + + .gradient { + position: absolute; + left: 0; + right: 0; + top: -$gradient-size; + height: $gradient-size; + background: linear-gradient(to top, $white, transparentize($white, 1)); + } +} diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 91fc1bf489d..53d302f60ee 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -6,7 +6,7 @@ class Admin::ApplicationsController < Admin::ApplicationController before_action :set_application, only: [:show, :edit, :update, :renew, :destroy] before_action :load_scopes, only: [:new, :create, :edit, :update] - feature_category :authentication_and_authorization + feature_category :system_access def index applications = ApplicationsFinder.new.execute diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb index dcec50e882d..0745ba328c6 100644 --- a/app/controllers/admin/identities_controller.rb +++ b/app/controllers/admin/identities_controller.rb @@ -4,7 +4,7 @@ class Admin::IdentitiesController < Admin::ApplicationController before_action :user before_action :identity, except: [:index, :new, :create] - feature_category :authentication_and_authorization + feature_category :system_access def new @identity = Identity.new diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index ddc555add5c..dae3337d19b 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -4,7 +4,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController before_action :user before_action :verify_impersonation_enabled! - feature_category :authentication_and_authorization + feature_category :user_management def index set_index_vars diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb index 6c45b03455e..c1a6cb350ec 100644 --- a/app/controllers/admin/impersonations_controller.rb +++ b/app/controllers/admin/impersonations_controller.rb @@ -4,7 +4,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController skip_before_action :authenticate_admin! before_action :authenticate_impersonator! - feature_category :authentication_and_authorization + feature_category :user_management def destroy original_user = stop_impersonation diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index 03383604e30..e4a756ec12d 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -3,7 +3,7 @@ class Admin::KeysController < Admin::ApplicationController before_action :user, only: [:show, :destroy] - feature_category :authentication_and_authorization + feature_category :user_management def show @key = user.keys.find(params[:id]) diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb index 63579421573..bb275532170 100644 --- a/app/controllers/admin/sessions_controller.rb +++ b/app/controllers/admin/sessions_controller.rb @@ -7,7 +7,7 @@ class Admin::SessionsController < ApplicationController before_action :user_is_admin! - feature_category :authentication_and_authorization + feature_category :system_access def new if current_user_mode.admin_mode? diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index a0ba5b9c8a4..4d1cbd8becc 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController prepend_before_action :check_recaptcha, only: :create before_action :load_recaptcha, only: :new - feature_category :authentication_and_authorization + feature_category :user_management def almost_there flash[:notice] = nil diff --git a/app/controllers/groups/settings/access_tokens_controller.rb b/app/controllers/groups/settings/access_tokens_controller.rb index d86ddcfe2d0..ff07e881bfa 100644 --- a/app/controllers/groups/settings/access_tokens_controller.rb +++ b/app/controllers/groups/settings/access_tokens_controller.rb @@ -7,7 +7,7 @@ module Groups include AccessTokensActions layout 'group_settings' - feature_category :authentication_and_authorization + feature_category :system_access alias_method :resource, :group diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb index 6fb2b65feb8..b174ba9a6ad 100644 --- a/app/controllers/groups/settings/applications_controller.rb +++ b/app/controllers/groups/settings/applications_controller.rb @@ -9,7 +9,7 @@ module Groups before_action :set_application, only: [:show, :edit, :update, :renew, :destroy] before_action :load_scopes, only: [:index, :create, :edit, :update] - feature_category :authentication_and_authorization + feature_category :system_access def index set_index_vars diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 0bee1faccf5..2729b11fcff 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -245,11 +245,7 @@ class Import::GithubController < Import::BaseController { before: params[:before].presence, after: params[:after].presence, - first: PAGE_LENGTH, - # TODO: remove after rollout FF github_client_fetch_repos_via_graphql - # https://gitlab.com/gitlab-org/gitlab/-/issues/385649 - page: [1, params[:page].to_i].max, - per_page: PAGE_LENGTH + first: PAGE_LENGTH } end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 2a7f2d42e2a..0a2c98af8ec 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -13,7 +13,7 @@ class InvitesController < ApplicationController respond_to :html - feature_category :authentication_and_authorization + feature_category :system_access def show accept if skip_invitation_prompt? diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7211eebdb4b..d299613f498 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -8,7 +8,7 @@ class JwtController < ApplicationController # Add this before other actions, since we want to have the user or project prepend_before_action :auth_user, :authenticate_project_or_user - feature_category :authentication_and_authorization + feature_category :system_access # https://gitlab.com/gitlab-org/gitlab/-/issues/357037 urgency :low diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 4046433f8ea..e450151fd82 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -12,7 +12,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController protect_from_forgery except: [:cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true - feature_category :authentication_and_authorization + feature_category :system_access def handle_omniauth omniauth_flow(Gitlab::Auth::OAuth) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 38cdb16c350..38839497fb6 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -12,7 +12,7 @@ class PasswordsController < Devise::PasswordsController before_action :check_password_authentication_available, only: [:create] before_action :throttle_reset, only: [:create] - feature_category :authentication_and_authorization + feature_category :system_access # rubocop: disable CodeReuse/ActiveRecord def edit diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index cb8b2783000..eb64016379d 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -3,7 +3,7 @@ class Profiles::AccountsController < Profiles::ApplicationController include AuthHelper - feature_category :authentication_and_authorization + feature_category :system_access urgency :low, [:show] def show diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb index 2607ba7d404..5a86179b89f 100644 --- a/app/controllers/profiles/active_sessions_controller.rb +++ b/app/controllers/profiles/active_sessions_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Profiles::ActiveSessionsController < Profiles::ApplicationController - feature_category :authentication_and_authorization + feature_category :system_access def index @sessions = ActiveSession.list(current_user).reject(&:is_impersonated) diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index 738c41207d5..7a0dfbbba0d 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -11,7 +11,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController layout :determine_layout - feature_category :authentication_and_authorization + feature_category :system_access def new end diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 1663aa61f62..8d5c690fbfe 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -3,7 +3,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController include RenderAccessTokens - feature_category :authentication_and_authorization + feature_category :system_access before_action :check_personal_access_tokens_enabled diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index aded295bfab..89151068696 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -12,7 +12,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController push_frontend_feature_flag(:webauthn) end - feature_category :authentication_and_authorization + feature_category :system_access def show setup_show_page diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb index 32ca303e722..2ee0e9fe960 100644 --- a/app/controllers/profiles/u2f_registrations_controller.rb +++ b/app/controllers/profiles/u2f_registrations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Profiles::U2fRegistrationsController < Profiles::ApplicationController - feature_category :authentication_and_authorization + feature_category :system_access def destroy u2f_registration = current_user.u2f_registrations.find(params[:id]) diff --git a/app/controllers/profiles/webauthn_registrations_controller.rb b/app/controllers/profiles/webauthn_registrations_controller.rb index a4a6d84f1ae..345d7bdbca8 100644 --- a/app/controllers/profiles/webauthn_registrations_controller.rb +++ b/app/controllers/profiles/webauthn_registrations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Profiles::WebauthnRegistrationsController < Profiles::ApplicationController - feature_category :authentication_and_authorization + feature_category :system_access def destroy webauthn_registration = current_user.webauthn_registrations.find(params[:id]) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 45b274fc920..70487915707 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -17,7 +17,7 @@ class ProfilesController < Profiles::ApplicationController feature_category :user_profile, [:show, :update, :reset_incoming_email_token, :reset_feed_token, :reset_static_object_token, :update_username] - feature_category :authentication_and_authorization, [:audit_log] + feature_category :system_access, [:audit_log] urgency :low, [:show, :update] def show diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index cfff281604e..d41b347dc5a 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -23,13 +23,47 @@ class Projects::BlameController < Projects::ApplicationController environment_params[:find_latest] = true @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last - blame_service = Projects::BlameService.new(@blob, @commit, params.permit(:page, :no_pagination)) + permitted_params = params.permit(:page, :no_pagination, :streaming) + blame_service = Projects::BlameService.new(@blob, @commit, permitted_params) @blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate! - @blame_pagination = blame_service.pagination + @entire_blame_path = full_blame_path(no_pagination: true) + @blame_pages_url = blame_pages_url(permitted_params) + if blame_service.streaming_possible + @entire_blame_path = full_blame_path(streaming: true) + end + + @streaming_enabled = blame_service.streaming_enabled + @blame_pagination = blame_service.pagination unless @streaming_enabled @blame_per_page = blame_service.per_page + + render locals: { total_extra_pages: blame_service.total_extra_pages } + end + + def page + @blob = @repository.blob_at(@commit.id, @path) + + environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } + environment_params[:find_latest] = true + @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last + + blame_service = Projects::BlameService.new(@blob, @commit, params.permit(:page, :streaming)) + + @blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate! + + render partial: 'page' + end + + private + + def full_blame_path(params) + namespace_project_blame_path(namespace_id: @project.namespace, project_id: @project, id: @id, **params) + end + + def blame_pages_url(params) + namespace_project_blame_page_url(namespace_id: @project.namespace, project_id: @project, id: @id, **params) end end diff --git a/app/controllers/projects/settings/access_tokens_controller.rb b/app/controllers/projects/settings/access_tokens_controller.rb index 0884816ef62..af1527ba6a3 100644 --- a/app/controllers/projects/settings/access_tokens_controller.rb +++ b/app/controllers/projects/settings/access_tokens_controller.rb @@ -7,7 +7,7 @@ module Projects include AccessTokensActions layout 'project_settings' - feature_category :authentication_and_authorization + feature_category :system_access alias_method :resource, :project diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb index cfb4e939b35..78c00a81a93 100644 --- a/app/controllers/registrations/welcome_controller.rb +++ b/app/controllers/registrations/welcome_controller.rb @@ -10,7 +10,7 @@ module Registrations skip_before_action :authenticate_user!, :required_signup_info, :check_two_factor_requirement, only: [:show, :update] before_action :require_current_user - feature_category :authentication_and_authorization + feature_category :user_management def show return redirect_to path_for_signed_in_user(current_user) if completed_welcome_step? diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 420ca6a2286..edc74dd71fc 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -27,7 +27,7 @@ class RegistrationsController < Devise::RegistrationsController push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops) end - feature_category :authentication_and_authorization + feature_category :user_management def new @resource = build_resource diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index b6aba04c877..83034e3faa6 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -56,7 +56,7 @@ class SessionsController < Devise::SessionsController # token mismatch. protect_from_forgery with: :exception, prepend: true, except: :destroy - feature_category :authentication_and_authorization + feature_category :system_access urgency :low CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha' diff --git a/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb new file mode 100644 index 00000000000..be17601e7a2 --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Resolvers + module Analytics + module CycleAnalytics + class DeploymentCountResolver < BaseResolver + type Types::Analytics::CycleAnalytics::MetricType, null: true + + argument :from, Types::TimeType, + required: true, + description: 'Deployments finished after the date.' + + argument :to, Types::TimeType, + required: true, + description: 'Deployments finished before the date.' + + def resolve(**args) + value = count(args) + { + value: value, + title: n_('Deploy', 'Deploys', value.to_i), + identifier: 'deploys', + links: [] + } + end + + private + + def count(args) + finder = DeploymentsFinder.new({ + finished_after: args[:from], + finished_before: args[:to], + project: object.project, + status: :success, + order_by: :finished_at + }) + + finder.execute.count + end + + # :project level: no customization, returning the original resolver + # :group level: add the project_ids argument + def self.[](context = :project) + case context + when :project + self + when :group + Class.new(self) do + argument :project_ids, [GraphQL::Types::ID], + required: false, + description: 'Project IDs within the group hierarchy.' + end + + end + end + end + end + end +end + +mod = Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver +mod.prepend_mod_with('Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver') diff --git a/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb index d320cd6cfc6..2645a86a9f8 100644 --- a/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb +++ b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb @@ -14,6 +14,11 @@ module Types null: true, description: 'Number of issues opened in the given period.', resolver: Resolvers::Analytics::CycleAnalytics::IssueCountResolver[context] + field :deployment_count, + Types::Analytics::CycleAnalytics::MetricType, + null: true, + description: 'Number of production deployments in the given period.', + resolver: Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver[context] end end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index a11e2c00cf8..ba6e760fd2b 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -85,7 +85,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? } condition(:create_runner_workflow_enabled) do - Feature.enabled?(:create_runner_workflow_for_admin) + Feature.enabled?(:create_runner_workflow_for_namespace, group) end condition(:achievements_enabled, scope: :subject) do diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index cab3dff0400..fbe0f9bfeea 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -235,7 +235,7 @@ class ProjectPolicy < BasePolicy end condition(:create_runner_workflow_enabled) do - Feature.enabled?(:create_runner_workflow_for_admin) + Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace) end # `:read_project` may be prevented in EE, but `:read_project_for_iids` should diff --git a/app/services/projects/blame_service.rb b/app/services/projects/blame_service.rb index 58e146e5a32..1ea16040655 100644 --- a/app/services/projects/blame_service.rb +++ b/app/services/projects/blame_service.rb @@ -5,15 +5,19 @@ module Projects class BlameService PER_PAGE = 1000 + STREAMING_FIRST_PAGE_SIZE = 200 + STREAMING_PER_PAGE = 2000 def initialize(blob, commit, params) @blob = blob @commit = commit - @page = extract_page(params) + @streaming_enabled = streaming_state(params) @pagination_enabled = pagination_state(params) + @page = extract_page(params) + @params = params end - attr_reader :page + attr_reader :page, :streaming_enabled def blame Gitlab::Blame.new(blob, commit, range: blame_range) @@ -28,7 +32,22 @@ module Projects end def per_page - PER_PAGE + streaming_enabled ? STREAMING_PER_PAGE : PER_PAGE + end + + def total_pages + total = (blob_lines_count.to_f / per_page).ceil + return total unless streaming_enabled + + ([blob_lines_count - STREAMING_FIRST_PAGE_SIZE, 0].max.to_f / per_page).ceil + 1 + end + + def total_extra_pages + [total_pages - 1, 0].max + end + + def streaming_possible + Feature.enabled?(:blame_page_streaming, commit.project) end private @@ -36,9 +55,16 @@ module Projects attr_reader :blob, :commit, :pagination_enabled def blame_range - return unless pagination_enabled + return unless pagination_enabled || streaming_enabled first_line = (page - 1) * per_page + 1 + + if streaming_enabled + return 1..STREAMING_FIRST_PAGE_SIZE if page == 1 + + first_line = STREAMING_FIRST_PAGE_SIZE + (page - 2) * per_page + 1 + end + last_line = (first_line + per_page).to_i - 1 first_line..last_line @@ -52,6 +78,12 @@ module Projects page end + def streaming_state(params) + return false unless streaming_possible + + Gitlab::Utils.to_boolean(params[:streaming], default: false) + end + def pagination_state(params) return false if Gitlab::Utils.to_boolean(params[:no_pagination], default: false) @@ -59,7 +91,7 @@ module Projects end def overlimit?(page) - page * per_page >= blob_lines_count + per_page + page > total_pages end def blob_lines_count diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb index 951017b2d01..6906ab2b642 100644 --- a/app/services/protected_branches/base_service.rb +++ b/app/services/protected_branches/base_service.rb @@ -21,3 +21,5 @@ module ProtectedBranches end end end + +ProtectedBranches::BaseService.prepend_mod diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index dd441d0d155..f0c1b090140 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -10,6 +10,7 @@ %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' } = render 'layouts/startup_js' + = yield :startup_js - if page_canonical_link %link{ rel: 'canonical', href: page_canonical_link } diff --git a/app/views/layouts/component_preview.html.haml b/app/views/layouts/component_preview.html.haml index a1b1304f994..8217ac13c52 100644 --- a/app/views/layouts/component_preview.html.haml +++ b/app/views/layouts/component_preview.html.haml @@ -1,12 +1,12 @@ %head - - if params[:lookbook][:display][:theme] == 'light' + - if params[:lookbook][:display][:theme] == "light" = stylesheet_link_tag "application" = stylesheet_link_tag "application_utilities" - else = stylesheet_link_tag "application_dark" = stylesheet_link_tag "application_utilities_dark" %body - .container.gl-mt-6 + .gl-mt-6{ class: (params[:lookbook][:display][:layout] == "fluid" ? "container-fluid" : "container") } - if params[:lookbook][:display][:bg_dark] .bg-dark.rounded.shadow.p-4 = yield diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 74b85a93c8e..ee7ca9cd351 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,6 +1,15 @@ - page_title _("Blame"), @blob.path, @ref - add_page_specific_style 'page_bundles/tree' -- dataset = { testid: 'blob-content-holder', qa_selector: 'blame_file_content', per_page: @blame_per_page } +- if @streaming_enabled && total_extra_pages > 0 + - content_for :startup_js do + = javascript_tag do + :plain + window.blamePageStream = (() => { + const url = new URL("#{@blame_pages_url}"); + url.searchParams.set('page', 2); + return fetch(url).then(response => response.body); + })(); +- dataset = { testid: 'blob-content-holder', qa_selector: 'blame_file_content', per_page: @blame_per_page, total_extra_pages: total_extra_pages - 1, pages_url: @blame_pages_url } #blob-content-holder.tree-holder.js-per-page{ data: dataset } = render "projects/blob/breadcrumb", blob: @blob, blame: true @@ -26,11 +35,21 @@ .blame-table-wrapper = render partial: 'page' + - if @streaming_enabled + #blame-stream-container.blame-stream-container + - if @blame_pagination && @blame_pagination.total_pages > 1 .gl-display-flex.gl-justify-content-center.gl-flex-direction-column.gl-align-items-center.gl-p-3.gl-bg-gray-50.gl-border-t-solid.gl-border-t-1.gl-border-gray-100 - = _('For faster browsing, not all history is shown.') - = render Pajamas::ButtonComponent.new(href: namespace_project_blame_path(namespace_id: @project.namespace, project_id: @project, id: @id, no_pagination: true), size: :small, button_options: { class: 'gl-mt-3' }) do |c| - = _('View entire blame') + = render Pajamas::ButtonComponent.new(href: @entire_blame_path, size: :small, button_options: { class: 'gl-mt-3' }) do |c| + = _('Show full blame') + + - if @streaming_enabled + #blame-stream-loading.blame-stream-loading + .gradient + = gl_loading_icon(size: 'sm') + %span.gl-mx-2 + = _('Loading full blame...') - if @blame_pagination = paginate(@blame_pagination, theme: "gitlab") + diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml index b62440fcbde..c916b6c3d45 100644 --- a/app/views/users/_profile_basic_info.html.haml +++ b/app/views/users/_profile_basic_info.html.haml @@ -1,9 +1,9 @@ -.gl-text-gray-900.gl-mt-4 - = render 'middle_dot_divider' do +.gl-text-gray-900 + = render 'middle_dot_divider', stacking: true do @#{@user.username} - if can?(current_user, :read_user_profile, @user) - = render 'middle_dot_divider' do + = render 'middle_dot_divider', stacking: true do = s_('UserProfile|User ID: %{id}') % { id: @user.id } = clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id) - = render 'middle_dot_divider' do + = render 'middle_dot_divider', stacking: true do = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) } diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index dd84b303655..29fd894cebf 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -43,80 +43,82 @@ = _('Follow') .profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] } - .avatar-holder - = link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do - = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" }) - - - if @user.blocked? || !@user.confirmed? - .user-info - %h1.cover-title - = user_display_name(@user) - = render "users/profile_basic_info" - - else - .user-info - %h1.cover-title{ itemprop: 'name' } - = @user.name - - if @user.pronouns.present? - %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle - = "(#{@user.pronouns})" - - if @user.status&.busy? - %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)") - - - if @user.pronunciation.present? - .gl-align-items-center - %p.gl-mb-4.gl-text-gray-500.gl-max-w-80.gl-mx-auto= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation } - - - if @user.status&.customized? - .cover-status.gl-display-inline-flex.gl-align-items-center - = emoji_icon(@user.status.emoji, class: 'gl-mr-2') - = markdown_field(@user.status, :message) + .gl-display-inline-block.gl-mx-8.gl-vertical-align-top + .avatar-holder + = link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do + = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" }) + .gl-display-inline-block.gl-vertical-align-top.gl-text-left + - if @user.blocked? || !@user.confirmed? + .user-info + %h1.cover-title.gl-my-0 + = user_display_name(@user) = render "users/profile_basic_info" - .gl-text-gray-900.mb-1.mb-sm-2 - - unless @user.location.blank? - = render 'middle_dot_divider', stacking: true, itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' do - = sprite_icon('location', css_class: 'fgray') - %span{ itemprop: 'addressLocality' } - = @user.location + - else + .user-info + %h1.cover-title.gl-my-0{ itemprop: 'name' } + = @user.name + - if @user.pronouns.present? + %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle + = "(#{@user.pronouns})" + - if @user.status&.busy? + %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)") + + - if @user.pronunciation.present? + .gl-align-items-center + %p.gl-mb-4.gl-text-gray-500.gl-max-w-80.gl-mx-auto= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation } + + - if @user.status&.customized? + .cover-status.gl-display-inline-flex.gl-align-items-center.gl-mb-3 + = emoji_icon(@user.status.emoji, class: 'gl-mr-2') + = markdown_field(@user.status, :message) + = render "users/profile_basic_info" - user_local_time = local_time(@user.timezone) - - unless user_local_time.nil? - = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do - = sprite_icon('clock', css_class: 'fgray') - %span - = user_local_time - - unless work_information(@user).blank? - = render 'middle_dot_divider', stacking: true do - = sprite_icon('work', css_class: 'fgray') - %span - = work_information(@user, with_schema_markup: true) - .gl-text-gray-900 - - unless @user.skype.blank? - = render 'middle_dot_divider' do - = link_to "skype:#{@user.skype}", class: 'gl-hover-text-decoration-none', title: "Skype" do - = sprite_icon('skype', css_class: 'skype-icon') - - unless @user.linkedin.blank? - = render 'middle_dot_divider' do - = link_to linkedin_url(@user), class: 'gl-hover-text-decoration-none', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do - = sprite_icon('linkedin', css_class: 'linkedin-icon') - - unless @user.twitter.blank? - = render 'middle_dot_divider', breakpoint: 'sm' do - = link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do - = sprite_icon('twitter', css_class: 'twitter-icon') - - unless @user.discord.blank? - = render 'middle_dot_divider', breakpoint: 'sm' do - = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do - = sprite_icon('discord', css_class: 'discord-icon') - - unless @user.website_url.blank? - = render 'middle_dot_divider', stacking: true do - - if Feature.enabled?(:security_auto_fix) && @user.bot? - = sprite_icon('question', css_class: 'gl-text-blue-600') - = link_to @user.short_website_url, @user.full_website_url, target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url' - - if display_public_email?(@user) - = render 'middle_dot_divider', stacking: true do - = link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email' - - if @user.bio.present? - .gl-text-gray-900 - .profile-user-bio - = @user.bio + - if @user.location.present? || user_local_time.present? || work_information(@user).present? + .gl-text-gray-900 + - if @user.location.present? + = render 'middle_dot_divider', stacking: true, itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' do + = sprite_icon('location', css_class: 'fgray') + %span{ itemprop: 'addressLocality' } + = @user.location + - if user_local_time.present? + = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do + = sprite_icon('clock', css_class: 'fgray') + %span + = user_local_time + - if work_information(@user).present? + = render 'middle_dot_divider', stacking: true do + = sprite_icon('work', css_class: 'fgray') + %span + = work_information(@user, with_schema_markup: true) + .gl-text-gray-900 + - if @user.skype.present? + = render 'middle_dot_divider' do + = link_to "skype:#{@user.skype}", class: 'gl-hover-text-decoration-none', title: "Skype" do + = sprite_icon('skype', css_class: 'skype-icon') + - if @user.linkedin.present? + = render 'middle_dot_divider' do + = link_to linkedin_url(@user), class: 'gl-hover-text-decoration-none', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do + = sprite_icon('linkedin', css_class: 'linkedin-icon') + - if @user.twitter.present? + = render 'middle_dot_divider', breakpoint: 'sm' do + = link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do + = sprite_icon('twitter', css_class: 'twitter-icon') + - if @user.discord.present? + = render 'middle_dot_divider', breakpoint: 'sm' do + = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do + = sprite_icon('discord', css_class: 'discord-icon') + - if @user.website_url.present? + = render 'middle_dot_divider', stacking: true do + - if Feature.enabled?(:security_auto_fix) && @user.bot? + = sprite_icon('question', css_class: 'gl-text-blue-600') + = link_to @user.short_website_url, @user.full_website_url, target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url' + - if display_public_email?(@user) + = render 'middle_dot_divider', stacking: true do + = link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email' + - if @user.bio.present? && @user.confirmed? && !@user.blocked? + .gl-text-gray-900.gl-mx-5 + .profile-user-bio.gl-text-left + = @user.bio - if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user) .scrolling-tabs-container diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index fbb348811e0..16930548124 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -5,7 +5,7 @@ --- - :name: authorized_project_update:authorized_project_update_project_recalculate :worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :high :resource_boundary: :unknown @@ -14,7 +14,7 @@ :tags: [] - :name: authorized_project_update:authorized_project_update_project_recalculate_per_user :worker_name: AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :high :resource_boundary: :unknown @@ -23,7 +23,7 @@ :tags: [] - :name: authorized_project_update:authorized_project_update_user_refresh_from_replica :worker_name: AuthorizedProjectUpdate::UserRefreshFromReplicaWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -32,7 +32,7 @@ :tags: [] - :name: authorized_project_update:authorized_project_update_user_refresh_over_user_range :worker_name: AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -41,7 +41,7 @@ :tags: [] - :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency :worker_name: AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -617,7 +617,7 @@ :tags: [] - :name: cronjob:personal_access_tokens_expired_notification :worker_name: PersonalAccessTokens::ExpiredNotificationWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -626,7 +626,7 @@ :tags: [] - :name: cronjob:personal_access_tokens_expiring :worker_name: PersonalAccessTokens::ExpiringWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -680,7 +680,7 @@ :tags: [] - :name: cronjob:remove_expired_group_links :worker_name: RemoveExpiredGroupLinksWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -689,7 +689,7 @@ :tags: [] - :name: cronjob:remove_expired_members :worker_name: RemoveExpiredMembersWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :cpu @@ -698,7 +698,7 @@ :tags: [] - :name: cronjob:remove_unaccepted_member_invites :worker_name: RemoveUnacceptedMemberInvitesWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -2165,7 +2165,7 @@ :tags: [] - :name: unassign_issuables:members_destroyer_unassign_issuables :worker_name: MembersDestroyer::UnassignIssuablesWorker - :feature_category: :authentication_and_authorization + :feature_category: :user_management :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -2219,7 +2219,7 @@ :tags: [] - :name: authorized_projects :worker_name: AuthorizedProjectsWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :high :resource_boundary: :unknown @@ -2417,7 +2417,7 @@ :tags: [] - :name: delete_user :worker_name: DeleteUserWorker - :feature_category: :authentication_and_authorization + :feature_category: :user_management :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown @@ -2642,7 +2642,7 @@ :tags: [] - :name: groups_update_two_factor_requirement_for_members :worker_name: Groups::UpdateTwoFactorRequirementForMembersWorker - :feature_category: :authentication_and_authorization + :feature_category: :system_access :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown diff --git a/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb b/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb index 352c82e5021..96647cc671c 100644 --- a/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb +++ b/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb @@ -4,7 +4,7 @@ module AuthorizedProjectUpdate class ProjectRecalculatePerUserWorker < ProjectRecalculateWorker data_consistency :always - feature_category :authentication_and_authorization + feature_category :system_access urgency :high queue_namespace :authorized_project_update diff --git a/app/workers/authorized_project_update/project_recalculate_worker.rb b/app/workers/authorized_project_update/project_recalculate_worker.rb index 1b5faee0b6f..cbf068f0b85 100644 --- a/app/workers/authorized_project_update/project_recalculate_worker.rb +++ b/app/workers/authorized_project_update/project_recalculate_worker.rb @@ -9,7 +9,7 @@ module AuthorizedProjectUpdate prepend WaitableWorker - feature_category :authentication_and_authorization + feature_category :system_access urgency :high queue_namespace :authorized_project_update diff --git a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb index daebb23baae..cdc0a097c92 100644 --- a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb +++ b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb @@ -5,7 +5,7 @@ module AuthorizedProjectUpdate include ApplicationWorker sidekiq_options retry: 3 - feature_category :authentication_and_authorization + feature_category :system_access urgency :low data_consistency :always queue_namespace :authorized_project_update diff --git a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb index 8452f2a7821..ae243a94d3d 100644 --- a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb +++ b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb @@ -16,7 +16,7 @@ module AuthorizedProjectUpdate sidekiq_options retry: 3 - feature_category :authentication_and_authorization + feature_category :system_access urgency :low queue_namespace :authorized_project_update diff --git a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb index 7ca59a72adf..d6b41ba949c 100644 --- a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb +++ b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb @@ -2,7 +2,7 @@ module AuthorizedProjectUpdate class UserRefreshWithLowUrgencyWorker < ::AuthorizedProjectsWorker - feature_category :authentication_and_authorization + feature_category :system_access urgency :low queue_namespace :authorized_project_update deduplicate :until_executing, including_scheduled: true diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 4312ba41367..b553a2cd14e 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -8,7 +8,7 @@ class AuthorizedProjectsWorker sidekiq_options retry: 3 prepend WaitableWorker - feature_category :authentication_and_authorization + feature_category :system_access urgency :high weight 2 idempotent! diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb index 0af084caf86..bca156ff84c 100644 --- a/app/workers/delete_user_worker.rb +++ b/app/workers/delete_user_worker.rb @@ -7,7 +7,7 @@ class DeleteUserWorker # rubocop:disable Scalability/IdempotentWorker sidekiq_options retry: 3 - feature_category :authentication_and_authorization + feature_category :user_management loggable_arguments 2 def perform(current_user_id, delete_user_id, options = {}) diff --git a/app/workers/groups/update_two_factor_requirement_for_members_worker.rb b/app/workers/groups/update_two_factor_requirement_for_members_worker.rb index ac1d3589516..ca68a82ec66 100644 --- a/app/workers/groups/update_two_factor_requirement_for_members_worker.rb +++ b/app/workers/groups/update_two_factor_requirement_for_members_worker.rb @@ -9,7 +9,7 @@ module Groups idempotent! - feature_category :authentication_and_authorization + feature_category :system_access def perform(group_id) group = Group.find_by_id(group_id) diff --git a/app/workers/members_destroyer/unassign_issuables_worker.rb b/app/workers/members_destroyer/unassign_issuables_worker.rb index 915551d6e30..2e6ce0005fc 100644 --- a/app/workers/members_destroyer/unassign_issuables_worker.rb +++ b/app/workers/members_destroyer/unassign_issuables_worker.rb @@ -11,7 +11,7 @@ module MembersDestroyer ENTITY_TYPES = %w(Group Project).freeze queue_namespace :unassign_issuables - feature_category :authentication_and_authorization + feature_category :user_management idempotent! diff --git a/app/workers/personal_access_tokens/expired_notification_worker.rb b/app/workers/personal_access_tokens/expired_notification_worker.rb index b119957fa2c..f86bd604cdf 100644 --- a/app/workers/personal_access_tokens/expired_notification_worker.rb +++ b/app/workers/personal_access_tokens/expired_notification_worker.rb @@ -8,7 +8,7 @@ module PersonalAccessTokens include CronjobQueue - feature_category :authentication_and_authorization + feature_category :system_access MAX_TOKENS = 100 diff --git a/app/workers/personal_access_tokens/expiring_worker.rb b/app/workers/personal_access_tokens/expiring_worker.rb index f4afa9f8994..de0bda82573 100644 --- a/app/workers/personal_access_tokens/expiring_worker.rb +++ b/app/workers/personal_access_tokens/expiring_worker.rb @@ -8,7 +8,7 @@ module PersonalAccessTokens include CronjobQueue - feature_category :authentication_and_authorization + feature_category :system_access MAX_TOKENS = 100 diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb index 37298c53a5c..f1da5f37945 100644 --- a/app/workers/remove_expired_group_links_worker.rb +++ b/app/workers/remove_expired_group_links_worker.rb @@ -7,7 +7,7 @@ class RemoveExpiredGroupLinksWorker # rubocop:disable Scalability/IdempotentWork include CronjobQueue # rubocop:disable Scalability/CronWorkerContext - feature_category :authentication_and_authorization + feature_category :system_access def perform ProjectGroupLink.expired.find_each do |link| diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb index c9eb715a522..b5031f4cda6 100644 --- a/app/workers/remove_expired_members_worker.rb +++ b/app/workers/remove_expired_members_worker.rb @@ -7,7 +7,7 @@ class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker include CronjobQueue - feature_category :authentication_and_authorization + feature_category :system_access worker_resource_boundary :cpu # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/workers/remove_unaccepted_member_invites_worker.rb b/app/workers/remove_unaccepted_member_invites_worker.rb index 7fe45b26094..96f60b5fa12 100644 --- a/app/workers/remove_unaccepted_member_invites_worker.rb +++ b/app/workers/remove_unaccepted_member_invites_worker.rb @@ -7,7 +7,7 @@ class RemoveUnacceptedMemberInvitesWorker # rubocop:disable Scalability/Idempote include CronjobQueue # rubocop:disable Scalability/CronWorkerContext - feature_category :authentication_and_authorization + feature_category :system_access urgency :low idempotent! diff --git a/config/environments/development.rb b/config/environments/development.rb index 6b44af3b658..91184004f80 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -47,6 +47,7 @@ Rails.application.configure do config.lookbook.page_paths = ["#{config.root}/spec/components/docs"] config.lookbook.preview_params_options_eval = true config.lookbook.preview_display_options = { + layout: %w[fixed fluid], theme: ["light", "dark (alpha)"] } diff --git a/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml b/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml index d19db52074b..4fc127ebfb1 100644 --- a/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml +++ b/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml @@ -16,7 +16,7 @@ identifiers: product_section: dev product_stage: manage product_group: group::authentication and authorization -product_category: authentication_and_authorization +product_category: system_access milestone: "15.6" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100237 distributions: diff --git a/config/feature_categories.yml b/config/feature_categories.yml index 3a75ba4f2ab..1a4d6e59f2d 100644 --- a/config/feature_categories.yml +++ b/config/feature_categories.yml @@ -15,7 +15,6 @@ - application_performance - attack_emulation - audit_events -- authentication_and_authorization - auto_devops - backup_restore - billing_and_payments @@ -36,7 +35,6 @@ - continuous_delivery - continuous_integration - continuous_verification -- credential_management - customersdot_application - database - dataops @@ -95,7 +93,6 @@ - onboarding - package_registry - pages -- permissions - pipeline_composition - planning_analytics - pods diff --git a/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml b/config/feature_flags/development/blame_page_streaming.yml index 2d045e8ca06..44d64800dab 100644 --- a/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml +++ b/config/feature_flags/development/blame_page_streaming.yml @@ -1,8 +1,8 @@ --- -name: github_client_fetch_repos_via_graphql -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105824 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385649 -milestone: '15.7' +name: blame_page_streaming +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110208 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/392890 +milestone: '15.10' type: development -group: group::import -default_enabled: true +group: group::source code +default_enabled: false diff --git a/config/feature_flags/development/create_runner_workflow_for_namespace.yml b/config/feature_flags/development/create_runner_workflow_for_namespace.yml new file mode 100644 index 00000000000..783bb9803a1 --- /dev/null +++ b/config/feature_flags/development/create_runner_workflow_for_namespace.yml @@ -0,0 +1,8 @@ +--- +name: create_runner_workflow_for_namespace +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113535 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393919 +milestone: '15.10' +type: development +group: group::runner +default_enabled: false diff --git a/config/metrics/counts_28d/20210216183627_omniauth_providers.yml b/config/metrics/counts_28d/20210216183627_omniauth_providers.yml index b65141906e8..ab7e89ce449 100644 --- a/config/metrics/counts_28d/20210216183627_omniauth_providers.yml +++ b/config/metrics/counts_28d/20210216183627_omniauth_providers.yml @@ -5,7 +5,7 @@ description: List of unique OmniAuth providers product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: object status: active time_frame: 28d diff --git a/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml b/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml index 0ee3d694317..7aad160f01d 100644 --- a/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml +++ b/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml @@ -5,7 +5,7 @@ description: Number of unique user logins using two factor authentication for av product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: object status: active milestone: "14.3" diff --git a/config/metrics/counts_all/20210216180752_keys.yml b/config/metrics/counts_all/20210216180752_keys.yml index dad2a777d26..afa2559310d 100644 --- a/config/metrics/counts_all/20210216180752_keys.yml +++ b/config/metrics/counts_all/20210216180752_keys.yml @@ -5,7 +5,7 @@ description: Number of keys. product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: number status: active time_frame: all diff --git a/config/metrics/counts_all/20210216183400_omniauth_providers.yml b/config/metrics/counts_all/20210216183400_omniauth_providers.yml index f4d6e2bc57b..aa314730665 100644 --- a/config/metrics/counts_all/20210216183400_omniauth_providers.yml +++ b/config/metrics/counts_all/20210216183400_omniauth_providers.yml @@ -5,7 +5,7 @@ description: List of unique OmniAuth providers product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: object status: active time_frame: all diff --git a/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml b/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml index 98ac9ace52f..c183edf1836 100644 --- a/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml +++ b/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml @@ -5,7 +5,7 @@ description: Number of unique user logins using two factor authentication for av product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: object status: active milestone: "14.3" diff --git a/config/metrics/settings/20210204124906_ldap_enabled.yml b/config/metrics/settings/20210204124906_ldap_enabled.yml index 2c506cb40fc..d25cb1d2628 100644 --- a/config/metrics/settings/20210204124906_ldap_enabled.yml +++ b/config/metrics/settings/20210204124906_ldap_enabled.yml @@ -5,7 +5,7 @@ description: Whether LDAP is enabled product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: boolean status: active time_frame: none diff --git a/config/metrics/settings/20210204124910_omniauth_enabled.yml b/config/metrics/settings/20210204124910_omniauth_enabled.yml index 83ea666a331..0939ce43903 100644 --- a/config/metrics/settings/20210204124910_omniauth_enabled.yml +++ b/config/metrics/settings/20210204124910_omniauth_enabled.yml @@ -5,7 +5,7 @@ description: Whether OmniAuth is enabled product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: boolean status: active time_frame: none diff --git a/config/metrics/settings/20210204124918_signup_enabled.yml b/config/metrics/settings/20210204124918_signup_enabled.yml index df7f03a7d2e..9371a08613d 100644 --- a/config/metrics/settings/20210204124918_signup_enabled.yml +++ b/config/metrics/settings/20210204124918_signup_enabled.yml @@ -5,7 +5,7 @@ description: Whether public signup is enabled product_section: dev product_stage: manage product_group: authentication_and_authorization -product_category: authentication_and_authorization +product_category: system_access value_type: boolean status: active time_frame: none diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 0202eb80b23..60d3d37bdc8 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -75,6 +75,7 @@ scope format: false do get '/tree/*id', to: 'tree#show', as: :tree get '/raw/*id', to: 'raw#show', as: :raw + get '/blame_page/*id', to: 'blame#page', as: :blame_page get '/blame/*id', to: 'blame#show', as: :blame get '/commits', to: 'commits#commits_root', as: :commits_root diff --git a/db/docs/application_setting_terms.yml b/db/docs/application_setting_terms.yml index 046231b13a4..d58d4d67569 100644 --- a/db/docs/application_setting_terms.yml +++ b/db/docs/application_setting_terms.yml @@ -3,7 +3,7 @@ table_name: application_setting_terms classes: - ApplicationSetting::Term feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/cf37bef287d7dd5d2dce3e2276489767b8c0671f milestone: '10.8' diff --git a/db/docs/atlassian_identities.yml b/db/docs/atlassian_identities.yml index e43c8018d5c..e24c316c0f6 100644 --- a/db/docs/atlassian_identities.yml +++ b/db/docs/atlassian_identities.yml @@ -3,7 +3,7 @@ table_name: atlassian_identities classes: - Atlassian::Identity feature_categories: -- authentication_and_authorization +- system_access description: Stores Atlassian credentials that are used to integrate with Atlassian API introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40176 milestone: '13.4' diff --git a/db/docs/authentication_events.yml b/db/docs/authentication_events.yml index eaede3b7cd4..440ca695ad2 100644 --- a/db/docs/authentication_events.yml +++ b/db/docs/authentication_events.yml @@ -3,7 +3,7 @@ table_name: authentication_events classes: - AuthenticationEvent feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39652 milestone: '13.4' diff --git a/db/docs/banned_users.yml b/db/docs/banned_users.yml index d14b6d77234..33c5c9024cd 100644 --- a/db/docs/banned_users.yml +++ b/db/docs/banned_users.yml @@ -3,7 +3,7 @@ table_name: banned_users classes: - Users::BannedUser feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64728 milestone: '14.2' diff --git a/db/docs/group_group_links.yml b/db/docs/group_group_links.yml index f1541871795..1fa70ec02a6 100644 --- a/db/docs/group_group_links.yml +++ b/db/docs/group_group_links.yml @@ -3,7 +3,7 @@ table_name: group_group_links classes: - GroupGroupLink feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17117 milestone: '12.5' diff --git a/db/docs/identities.yml b/db/docs/identities.yml index 149907a419e..f2790c53466 100644 --- a/db/docs/identities.yml +++ b/db/docs/identities.yml @@ -3,7 +3,7 @@ table_name: identities classes: - Identity feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/1a80d13a3990937580c97e2b0ba8fb98f69bc055 milestone: '7.6' diff --git a/db/docs/ip_restrictions.yml b/db/docs/ip_restrictions.yml index 93f0da0505a..fbf90135d0a 100644 --- a/db/docs/ip_restrictions.yml +++ b/db/docs/ip_restrictions.yml @@ -3,7 +3,7 @@ table_name: ip_restrictions classes: - IpRestriction feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12669 milestone: '12.0' diff --git a/db/docs/keys.yml b/db/docs/keys.yml index 4e626b1465c..41f6786a6cc 100644 --- a/db/docs/keys.yml +++ b/db/docs/keys.yml @@ -5,7 +5,7 @@ classes: - Key - LDAPKey feature_categories: -- authentication_and_authorization +- system_access - continuous_delivery description: SSH keys used by users or for deployments. introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/9ba1224867665844b117fa037e1465bb706b3685 diff --git a/db/docs/ldap_group_links.yml b/db/docs/ldap_group_links.yml index d9a1b0acca5..74cc1a13d69 100644 --- a/db/docs/ldap_group_links.yml +++ b/db/docs/ldap_group_links.yml @@ -3,7 +3,7 @@ table_name: ldap_group_links classes: - LdapGroupLink feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/b017947ac91655f8ae6593fb63c3423cd1b439f4 milestone: '7.3' diff --git a/db/docs/namespace_admin_notes.yml b/db/docs/namespace_admin_notes.yml index 6d6710f7ee4..50ca72b270c 100644 --- a/db/docs/namespace_admin_notes.yml +++ b/db/docs/namespace_admin_notes.yml @@ -3,7 +3,7 @@ table_name: namespace_admin_notes classes: - Namespace::AdminNote feature_categories: -- authentication_and_authorization +- system_access - subgroups description: Contains notes about groups that are visible to server administrators. introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47825 diff --git a/db/docs/namespace_ldap_settings.yml b/db/docs/namespace_ldap_settings.yml index d30a835b55f..e2ebbf54fde 100644 --- a/db/docs/namespace_ldap_settings.yml +++ b/db/docs/namespace_ldap_settings.yml @@ -3,7 +3,7 @@ table_name: namespace_ldap_settings classes: - Namespaces::LdapSetting feature_categories: - - authentication_and_authorization + - system_access description: Used to store LDAP settings for namespaces introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108908 milestone: '15.10' diff --git a/db/docs/oauth_access_grants.yml b/db/docs/oauth_access_grants.yml index 197d4fc59bd..8339863cca7 100644 --- a/db/docs/oauth_access_grants.yml +++ b/db/docs/oauth_access_grants.yml @@ -4,7 +4,7 @@ classes: - Doorkeeper::AccessGrant - OauthAccessGrant feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e41dadcb33fda44ee274daa673bd933e13aa90eb milestone: '7.7' diff --git a/db/docs/oauth_access_tokens.yml b/db/docs/oauth_access_tokens.yml index f409762f483..4f68fe5b6c6 100644 --- a/db/docs/oauth_access_tokens.yml +++ b/db/docs/oauth_access_tokens.yml @@ -4,7 +4,7 @@ classes: - Doorkeeper::AccessToken - OauthAccessToken feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e41dadcb33fda44ee274daa673bd933e13aa90eb milestone: '7.7' diff --git a/db/docs/oauth_applications.yml b/db/docs/oauth_applications.yml index ac13ab3319a..e24578c3272 100644 --- a/db/docs/oauth_applications.yml +++ b/db/docs/oauth_applications.yml @@ -3,7 +3,7 @@ table_name: oauth_applications classes: - Doorkeeper::Application feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e41dadcb33fda44ee274daa673bd933e13aa90eb milestone: '7.7' diff --git a/db/docs/oauth_openid_requests.yml b/db/docs/oauth_openid_requests.yml index 011b91a758a..59de50597c3 100644 --- a/db/docs/oauth_openid_requests.yml +++ b/db/docs/oauth_openid_requests.yml @@ -3,7 +3,7 @@ table_name: oauth_openid_requests classes: - Doorkeeper::OpenidConnect::Request feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/c4982890489d254da2fe998aab30bf257767ed5e milestone: '9.0' diff --git a/db/docs/personal_access_tokens.yml b/db/docs/personal_access_tokens.yml index 8241f4234d8..2739db8371f 100644 --- a/db/docs/personal_access_tokens.yml +++ b/db/docs/personal_access_tokens.yml @@ -3,7 +3,7 @@ table_name: personal_access_tokens classes: - PersonalAccessToken feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/3a609038748055a27c7e01cf4b55d8249709c9cc milestone: '8.9' diff --git a/db/docs/project_access_tokens.yml b/db/docs/project_access_tokens.yml index ddaca744571..3c19e4dc19f 100644 --- a/db/docs/project_access_tokens.yml +++ b/db/docs/project_access_tokens.yml @@ -2,7 +2,7 @@ table_name: project_access_tokens classes: [] feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33272 milestone: '13.1' diff --git a/db/docs/project_authorizations.yml b/db/docs/project_authorizations.yml index b37634047f0..b81235d4aac 100644 --- a/db/docs/project_authorizations.yml +++ b/db/docs/project_authorizations.yml @@ -4,7 +4,7 @@ classes: - ProjectAuthorization feature_categories: - projects -- authentication_and_authorization +- system_access description: Stores maximal access to the project per user introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6839 milestone: '8.14' diff --git a/db/docs/project_group_links.yml b/db/docs/project_group_links.yml index c03141058b6..aa981adb745 100644 --- a/db/docs/project_group_links.yml +++ b/db/docs/project_group_links.yml @@ -3,7 +3,7 @@ table_name: project_group_links classes: - ProjectGroupLink feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/3ac5a759e93e632539438d4564582c645a9f6799 milestone: "<6.0" diff --git a/db/docs/saml_group_links.yml b/db/docs/saml_group_links.yml index 5fd2372a22d..4dfb33e37a5 100644 --- a/db/docs/saml_group_links.yml +++ b/db/docs/saml_group_links.yml @@ -3,7 +3,7 @@ table_name: saml_group_links classes: - SamlGroupLink feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45061 milestone: '13.5' diff --git a/db/docs/saml_providers.yml b/db/docs/saml_providers.yml index 6fcc0e0e370..21ef2ed3a26 100644 --- a/db/docs/saml_providers.yml +++ b/db/docs/saml_providers.yml @@ -3,7 +3,7 @@ table_name: saml_providers classes: - SamlProvider feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4549 milestone: '10.7' diff --git a/db/docs/scim_identities.yml b/db/docs/scim_identities.yml index 6ad69d9b4cc..16fec8da041 100644 --- a/db/docs/scim_identities.yml +++ b/db/docs/scim_identities.yml @@ -3,7 +3,7 @@ table_name: scim_identities classes: - ScimIdentity feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26124 milestone: '12.9' diff --git a/db/docs/scim_oauth_access_tokens.yml b/db/docs/scim_oauth_access_tokens.yml index e26cd94f4cd..addd4c49ed5 100644 --- a/db/docs/scim_oauth_access_tokens.yml +++ b/db/docs/scim_oauth_access_tokens.yml @@ -3,7 +3,7 @@ table_name: scim_oauth_access_tokens classes: - ScimOauthAccessToken feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e9b2253fe3538234d1c4d173c4549a955233d836 milestone: '11.10' diff --git a/db/docs/smartcard_identities.yml b/db/docs/smartcard_identities.yml index 76b8d1a1368..905811768c1 100644 --- a/db/docs/smartcard_identities.yml +++ b/db/docs/smartcard_identities.yml @@ -3,7 +3,7 @@ table_name: smartcard_identities classes: - SmartcardIdentity feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/b6316689fdc2d142af85b17d511d39e50712b420 milestone: '11.6' diff --git a/db/docs/term_agreements.yml b/db/docs/term_agreements.yml index 502adad8ac0..bc2abea809e 100644 --- a/db/docs/term_agreements.yml +++ b/db/docs/term_agreements.yml @@ -3,7 +3,7 @@ table_name: term_agreements classes: - TermAgreement feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/82eeb72c8c03727540b902d40e7e657d0a5ecb4c milestone: '10.8' diff --git a/db/docs/token_with_ivs.yml b/db/docs/token_with_ivs.yml index 2acdff0dad1..521e26baac0 100644 --- a/db/docs/token_with_ivs.yml +++ b/db/docs/token_with_ivs.yml @@ -3,7 +3,7 @@ table_name: token_with_ivs classes: - TokenWithIv feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/37b80b4048190c2e1a35ec399e4aeb35d511090e milestone: '13.9' diff --git a/db/docs/u2f_registrations.yml b/db/docs/u2f_registrations.yml index 27b0ca3f2f5..b1aaa8148bd 100644 --- a/db/docs/u2f_registrations.yml +++ b/db/docs/u2f_registrations.yml @@ -3,7 +3,7 @@ table_name: u2f_registrations classes: - U2fRegistration feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/791cc9138be6ea1783e3c3853370cf0290f4d41e milestone: '8.9' diff --git a/db/docs/user_canonical_emails.yml b/db/docs/user_canonical_emails.yml index aeb1c3d830f..df3240b52fa 100644 --- a/db/docs/user_canonical_emails.yml +++ b/db/docs/user_canonical_emails.yml @@ -3,7 +3,7 @@ table_name: user_canonical_emails classes: - UserCanonicalEmail feature_categories: -- authentication_and_authorization +- system_access description: stores the canonical version of user's primary email address introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27722 milestone: '13.0' diff --git a/db/docs/user_highest_roles.yml b/db/docs/user_highest_roles.yml index cc12e3080ff..cfe4c2e5ce0 100644 --- a/db/docs/user_highest_roles.yml +++ b/db/docs/user_highest_roles.yml @@ -3,7 +3,7 @@ table_name: user_highest_roles classes: - UserHighestRole feature_categories: -- authentication_and_authorization +- system_access description: Stores highest role per User they have in a Group or a Project. If a User has an open invite or pending access request or no membership the highest role will be set to nil. introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26987 milestone: '12.9' diff --git a/db/docs/user_permission_export_uploads.yml b/db/docs/user_permission_export_uploads.yml index 217ede5bad2..fe76f1fa618 100644 --- a/db/docs/user_permission_export_uploads.yml +++ b/db/docs/user_permission_export_uploads.yml @@ -3,7 +3,7 @@ table_name: user_permission_export_uploads classes: - UserPermissionExportUpload feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47846 milestone: '13.7' diff --git a/db/docs/user_synced_attributes_metadata.yml b/db/docs/user_synced_attributes_metadata.yml index efc0ad1ec95..a2162c071c9 100644 --- a/db/docs/user_synced_attributes_metadata.yml +++ b/db/docs/user_synced_attributes_metadata.yml @@ -3,7 +3,7 @@ table_name: user_synced_attributes_metadata classes: - UserSyncedAttributesMetadata feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/4df54f260751a832ebf0b8c18524020d6604994b milestone: '10.0' diff --git a/db/docs/webauthn_registrations.yml b/db/docs/webauthn_registrations.yml index fc983ea60ca..1ec27e1bb3b 100644 --- a/db/docs/webauthn_registrations.yml +++ b/db/docs/webauthn_registrations.yml @@ -3,7 +3,7 @@ table_name: webauthn_registrations classes: - WebauthnRegistration feature_categories: -- authentication_and_authorization +- system_access description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35797 milestone: '13.2' diff --git a/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb b/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb index 583a9cd31f7..bcb1147605e 100644 --- a/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb +++ b/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true class ScheduleFkValidationForPCiBuildsMetadataPartitionsAndCiBuilds < Gitlab::Database::Migration[2.1] - TABLE_NAME = :p_ci_builds_metadata - FK_NAME = :fk_e20479742e_p + # This migration was used to validate the foreign keys on partitions introduced by + # db/post_migrate/20230221125148_add_fk_to_p_ci_builds_metadata_partitions_on_partition_id_and_build_id.rb + # but executing the rollback of + # db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb + # would also remove the FKs on partitions and this would errors out. def up - prepare_partitioned_async_foreign_key_validation TABLE_NAME, name: FK_NAME + # No-op end def down - unprepare_partitioned_async_foreign_key_validation TABLE_NAME, name: FK_NAME + # No-op end end diff --git a/db/post_migrate/20230306071456_validate_partitioning_fk_on_p_ci_builds_metadata_partitions.rb b/db/post_migrate/20230306071456_validate_partitioning_fk_on_p_ci_builds_metadata_partitions.rb new file mode 100644 index 00000000000..f07175e82f9 --- /dev/null +++ b/db/post_migrate/20230306071456_validate_partitioning_fk_on_p_ci_builds_metadata_partitions.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ValidatePartitioningFkOnPCiBuildsMetadataPartitions < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :p_ci_builds_metadata + FK_NAME = :fk_e20479742e_p + + def up + Gitlab::Database::PostgresPartitionedTable.each_partition(SOURCE_TABLE_NAME) do |partition| + next unless foreign_key_exists?(partition.identifier, name: FK_NAME) + + validate_foreign_key(partition.identifier, nil, name: FK_NAME) + end + end + + def down + # No-op + end +end diff --git a/db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb b/db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb new file mode 100644 index 00000000000..9328e3ff00e --- /dev/null +++ b/db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class AddPartitionedFkToPCiBuildsMetadataOnPartitionIdAndBuildId < Gitlab::Database::Migration[2.1] + SOURCE_TABLE_NAME = :p_ci_builds_metadata + TARGET_TABLE_NAME = :ci_builds + FK_NAME = :fk_e20479742e_p + + disable_ddl_transaction! + + def up + return if foreign_key_exists?(SOURCE_TABLE_NAME, TARGET_TABLE_NAME, name: FK_NAME) + + with_lock_retries do + execute("LOCK TABLE #{TARGET_TABLE_NAME}, #{SOURCE_TABLE_NAME} IN SHARE ROW EXCLUSIVE MODE") + + execute(<<~SQL.squish) + ALTER TABLE #{SOURCE_TABLE_NAME} + ADD CONSTRAINT #{FK_NAME} + FOREIGN KEY (partition_id, build_id) + REFERENCES #{TARGET_TABLE_NAME} (partition_id, id) + ON UPDATE CASCADE ON DELETE CASCADE; + SQL + end + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end +end diff --git a/db/post_migrate/20230306082852_remove_fk_to_ci_builds_p_ci_builds_metadata_on_build_id.rb b/db/post_migrate/20230306082852_remove_fk_to_ci_builds_p_ci_builds_metadata_on_build_id.rb new file mode 100644 index 00000000000..108a92aec3b --- /dev/null +++ b/db/post_migrate/20230306082852_remove_fk_to_ci_builds_p_ci_builds_metadata_on_build_id.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class RemoveFkToCiBuildsPCiBuildsMetadataOnBuildId < Gitlab::Database::Migration[2.1] + include Gitlab::Database::PartitioningMigrationHelpers + + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :p_ci_builds_metadata + TARGET_TABLE_NAME = :ci_builds + FK_NAME = :fk_e20479742e + + def up + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end + + def down + add_concurrent_partitioned_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: :build_id, + on_delete: :cascade, + name: FK_NAME + ) + end +end diff --git a/db/schema_migrations/20230306071456 b/db/schema_migrations/20230306071456 new file mode 100644 index 00000000000..b4ac086f125 --- /dev/null +++ b/db/schema_migrations/20230306071456 @@ -0,0 +1 @@ +7f431d6dd4f9dc237623c18465995fa59c9902187f433375baa03194f7a6b88f
\ No newline at end of file diff --git a/db/schema_migrations/20230306072532 b/db/schema_migrations/20230306072532 new file mode 100644 index 00000000000..f1604aa84a7 --- /dev/null +++ b/db/schema_migrations/20230306072532 @@ -0,0 +1 @@ +f6613d1fd3b99fa0e8ea059c6d53e8d226ce3fd8c07e44a024b065d8d110876f
\ No newline at end of file diff --git a/db/schema_migrations/20230306082852 b/db/schema_migrations/20230306082852 new file mode 100644 index 00000000000..bbbe7cb27ef --- /dev/null +++ b/db/schema_migrations/20230306082852 @@ -0,0 +1 @@ +580efa96f235c47de1bcea172544e51e8207dd0a81bd888567b30ce02e453f7d
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 64beb9476f9..8341c91b5e6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -34838,10 +34838,7 @@ ALTER TABLE ONLY ci_sources_pipelines ADD CONSTRAINT fk_e1bad85861 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE; ALTER TABLE p_ci_builds_metadata - ADD CONSTRAINT fk_e20479742e FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE; - -ALTER TABLE ONLY ci_builds_metadata - ADD CONSTRAINT fk_e20479742e_p FOREIGN KEY (partition_id, build_id) REFERENCES ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; + ADD CONSTRAINT fk_e20479742e_p FOREIGN KEY (partition_id, build_id) REFERENCES ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY gitlab_subscriptions ADD CONSTRAINT fk_e2595d00a1 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md index 22b8cc13637..926500090dc 100644 --- a/doc/administration/dedicated/index.md +++ b/doc/administration/dedicated/index.md @@ -97,7 +97,7 @@ Prerequisites: To activate SAML for your GitLab Dedicated instance: -1. Read the [GitLab documentation about SAML](../../integration/saml.md#https://docs.gitlab.com/ee/integration/saml.html#configure-saml-on-your-idp) to gather all data your identity provider requires for configuration. You can also find some providers and their requirements in the [group SAML documentation](../../user/group/saml_sso/index.md#providers). +1. Read the [GitLab documentation about SAML](../../integration/saml.md#https://docs.gitlab.com/ee/integration/saml.html#configure-saml-on-your-idp) to gather all data your identity provider requires for configuration. You can also find some providers and their requirements in the [group SAML documentation](../../user/group/saml_sso/index.md#set-up-identity-provider). 1. To make the necessary changes, include in your [support ticket](#configuration-changes) the desired [SAML configuration block](../../integration/saml.md#configure-saml-support-in-gitlab) that will be set on the GitLab application. At a minimum, GitLab needs the following information to enable SAML for your instance: - Assertion consumer service URL - Certificate fingerprint or certificate diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1ce6c1784a2..701070b11d7 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15073,6 +15073,20 @@ Exposes aggregated value stream flow metrics. #### Fields with arguments +##### `GroupValueStreamAnalyticsFlowMetrics.deploymentCount` + +Number of production deployments in the given period. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | Deployments finished after the date. | +| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. | +| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Deployments finished before the date. | + ##### `GroupValueStreamAnalyticsFlowMetrics.issueCount` Number of issues opened in the given period. @@ -19509,6 +19523,19 @@ Exposes aggregated value stream flow metrics. #### Fields with arguments +##### `ProjectValueStreamAnalyticsFlowMetrics.deploymentCount` + +Number of production deployments in the given period. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | Deployments finished after the date. | +| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Deployments finished before the date. | + ##### `ProjectValueStreamAnalyticsFlowMetrics.issueCount` Number of issues opened in the given period. diff --git a/doc/api/keys.md b/doc/api/keys.md index e7bdc70017c..90144310238 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -7,6 +7,8 @@ type: reference, api # Keys API **(FREE)** +If using a SHA256 fingerprint in an API call, you should URL-encode the fingerprint. + ## Get SSH key with user by ID of an SSH key Get SSH key with user by ID of an SSH key. Note only administrators can lookup SSH key with user by ID of an SSH key. @@ -22,7 +24,8 @@ GET /keys/:id Example request: ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys/1" +curl --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/keys/1" ``` ```json @@ -82,15 +85,8 @@ GET /keys Example request: ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys?fingerprint=ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1" -``` - -If using sha256 fingerprint API calls, make sure that the fingerprint is URL-encoded. - -For example, `/` is represented by `%2F` and `:` is represented by`%3A`: - -```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys?fingerprint=SHA256%3AnUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo%2FlCg" +curl --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/keys?fingerprint=ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1" ``` Example response: @@ -142,15 +138,21 @@ Example response: ## Get user by deploy key fingerprint -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/119209) in GitLab 12.7. +Deploy keys are bound to the creating user. If you query with a deploy key +fingerprint, you get additional information about the projects using that key. -Deploy keys are bound to the creating user, so if you query with a deploy key -fingerprint you get additional information about the projects using that key. +Example request with an MD5 fingerprint: -Example request: +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/keys?fingerprint=ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1" +``` + +In this SHA256 example, `/` is represented by `%2F` and `:` is represented by`%3A`: ```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys?fingerprint=SHA256%3AnUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo%2FlCg" +curl --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/keys?fingerprint=SHA256%3AnUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo%2FlCg" ``` Example response: diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index cbf7ea079bc..25c26ee9339 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -100,9 +100,9 @@ Owners also see the `plan` property associated with a namespace: ] ``` -Users on GitLab.com also see `max_seats_used` and `seats_in_use` parameters. +Users on GitLab.com also see `max_seats_used`, `seats_in_use` and `max_seats_used_changed_at` parameters. `max_seats_used` is the highest number of users the group had. `seats_in_use` is -the number of license seats currently being used. Both values are updated +the number of license seats currently being used. `max_seats_used_changed_at` shows the date when the `max_seats_used` value changed. All the values are updated once a day. `max_seats_used` and `seats_in_use` are non-zero only for namespaces on paid plans. @@ -114,6 +114,7 @@ once a day. "name": "user1", "billable_members_count": 2, "max_seats_used": 3, + "max_seats_used_changed_at":"2023-02-13T12:00:02.000Z", "seats_in_use": 2, ... } diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md index c6e1e143e96..b5be1689785 100644 --- a/doc/architecture/blueprints/runner_tokens/index.md +++ b/doc/architecture/blueprints/runner_tokens/index.md @@ -393,7 +393,7 @@ scope. | GitLab Rails app | `%15.9` | Implement new GraphQL user-authenticated API to create a new runner. | | GitLab Rails app | `%15.10` | Return token and runner ID information from `/runners/verify` REST endpoint. | | GitLab Runner | `%15.10` | [Modify register command to allow new flow with glrt- prefixed authentication tokens](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29613). | -| GitLab Rails app | `%15.11` | Define feature flag and policies for "New Runner creation workflow" for groups and projects. | +| GitLab Rails app | `%15.10` | Define feature flag and policies for "New Runner creation workflow" for groups and projects. | | GitLab Rails app | `%15.11` | Update service and mutation to accept groups and projects. | | GitLab Rails app | `%15.11` | Implement UI to create new runner. | | GitLab Rails app | `%15.11` | GraphQL changes to `CiRunner` type. (?) | diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index e80e2caf636..5dfad57facf 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -8,17 +8,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w > Introduced in GitLab 11.0. -This page describes SAML for groups. For instance-wide SAML on self-managed GitLab instances, see [SAML SSO for self-managed GitLab instances](../../../integration/saml.md). -[View the differences between SaaS and Self-Managed Authentication and Authorization Options](../../../administration/auth/index.md#saas-vs-self-managed-comparison). +Users can sign in to GitLab through their SAML identity provider. -SAML on GitLab.com allows users to sign in through their SAML identity provider. If the user is not already a member, the sign-in process automatically adds the user to the appropriate group. +[SCIM](scim_setup.md) synchronizes users with the group on GitLab.com. -User synchronization of SAML SSO groups is supported through [SCIM](scim_setup.md). SCIM supports adding and removing users from the GitLab group automatically. -For example, if you remove a user from the SCIM app, SCIM removes that same user from the GitLab group. +- When you add or remove a user from the SCIM app, SCIM adds or removes the user + from the GitLab group. +- If the user is not already a group member, the user is added to the group as part of the sign-in process. -SAML SSO is only configurable at the top-level group. - -If required, you can find [a glossary of common terms](../../../integration/saml.md#glossary-of-common-terms). +You can configure SAML SSO for the top-level group only. ## Configure your identity provider @@ -28,7 +26,7 @@ If required, you can find [a glossary of common terms](../../../integration/saml 1. Note the **Assertion consumer service URL**, **Identifier**, and **GitLab single sign-on URL**. 1. Configure your SAML identity provider app using the noted details. Alternatively, GitLab provides a [metadata XML configuration](#metadata-configuration). - See [specific identity provider documentation](#providers) for more details. + See [specific identity provider documentation](#set-up-identity-provider) for more details. 1. Configure the SAML response to include a [NameID](#nameid) that uniquely identifies each user. 1. Configure the required [user attributes](#user-attributes), ensuring you include the user's email address. 1. While the default is enabled for most SAML providers, ensure the app is set to have service provider @@ -40,6 +38,121 @@ If required, you can find [a glossary of common terms](../../../integration/saml If your account is the only owner in the group after SAML is set up, you can't unlink the account. To [unlink the account](#unlinking-accounts), set up another user as a group owner. +## Set up identity provider + +The SAML standard means that you can use a wide range of identity providers with GitLab. Your identity provider might have relevant documentation. It can be generic SAML documentation or specifically targeted for GitLab. + +When [configuring your identity provider](#configure-your-identity-provider), consider the notes below for specific providers to help avoid common issues and as a guide for terminology used. + +For providers not listed below, you can refer to the [instance SAML notes on configuring an identity provider](../../../integration/saml.md#configure-saml-on-your-idp) +for additional guidance on information your identity provider may require. + +GitLab provides the following information for guidance only. +If you have any questions on configuring the SAML app, contact your provider's support. + +### Set up Azure + +Follow the Azure documentation on [configuring single sign-on to applications](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-sso), and use the following notes when needed. + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azure Using SAML SSO for Groups Demo](https://youtu.be/24-ZxmTeEBU). +The video is outdated in regard to objectID mapping and you should follow the [SCIM documentation](scim_setup.md#configure-azure-active-directory). + +| GitLab Setting | Azure Field | +| ------------------------------------ | ------------------------------------------ | +| Identifier | Identifier (Entity ID) | +| Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) | +| GitLab single sign-on URL | Sign on URL | +| Identity provider single sign-on URL | Login URL | +| Certificate fingerprint | Thumbprint | + +You should set the following attributes: + +- **Unique User Identifier (Name identifier)** to `user.objectID`. +- **nameid-format** to persistent. +- Additional claims to [supported attributes](#user-attributes). + +If using [Group Sync](#group-sync), customize the name of the group claim to match the required attribute. + +See our [example configuration page](example_saml_config.md#azure-active-directory). + +### Set up Google Workspace + +1. [Set up SSO with Google as your identity provider](https://support.google.com/a/answer/6087519?hl=en). + The following GitLab settings correspond to the Google Workspace fields. + + | GitLab setting | Google Workspace field | + |:-------------------------------------|:-----------------------| + | Identifier | **Entity ID** | + | Assertion consumer service URL | **ACS URL** | + | GitLab single sign-on URL | **Start URL** | + | Identity provider single sign-on URL | **SSO URL** | + +1. Google Workspace displays a SHA256 fingerprint. To retrieve the SHA1 fingerprint + required by GitLab to [configure SAML](#configure-gitlab): + 1. Download the certificate. + 1. Run this command: + + ```shell + openssl x509 -noout -fingerprint -sha1 -inform pem -in "GoogleIDPCertificate-domain.com.pem" + ``` + +1. Set these values: + - For **Primary email**: `email` + - For **First name**: `first_name` + - For **Last name**: `last_name` + - For **Name ID format**: `EMAIL` + - For **NameID**: `Basic Information > Primary email` + +On the GitLab SAML SSO page, when you select **Verify SAML Configuration**, disregard +the warning that recommends setting the **NameID** format to `persistent`. + +For details, see the [example configuration page](example_saml_config.md#google-workspace). + +### Set up Okta + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & SCIM setup](https://youtu.be/0ES9HsZq0AQ). + +1. [Set up a SAML application in Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/main/). + The following GitLab settings correspond to the Okta fields. + + | GitLab setting | Okta field | + | ------------------------------------ | ---------------------------------------------------------- | + | Identifier | **Audience URI** | + | Assertion consumer service URL | **Single sign-on URL** | + | GitLab single sign-on URL | **Login page URL** (under **Application Login Page** settings) | + | Identity provider single sign-on URL | **Identity Provider Single Sign-On URL** | + +1. Under the Okta **Single sign-on URL** field, select the **Use this for Recipient URL and Destination URL** checkbox. + +1. Set these values: + - For **Application username (NameID)**: **Custom** `user.getInternalProperty("id")` + - For **Name ID Format**: `Persistent` + +The Okta GitLab application available in the App Catalog only supports [SCIM](scim_setup.md). Support +for SAML is proposed in [issue 216173](https://gitlab.com/gitlab-org/gitlab/-/issues/216173). + +### Set up OneLogin + +OneLogin supports its own [GitLab (SaaS) application](https://onelogin.service-now.com/support?id=kb_article&sys_id=92e4160adbf16cd0ca1c400e0b961923&kb_category=50984e84db738300d5505eea4b961913). + +1. If you use the OneLogin generic + [SAML Test Connector (Advanced)](https://onelogin.service-now.com/support?id=kb_article&sys_id=b2c19353dbde7b8024c780c74b9619fb&kb_category=93e869b0db185340d5505eea4b961934), + you should [use the OneLogin SAML Test Connector](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f). The following GitLab settings correspond + to the OneLogin fields: + + | GitLab setting | OneLogin field | + | ------------------------------------------------ | -------------------------------- | + | Identifier | **Audience** | + | Assertion consumer service URL | **Recipient** | + | Assertion consumer service URL | **ACS (Consumer) URL** | + | Assertion consumer service URL (escaped version) | **ACS (Consumer) URL Validator** | + | GitLab single sign-on URL | **Login URL** | + | Identity provider single sign-on URL | **SAML 2.0 Endpoint** | + +1. For **NameID**, use `OneLogin ID`. + ### NameID GitLab.com uses the SAML NameID to identify users. The NameID element: @@ -52,7 +165,7 @@ GitLab.com uses the SAML NameID to identify users. The NameID element: guarantee it doesn't ever change, for example, when a person's name changes. Email addresses are also case-insensitive, which can result in users being unable to sign in. -The relevant field name and recommended value for supported providers are in the [provider specific notes](#providers). +The relevant field name and recommended value for supported providers are in the [provider specific notes](#set-up-identity-provider). WARNING: Once users have signed into GitLab using the SSO SAML setup, changing the `NameID` breaks the configuration and potentially locks users out of the GitLab group. @@ -201,121 +314,7 @@ immediately. If the user: - Is signed out, they cannot access the group after being removed from the identity provider. -## Providers - -The SAML standard means that you can use a wide range of identity providers with GitLab. Your identity provider might have relevant documentation. It can be generic SAML documentation or specifically targeted for GitLab. - -When [configuring your identity provider](#configure-your-identity-provider), consider the notes below for specific providers to help avoid common issues and as a guide for terminology used. - -For providers not listed below, you can refer to the [instance SAML notes on configuring an identity provider](../../../integration/saml.md#configure-saml-on-your-idp) -for additional guidance on information your identity provider may require. - -GitLab provides the following information for guidance only. -If you have any questions on configuring the SAML app, contact your provider's support. - -### Set up Azure - -1. [Use Azure to configure SSO for an application](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-sso). The following GitLab settings correspond to the Azure fields. - - | GitLab setting | Azure field | - | ------------------------------------ | ------------------------------------------ | - | Identifier | Identifier (Entity ID) | - | Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) | - | GitLab single sign-on URL | Sign on URL | - | Identity provider single sign-on URL | Login URL | - | Certificate fingerprint | Thumbprint | - -1. Set the following attributes: - - **Unique User Identifier (Name identifier)** to `user.objectID`. - - **nameid-format** to persistent. - - **Additional claims** to [supported attributes](#user-attributes). - -1. Optional. If you use [Group Sync](group_sync.md), customize the name of the group - claim to match the required attribute. - -<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -View a demo of [SCIM provisioning on Azure using SAML SSO for groups](https://youtu.be/24-ZxmTeEBU). The `objectID` mapping is outdated in this video. Follow the [SCIM documentation](scim_setup.md#configure-azure-active-directory) instead. - -View an [example configuration page](example_saml_config.md#azure-active-directory). - -### Set up Google Workspace - -1. [Set up SSO with Google as your identity provider](https://support.google.com/a/answer/6087519?hl=en). - The following GitLab settings correspond to the Google Workspace fields. - - | GitLab setting | Google Workspace field | - |:-------------------------------------|:-----------------------| - | Identifier | **Entity ID** | - | Assertion consumer service URL | **ACS URL** | - | GitLab single sign-on URL | **Start URL** | - | Identity provider single sign-on URL | **SSO URL** | - -1. Google Workspace displays a SHA256 fingerprint. To retrieve the SHA1 fingerprint - required by GitLab to [configure SAML](#configure-gitlab): - 1. Download the certificate. - 1. Run this command: - - ```shell - openssl x509 -noout -fingerprint -sha1 -inform pem -in "GoogleIDPCertificate-domain.com.pem" - ``` - -1. Set these values: - - For **Primary email**: `email` - - For **First name**: `first_name` - - For **Last name**: `last_name` - - For **Name ID format**: `EMAIL` - - For **NameID**: `Basic Information > Primary email` - -On the GitLab SAML SSO page, when you select **Verify SAML Configuration**, disregard -the warning that recommends setting the **NameID** format to `persistent`. - -For details, see the [example configuration page](example_saml_config.md#google-workspace). - -### Set up Okta - -<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & SCIM setup](https://youtu.be/0ES9HsZq0AQ). - -1. [Set up a SAML application in Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/main/). - The following GitLab settings correspond to the Okta fields. - - | GitLab setting | Okta field | - | ------------------------------------ | ---------------------------------------------------------- | - | Identifier | **Audience URI** | - | Assertion consumer service URL | **Single sign-on URL** | - | GitLab single sign-on URL | **Login page URL** (under **Application Login Page** settings) | - | Identity provider single sign-on URL | **Identity Provider Single Sign-On URL** | - -1. Under the Okta **Single sign-on URL** field, select the **Use this for Recipient URL and Destination URL** checkbox. - -1. Set these values: - - For **Application username (NameID)**: **Custom** `user.getInternalProperty("id")` - - For **Name ID Format**: `Persistent` - -The Okta GitLab application available in the App Catalog only supports [SCIM](scim_setup.md). Support -for SAML is proposed in [issue 216173](https://gitlab.com/gitlab-org/gitlab/-/issues/216173). - -### Set up OneLogin - -OneLogin supports its own [GitLab (SaaS) application](https://onelogin.service-now.com/support?id=kb_article&sys_id=92e4160adbf16cd0ca1c400e0b961923&kb_category=50984e84db738300d5505eea4b961913). - -1. If you use the OneLogin generic - [SAML Test Connector (Advanced)](https://onelogin.service-now.com/support?id=kb_article&sys_id=b2c19353dbde7b8024c780c74b9619fb&kb_category=93e869b0db185340d5505eea4b961934), - you should [use the OneLogin SAML Test Connector](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f). The following GitLab settings correspond - to the OneLogin fields: - - | GitLab setting | OneLogin field | - | ------------------------------------------------ | -------------------------------- | - | Identifier | **Audience** | - | Assertion consumer service URL | **Recipient** | - | Assertion consumer service URL | **ACS (Consumer) URL** | - | Assertion consumer service URL (escaped version) | **ACS (Consumer) URL Validator** | - | GitLab single sign-on URL | **Login URL** | - | Identity provider single sign-on URL | **SAML 2.0 Endpoint** | - -1. For **NameID**, use `OneLogin ID`. - -## Manage your identity provider +### Change the SAML app After you have configured your identity provider, you can: @@ -482,7 +481,7 @@ To rescind a user's access to the group when only SAML SSO is configured, either - Remove (in order) the user from: 1. The user data store on the identity provider or the list of users on the specific app. 1. The GitLab.com group. -- Use Group Sync at the top-level of your group to [automatically remove the user](group_sync.md#automatic-member-removal). +- Use [Group Sync](group_sync.md#automatic-member-removal) at the top-level of your group with the [default role](#role) set to [minimal access](../../permissions.md#users-with-minimal-access) to automatically block access to all resources within the group. Users may continue to [use a seat](../../permissions.md#minimal-access-users-take-license-seats). To rescind a user's access to the group when also using SCIM, refer to [Remove access](scim_setup.md#remove-access). @@ -516,6 +515,17 @@ For information on automatically managing GitLab group membership, see [SAML Gro The [Generated passwords for users created through integrated authentication](../../../security/passwords_for_integrated_authentication_methods.md) guide provides an overview of how GitLab generates and sets passwords for users created via SAML SSO for Groups. +## Related topics + +For more information on: + +- Setting up SAML on self-managed GitLab instances, see + [SAML SSO for self-managed GitLab instances](../../../integration/saml.md). +- Commonly-used terms, see the + [glossary of common terms](../../../integration/saml.md#glossary-of-common-terms). +- The differences between SaaS and self-managed authentication and authorization, + see the [SaaS vs. Self-Managed comparison](../../../administration/auth/index.md#saas-vs-self-managed-comparison). + ## Troubleshooting See our [troubleshooting SAML guide](troubleshooting.md). diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md index 9e6e9984c15..e6675b4eff2 100644 --- a/doc/user/profile/notifications.md +++ b/doc/user/profile/notifications.md @@ -286,7 +286,8 @@ To always receive notifications on your own issues, merge requests, and so on, t ## Notifications for unknown sign-ins -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0. +> - Listing the full name and username of the signed-in user [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225183) in GitLab 15.10. NOTE: This feature is enabled by default for self-managed instances. Administrators may disable this feature @@ -295,7 +296,12 @@ The feature is always enabled on GitLab.com. When a user successfully signs in from a previously unknown IP address or device, GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially -malicious or unauthorized sign-ins. +malicious or unauthorized sign-ins. This notification email includes the: + +- Hostname. +- User's name and username. +- IP address. +- Date and time of sign-in. GitLab uses several methods to identify a known sign-in. All methods must fail for a notification email to be sent. diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 38a9856ca58..249301f308c 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -8,7 +8,7 @@ module API helpers ::API::Helpers::MembersHelpers - feature_category :authentication_and_authorization + feature_category :system_access %w[group project].each do |source_type| params do diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 6fc9408a570..39f1638301b 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -5,7 +5,7 @@ module API class Applications < ::API::Base before { authenticated_as_admin! } - feature_category :authentication_and_authorization + feature_category :system_access resource :applications do desc 'Create a new application' do diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 3f6e052f7b6..2a5ff257718 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -195,7 +195,7 @@ module API # # Discover user by ssh key, user id or username # - get '/discover', feature_category: :authentication_and_authorization do + get '/discover', feature_category: :system_access do present actor.user, with: Entities::UserSafe end @@ -208,7 +208,7 @@ module API } end - post '/two_factor_recovery_codes', feature_category: :authentication_and_authorization do + post '/two_factor_recovery_codes', feature_category: :system_access do status 200 actor.update_last_used_at! @@ -237,7 +237,7 @@ module API { success: true, recovery_codes: codes } end - post '/personal_access_token', feature_category: :authentication_and_authorization do + post '/personal_access_token', feature_category: :system_access do status 200 actor.update_last_used_at! @@ -308,7 +308,7 @@ module API # decided to pursue a different approach, so it's currently not used. # We might revive the PAM module though as it provides better user # flow. - post '/two_factor_config', feature_category: :authentication_and_authorization do + post '/two_factor_config', feature_category: :system_access do status 200 break { success: false } unless Feature.enabled?(:two_factor_for_cli) @@ -330,13 +330,13 @@ module API end end - post '/two_factor_push_otp_check', feature_category: :authentication_and_authorization do + post '/two_factor_push_otp_check', feature_category: :system_access do status 200 two_factor_push_otp_check end - post '/two_factor_manual_otp_check', feature_category: :authentication_and_authorization do + post '/two_factor_manual_otp_check', feature_category: :system_access do status 200 two_factor_manual_otp_check diff --git a/lib/api/keys.rb b/lib/api/keys.rb index 77952bac01a..c711b3d9c19 100644 --- a/lib/api/keys.rb +++ b/lib/api/keys.rb @@ -5,7 +5,7 @@ module API class Keys < ::API::Base before { authenticate! } - feature_category :authentication_and_authorization + feature_category :system_access resource :keys do desc 'Get single ssh key by id. Only available to admin users' do diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb index 66930ecd797..e588eb17720 100644 --- a/lib/api/personal_access_tokens.rb +++ b/lib/api/personal_access_tokens.rb @@ -4,7 +4,7 @@ module API class PersonalAccessTokens < ::API::Base include ::API::PaginationParams - feature_category :authentication_and_authorization + feature_category :system_access before do authenticate! diff --git a/lib/api/personal_access_tokens/self_information.rb b/lib/api/personal_access_tokens/self_information.rb index 5735fe49f33..4f17ca955ac 100644 --- a/lib/api/personal_access_tokens/self_information.rb +++ b/lib/api/personal_access_tokens/self_information.rb @@ -5,7 +5,7 @@ module API class SelfInformation < ::API::Base include APIGuard - feature_category :authentication_and_authorization + feature_category :system_access helpers ::API::Helpers::PersonalAccessTokensHelpers diff --git a/lib/api/projects.rb b/lib/api/projects.rb index fbe30aad120..b39444f642f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -709,7 +709,7 @@ module API requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level' optional :expires_at, type: Date, desc: 'Share expiration date' end - post ":id/share", feature_category: :authentication_and_authorization do + post ":id/share", feature_category: :system_access do authorize! :admin_project, user_project shared_with_group = Group.find_by_id(params[:group_id]) @@ -739,7 +739,7 @@ module API requires :group_id, type: Integer, desc: 'The ID of the group' end # rubocop: disable CodeReuse/ActiveRecord - delete ":id/share/:group_id", feature_category: :authentication_and_authorization do + delete ":id/share/:group_id", feature_category: :system_access do authorize! :admin_project, user_project link = user_project.project_group_links.find_by(group_id: params[:group_id]) @@ -830,7 +830,7 @@ module API optional :skip_users, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Filter out users with the specified IDs' use :pagination end - get ':id/users', urgency: :low, feature_category: :authentication_and_authorization do + get ':id/users', urgency: :low, feature_category: :system_access do users = DeclarativePolicy.subject_scope { user_project.team.users } users = users.search(params[:search]) if params[:search].present? users = users.where_not_in(params[:skip_users]) if params[:skip_users].present? diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb index 754dfadb5fc..2726e05cd44 100644 --- a/lib/api/resource_access_tokens.rb +++ b/lib/api/resource_access_tokens.rb @@ -8,7 +8,7 @@ module API before { authenticate! } - feature_category :authentication_and_authorization + feature_category :system_access %w[project group].each do |source_type| resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do diff --git a/lib/api/users.rb b/lib/api/users.rb index cc7eb63798a..297306fa6d7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -381,7 +381,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - patch ":id/disable_two_factor", feature_category: :authentication_and_authorization do + patch ":id/disable_two_factor", feature_category: :system_access do authenticated_as_admin! user = User.find_by_id(params[:id]) @@ -407,7 +407,7 @@ module API requires :provider, type: String, desc: 'The external provider' end # rubocop: disable CodeReuse/ActiveRecord - delete ":id/identities/:provider", feature_category: :authentication_and_authorization do + delete ":id/identities/:provider", feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -456,7 +456,7 @@ module API desc: 'Scope of usage for the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - post ":user_id/keys", feature_category: :authentication_and_authorization do + post ":user_id/keys", feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params.delete(:user_id)) @@ -479,7 +479,7 @@ module API requires :user_id, type: String, desc: 'The ID or username of the user' use :pagination end - get ':user_id/keys', requirements: API::USER_REQUIREMENTS, feature_category: :authentication_and_authorization do + get ':user_id/keys', requirements: API::USER_REQUIREMENTS, feature_category: :system_access do user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) @@ -494,7 +494,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' requires :key_id, type: Integer, desc: 'The ID of the SSH key' end - get ':id/keys/:key_id', requirements: API::USER_REQUIREMENTS, feature_category: :authentication_and_authorization do + get ':id/keys/:key_id', requirements: API::USER_REQUIREMENTS, feature_category: :system_access do user = find_user(params[:id]) not_found!('User') unless user && can?(current_user, :read_user, user) @@ -512,7 +512,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - delete ':id/keys/:key_id', feature_category: :authentication_and_authorization do + delete ':id/keys/:key_id', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -537,7 +537,7 @@ module API requires :key, type: String, desc: 'The new GPG key' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/gpg_keys', feature_category: :authentication_and_authorization do + post ':id/gpg_keys', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) @@ -562,7 +562,7 @@ module API use :pagination end # rubocop: disable CodeReuse/ActiveRecord - get ':id/gpg_keys', feature_category: :authentication_and_authorization do + get ':id/gpg_keys', feature_category: :system_access do user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -579,7 +579,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - get ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do + get ':id/gpg_keys/:key_id', feature_category: :system_access do user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -598,7 +598,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - delete ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do + delete ':id/gpg_keys/:key_id', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -622,7 +622,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do + post ':id/gpg_keys/:key_id/revoke', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -726,7 +726,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/activate', feature_category: :authentication_and_authorization do + post ':id/activate', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -740,7 +740,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - post ':id/approve', feature_category: :authentication_and_authorization do + post ':id/approve', feature_category: :system_access do user = User.find_by(id: params[:id]) not_found!('User') unless can?(current_user, :read_user, user) @@ -757,7 +757,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - post ':id/reject', feature_category: :authentication_and_authorization do + post ':id/reject', feature_category: :system_access do user = find_user_by_id(params) result = ::Users::RejectService.new(current_user).execute(user) @@ -775,7 +775,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/deactivate', feature_category: :authentication_and_authorization do + post ':id/deactivate', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -801,7 +801,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/block', feature_category: :authentication_and_authorization do + post ':id/block', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -828,7 +828,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/unblock', feature_category: :authentication_and_authorization do + post ':id/unblock', feature_category: :system_access do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -848,7 +848,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - post ':id/ban', feature_category: :authentication_and_authorization do + post ':id/ban', feature_category: :system_access do authenticated_as_admin! user = find_user_by_id(params) @@ -864,7 +864,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - post ':id/unban', feature_category: :authentication_and_authorization do + post ':id/unban', feature_category: :system_access do authenticated_as_admin! user = find_user_by_id(params) @@ -928,7 +928,7 @@ module API use :pagination optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens' end - get feature_category: :authentication_and_authorization do + get feature_category: :system_access do present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken end @@ -941,7 +941,7 @@ module API optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token' optional :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The array of scopes of the impersonation token' end - post feature_category: :authentication_and_authorization do + post feature_category: :system_access do impersonation_token = finder.build(declared_params(include_missing: false)) if impersonation_token.save @@ -958,7 +958,7 @@ module API params do requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' end - get ':impersonation_token_id', feature_category: :authentication_and_authorization do + get ':impersonation_token_id', feature_category: :system_access do present find_impersonation_token, with: Entities::ImpersonationToken end @@ -968,7 +968,7 @@ module API params do requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' end - delete ':impersonation_token_id', feature_category: :authentication_and_authorization do + delete ':impersonation_token_id', feature_category: :system_access do token = find_impersonation_token destroy_conditionally!(token) do @@ -996,7 +996,7 @@ module API desc: 'The array of scopes of the personal access token' optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token' end - post feature_category: :authentication_and_authorization do + post feature_category: :system_access do response = ::PersonalAccessTokens::CreateService.new( current_user: current_user, target_user: target_user, params: declared_params(include_missing: false) ).execute @@ -1060,7 +1060,7 @@ module API params do use :pagination end - get "keys", feature_category: :authentication_and_authorization do + get "keys", feature_category: :system_access do keys = current_user.keys.preload_users present paginate(keys), with: Entities::SSHKey @@ -1073,7 +1073,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - get "keys/:key_id", feature_category: :authentication_and_authorization do + get "keys/:key_id", feature_category: :system_access do key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key @@ -1091,7 +1091,7 @@ module API optional :usage_type, type: String, values: Key.usage_types.keys, default: 'auth_and_signing', desc: 'Scope of usage for the SSH key' end - post "keys", feature_category: :authentication_and_authorization do + post "keys", feature_category: :system_access do key = ::Keys::CreateService.new(current_user, declared_params(include_missing: false)).execute if key.persisted? @@ -1108,7 +1108,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - delete "keys/:key_id", feature_category: :authentication_and_authorization do + delete "keys/:key_id", feature_category: :system_access do key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key @@ -1126,7 +1126,7 @@ module API params do use :pagination end - get 'gpg_keys', feature_category: :authentication_and_authorization do + get 'gpg_keys', feature_category: :system_access do present paginate(current_user.gpg_keys), with: Entities::GpgKey end @@ -1138,7 +1138,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - get 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do + get 'gpg_keys/:key_id', feature_category: :system_access do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key @@ -1153,7 +1153,7 @@ module API params do requires :key, type: String, desc: 'The new GPG key' end - post 'gpg_keys', feature_category: :authentication_and_authorization do + post 'gpg_keys', feature_category: :system_access do key = ::GpgKeys::CreateService.new(current_user, declared_params(include_missing: false)).execute if key.persisted? @@ -1170,7 +1170,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - post 'gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do + post 'gpg_keys/:key_id/revoke', feature_category: :system_access do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key @@ -1186,7 +1186,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - delete 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do + delete 'gpg_keys/:key_id', feature_category: :system_access do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key diff --git a/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb b/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb index 82e607ac7a7..6f5ddec628d 100644 --- a/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb +++ b/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb @@ -12,7 +12,7 @@ module Gitlab end operation_name :update_all - feature_category :authentication_and_authorization + feature_category :system_access ADMIN_MODE_SCOPE = ['admin_mode'].freeze diff --git a/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb b/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb index 9283842046a..1a5ad1c14a6 100644 --- a/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb +++ b/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb @@ -5,7 +5,7 @@ module Gitlab # Back-fill container_registry_size for project_statistics class BackfillNamespaceLdapSettings < Gitlab::BackgroundMigration::BatchedMigrationJob operation_name :backfill_namespace_ldap_settings - feature_category :authentication_and_authorization + feature_category :system_access def perform # no-op in FOSS diff --git a/lib/gitlab/github_import/clients/proxy.rb b/lib/gitlab/github_import/clients/proxy.rb index b12df404640..afe313c5340 100644 --- a/lib/gitlab/github_import/clients/proxy.rb +++ b/lib/gitlab/github_import/clients/proxy.rb @@ -13,19 +13,11 @@ module Gitlab def repos(search_text, options) return { repos: filtered(client.repos, search_text) } if use_legacy? - if use_graphql? - fetch_repos_via_graphql(search_text, options) - else - fetch_repos_via_rest(search_text, options) - end + fetch_repos_via_graphql(search_text, options) end private - def fetch_repos_via_rest(search_text, options) - { repos: client.search_repos_by_name(search_text, options)[:items] } - end - def fetch_repos_via_graphql(search_text, options) response = client.search_repos_by_name_graphql(search_text, options) { @@ -49,10 +41,6 @@ module Gitlab def use_legacy? Feature.disabled?(:remove_legacy_github_client) end - - def use_graphql? - Feature.enabled?(:github_client_fetch_repos_via_graphql) - end end end end diff --git a/lib/gitlab/github_import/clients/search_repos.rb b/lib/gitlab/github_import/clients/search_repos.rb index b72e5ac7751..3547173b4f8 100644 --- a/lib/gitlab/github_import/clients/search_repos.rb +++ b/lib/gitlab/github_import/clients/search_repos.rb @@ -13,14 +13,6 @@ module Gitlab end end - def search_repos_by_name(name, options = {}) - search_query = search_repos_query(name, options) - - with_retry do - octokit.search_repositories(search_query, options).to_h - end - end - private def graphql_search_repos_body(name, options) diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb index f635deabf76..b4baeba72e8 100644 --- a/lib/gitlab/metrics/requests_rack_middleware.rb +++ b/lib/gitlab/metrics/requests_rack_middleware.rb @@ -22,7 +22,7 @@ module Gitlab # reasonable default. If we initialize every category we'll end up # with an explosion in unused metric combinations, but we want the # most common ones to be always present. - FEATURE_CATEGORIES_TO_INITIALIZE = ['authentication_and_authorization', + FEATURE_CATEGORIES_TO_INITIALIZE = ['system_access', 'code_review_workflow', 'continuous_integration', 'not_owned', 'source_code_management', FEATURE_CATEGORY_DEFAULT].freeze diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a4d914797cf..f86eaaa88a5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6781,6 +6781,9 @@ msgstr "" msgid "Blame" msgstr "" +msgid "Blame could not be loaded as a single page." +msgstr "" + msgid "BlobViewer|View on %{environmentName}" msgstr "" @@ -18006,9 +18009,6 @@ msgstr "" msgid "For example, the application using the token or the purpose of the token. Do not give sensitive information for the name of the token, as it will be visible to all %{resource_type} members." msgstr "" -msgid "For faster browsing, not all history is shown." -msgstr "" - msgid "For files larger than this limit, only index the file name. The file content is neither indexed nor searchable." msgstr "" @@ -25708,6 +25708,9 @@ msgstr "" msgid "Loading files, directories, and submodules in the path %{path} for commit reference %{ref}" msgstr "" +msgid "Loading full blame..." +msgstr "" + msgid "Loading more" msgstr "" @@ -40219,6 +40222,9 @@ msgstr "" msgid "Show filters" msgstr "" +msgid "Show full blame" +msgstr "" + msgid "Show group milestones" msgstr "" @@ -47461,6 +47467,9 @@ msgstr "" msgid "View blame" msgstr "" +msgid "View blame as separate pages" +msgstr "" + msgid "View blame prior to this change" msgstr "" @@ -47490,9 +47499,6 @@ msgstr "" msgid "View eligible approvers" msgstr "" -msgid "View entire blame" -msgstr "" - msgid "View exposed artifact" msgid_plural "View %d exposed artifacts" msgstr[0] "" @@ -47695,6 +47701,21 @@ msgstr "" msgid "VulnerabilityChart|Severity" msgstr "" +msgid "VulnerabilityDismissalReasons|Acceptable risk" +msgstr "" + +msgid "VulnerabilityDismissalReasons|False positive" +msgstr "" + +msgid "VulnerabilityDismissalReasons|Mitigating control" +msgstr "" + +msgid "VulnerabilityDismissalReasons|Not applicable" +msgstr "" + +msgid "VulnerabilityDismissalReasons|Used in tests" +msgstr "" + msgid "VulnerabilityManagement|%{statusStart}Confirmed%{statusEnd} %{timeago} by %{user}" msgstr "" diff --git a/package.json b/package.json index c5976b96ce3..cec754ca1a2 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@gitlab/ui": "56.2.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230223005157", + "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@rails/actioncable": "6.1.4-7", "@rails/ujs": "6.1.4-7", "@sourcegraph/code-host-integration": "0.0.84", @@ -197,6 +198,7 @@ "vue-virtual-scroll-list": "^1.4.7", "vuedraggable": "^2.23.0", "vuex": "^3.6.2", + "web-streams-polyfill": "^3.2.1", "web-vitals": "^0.2.4", "webpack": "^4.46.0", "webpack-bundle-analyzer": "^4.6.1", diff --git a/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb b/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb index e0db758dde3..87218442c76 100644 --- a/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb +++ b/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Group access token', product_group: :authentication_and_authorization do + describe 'Group access token', product_group: :system_access do let(:group_access_token) { QA::Resource::GroupAccessToken.fabricate_via_api! } let(:api_client) { Runtime::API::Client.new(:gitlab, personal_access_token: group_access_token.token) } let(:project) do diff --git a/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb b/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb index d693bbd43ff..a85d8fa3327 100644 --- a/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb +++ b/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Project access token', product_group: :authentication_and_authorization do + describe 'Project access token', product_group: :system_access do before(:all) do @project_access_token = QA::Resource::ProjectAccessToken.fabricate_via_api! do |pat| pat.project = Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb b/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb index b7d0d72297a..9903c7f1f01 100644 --- a/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb +++ b/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'User', :requires_admin, :reliable, product_group: :authentication_and_authorization do + describe 'User', :requires_admin, :reliable, product_group: :system_access do before(:all) do admin_api_client = Runtime::API::Client.as_admin diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb index a35cde854a2..f095a902e11 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Group access tokens', product_group: :authentication_and_authorization do + describe 'Group access tokens', product_group: :system_access do let(:group_access_token) { QA::Resource::GroupAccessToken.fabricate_via_browser_ui! } it( diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb index 0f3d6a104a7..3b048ecbe24 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage', :requires_admin, :skip_live_env, :reliable do - describe '2FA', product_group: :authentication_and_authorization do + describe '2FA', product_group: :system_access do let(:owner_user) do Resource::User.fabricate_via_api! do |usr| usr.api_client = admin_api_client diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb index 9484f15f35d..d124048cc89 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Manage', :reliable, :requires_admin, :skip_live_env, product_group: :authentication_and_authorization do + context 'Manage', :reliable, :requires_admin, :skip_live_env, product_group: :system_access do describe '2FA' do let!(:user) { Resource::User.fabricate_via_api! } let!(:user_api_client) { Runtime::API::Client.new(:gitlab, user: user) } diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb index 7b91156d926..344cbefcea9 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :smoke, :mobile, product_group: :authentication_and_authorization do + RSpec.describe 'Manage', :smoke, :mobile, product_group: :system_access do describe 'basic user login' do it 'user logs in using basic credentials and logs out', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347880' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb index cf9282c1149..e04d688fdee 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :requires_admin, :skip_live_env, product_group: :authentication_and_authorization do + RSpec.describe 'Manage', :requires_admin, :skip_live_env, product_group: :system_access do describe '2FA' do let(:admin_api_client) { Runtime::API::Client.as_admin } let(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner_user) } diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb index 3d2e8c13900..3db8fdc5885 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls, product_group: :authentication_and_authorization do + RSpec.describe 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls, product_group: :system_access do describe 'LDAP login' do it 'user logs into GitLab using LDAP credentials', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347892' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb index 388c9f6b486..5132887f4dc 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :orchestrated, :mattermost, product_group: :authentication_and_authorization do + RSpec.describe 'Manage', :orchestrated, :mattermost, product_group: :system_access do describe 'Mattermost login' do it 'user logs into Mattermost using GitLab OAuth', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347891' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb index ca10a3f3d65..4a67668337a 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :orchestrated, :instance_saml, product_group: :authentication_and_authorization do + RSpec.describe 'Manage', :orchestrated, :instance_saml, product_group: :system_access do describe 'Instance wide SAML SSO' do it( 'user logs in to gitlab with SAML SSO', diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb index dd39b0c8835..20befbae773 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage', only: { subdomain: %i[staging staging-canary] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/344213', type: :stale } do - describe 'basic user', product_group: :authentication_and_authorization do + describe 'basic user', product_group: :system_access do it 'remains logged in when redirected from canary to non-canary node', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347626' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 3f5842d756e..22f4d007b01 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -15,7 +15,7 @@ module QA end end - RSpec.describe 'Manage', :skip_signup_disabled, :requires_admin, product_group: :authentication_and_authorization do + RSpec.describe 'Manage', :skip_signup_disabled, :requires_admin, product_group: :system_access do describe 'while LDAP is enabled', :orchestrated, :ldap_no_tls, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347934' do before do # When LDAP is enabled, a previous test might have created a token for the LDAP 'tanuki' user who is not an admin diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb index 55f63845acd..76ba541d052 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Project access tokens', :reliable, product_group: :authentication_and_authorization do + describe 'Project access tokens', :reliable, product_group: :system_access do let(:project_access_token) { QA::Resource::ProjectAccessToken.fabricate_via_browser_ui! } it( diff --git a/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb index ce5d9307769..2bd87cd9ea2 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Impersonation tokens', :requires_admin, product_group: :authentication_and_authorization do + describe 'Impersonation tokens', :requires_admin, product_group: :system_access do let(:admin_api_client) { Runtime::API::Client.as_admin } let!(:user) do diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 406a3604b23..b544c0615b3 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -161,9 +161,7 @@ RSpec.describe Import::GithubController, feature_category: :importers do let(:provider_repos) { [] } let(:expected_filter) { '' } let(:expected_options) do - pagination_params.merge(relation_params).merge( - first: 25, page: 1, per_page: 25 - ) + pagination_params.merge(relation_params).merge(first: 25) end before do @@ -279,22 +277,12 @@ RSpec.describe Import::GithubController, feature_category: :importers do it_behaves_like 'calls repos through Clients::Proxy with expected args' end - - context 'when page is specified' do - let(:pagination_params) { { before: nil, after: nil, page: 2 } } - let(:params) { pagination_params } - let(:expected_options) do - pagination_params.merge(relation_params).merge(first: 25, page: 2, per_page: 25) - end - - it_behaves_like 'calls repos through Clients::Proxy with expected args' - end end context 'when relation type params present' do let(:organization_login) { 'test-login' } let(:params) { pagination_params.merge(relation_type: 'organization', organization_login: organization_login) } - let(:pagination_defaults) { { first: 25, page: 1, per_page: 25 } } + let(:pagination_defaults) { { first: 25 } } let(:expected_options) do pagination_defaults.merge(pagination_params).merge( relation_type: 'organization', organization_login: organization_login diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index bd9e9f1c4cf..69eae1c00db 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -10,7 +10,8 @@ RSpec.describe 'Database schema', feature_category: :database do let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb } IGNORED_INDEXES_ON_FKS = { - slack_integrations_scopes: %w[slack_api_scope_id] + slack_integrations_scopes: %w[slack_api_scope_id], + p_ci_builds_metadata: %w[partition_id] # composable FK, the columns are reversed in the index definition }.with_indifferent_access.freeze TABLE_PARTITIONS = %w[ci_builds_metadata].freeze @@ -39,7 +40,7 @@ RSpec.describe 'Database schema', feature_category: :database do ci_build_trace_metadata: %w[partition_id build_id], ci_builds: %w[erased_by_id trigger_request_id partition_id], ci_builds_runner_session: %w[partition_id build_id], - p_ci_builds_metadata: %w[partition_id], + p_ci_builds_metadata: %w[partition_id build_id], ci_job_artifacts: %w[partition_id job_id], ci_job_variables: %w[partition_id job_id], ci_namespace_monthly_usages: %w[namespace_id], diff --git a/spec/features/merge_request/real_time_merge_widget_spec.rb b/spec/features/merge_request/real_time_merge_widget_spec.rb new file mode 100644 index 00000000000..299651feb53 --- /dev/null +++ b/spec/features/merge_request/real_time_merge_widget_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Merge request > Real-time merge widget', :js, feature_category: :code_review_workflow do + let_it_be_with_reload(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user) } + + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + context 'when merge status changes' do + let(:trigger_action) do + # There are different service classes that can change the merge_status + # so we simulate it here. + merge_request.mark_as_unchecked! + GraphqlTriggers.merge_request_merge_status_updated(merge_request) + end + + let(:widget_text) { s_('mrWidget|Checking if merge request can be merged…') } + + it_behaves_like 'updates merge widget in real-time' + end + + context 'when MR gets closed' do + let(:trigger_action) do + MergeRequests::CloseService + .new(project: project, current_user: user) + .execute(merge_request) + end + + let(:widget_text) { s_('mrWidget|Closed by') } + + it_behaves_like 'updates merge widget in real-time' + end + + context 'when MR gets marked as draft' do + let(:trigger_action) do + MergeRequests::UpdateService + .new(project: project, current_user: user, params: { title: 'Draft: title' }) + .execute(merge_request) + end + + let(:widget_text) { 'Merge blocked: Select Mark as ready to remove it from Draft status.' } + + it_behaves_like 'updates merge widget in real-time' + end + + context 'when MR gets approved' do + let(:trigger_action) do + MergeRequests::ApprovalService + .new(project: project, current_user: user) + .execute(merge_request) + end + + let(:widget_text) { _('Ready to merge!') } + + before do + merge_request.update!(approvals_before_merge: 1) + end + + it_behaves_like 'updates merge widget in real-time' + end + + context 'when a new discussion is started and all threads must be resolved before merge' do + let(:trigger_action) do + Notes::CreateService.new(project, user, { + merge_request_diff_head_sha: merge_request.diff_head_sha, + noteable_id: merge_request.id, + noteable_type: merge_request.class.name, + note: 'Unresolved discussion', + type: 'DiscussionNote' + }).execute + end + + let(:widget_text) { s_('mrWidget|Merge blocked: all threads must be resolved.') } + + before do + project.update!(only_allow_merge_if_all_discussions_are_resolved: true) + end + + it_behaves_like 'updates merge widget in real-time' + end +end diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb index 27b7c6ef2d5..d3558af81b8 100644 --- a/spec/features/projects/blobs/blame_spec.rb +++ b/spec/features/projects/blobs/blame_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do within '[data-testid="blob-content-holder"]' do expect(page).to have_css('.blame-commit') expect(page).not_to have_css('.gl-pagination') - expect(page).not_to have_link _('View entire blame') + expect(page).not_to have_link _('Show full blame') end end @@ -53,7 +53,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do within '[data-testid="blob-content-holder"]' do expect(page).to have_css('.blame-commit') expect(page).to have_css('.gl-pagination') - expect(page).to have_link _('View entire blame') + expect(page).to have_link _('Show full blame') expect(page).to have_css('#L1') expect(page).not_to have_css('#L3') @@ -85,19 +85,42 @@ RSpec.describe 'File blame', :js, feature_category: :projects do end end - context 'when user clicks on View entire blame button' do + shared_examples 'a full blame page' do + context 'when user clicks on Show full blame button' do + before do + visit_blob_blame(path) + click_link _('Show full blame') + end + + it 'displays the blame page without pagination' do + within '[data-testid="blob-content-holder"]' do + expect(page).to have_css('#L1') + expect(page).to have_css('#L667') + expect(page).not_to have_css('.gl-pagination') + end + end + end + end + + context 'when streaming is disabled' do before do - visit_blob_blame(path) + stub_feature_flags(blame_page_streaming: false) end - it 'displays the blame page without pagination' do - within '[data-testid="blob-content-holder"]' do - click_link _('View entire blame') + it_behaves_like 'a full blame page' + end - expect(page).to have_css('#L1') - expect(page).to have_css('#L3') - expect(page).not_to have_css('.gl-pagination') - end + context 'when streaming is enabled' do + before do + stub_const('Projects::BlameService::STREAMING_PER_PAGE', 50) + end + + it_behaves_like 'a full blame page' + + it 'shows loading text' do + visit_blob_blame(path) + click_link _('Show full blame') + expect(page).to have_text('Loading full blame...') end end @@ -112,7 +135,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do within '[data-testid="blob-content-holder"]' do expect(page).to have_css('.blame-commit') expect(page).not_to have_css('.gl-pagination') - expect(page).not_to have_link _('View entire blame') + expect(page).not_to have_link _('Show full blame') end end end diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js index 2fe9fe89a90..7fc81cf6548 100644 --- a/spec/frontend/__helpers__/shared_test_setup.js +++ b/spec/frontend/__helpers__/shared_test_setup.js @@ -1,4 +1,5 @@ /* Common setup for both unit and integration test environments */ +import { ReadableStream, WritableStream } from 'node:stream/web'; import * as jqueryMatchers from 'custom-jquery-matchers'; import Vue from 'vue'; import { enableAutoDestroy } from '@vue/test-utils'; @@ -13,6 +14,9 @@ import './dom_shims'; import './jquery'; import '~/commons/bootstrap'; +global.ReadableStream = ReadableStream; +global.WritableStream = WritableStream; + enableAutoDestroy(afterEach); // This module has some fairly decent visual test coverage in it's own repository. diff --git a/spec/frontend/__mocks__/lodash/debounce.js b/spec/frontend/__mocks__/lodash/debounce.js index d4fe2ce5406..15f806fc31a 100644 --- a/spec/frontend/__mocks__/lodash/debounce.js +++ b/spec/frontend/__mocks__/lodash/debounce.js @@ -9,9 +9,22 @@ // Further reference: https://github.com/facebook/jest/issues/3465 export default (fn) => { - const debouncedFn = jest.fn().mockImplementation(fn); - debouncedFn.cancel = jest.fn(); - debouncedFn.flush = jest.fn().mockImplementation(() => { + let id; + const debouncedFn = jest.fn(function run(...args) { + // this is calculated in runtime so beforeAll hook works in tests + const timeout = global.JEST_DEBOUNCE_THROTTLE_TIMEOUT; + if (timeout) { + id = setTimeout(() => { + fn.apply(this, args); + }, timeout); + } else { + fn.apply(this, args); + } + }); + debouncedFn.cancel = jest.fn(() => { + clearTimeout(id); + }); + debouncedFn.flush = jest.fn(() => { const errorMessage = "The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'."; diff --git a/spec/frontend/__mocks__/lodash/throttle.js b/spec/frontend/__mocks__/lodash/throttle.js index e8a82654c78..b1014662918 100644 --- a/spec/frontend/__mocks__/lodash/throttle.js +++ b/spec/frontend/__mocks__/lodash/throttle.js @@ -1,4 +1,4 @@ // Similar to `lodash/debounce`, `lodash/throttle` also causes flaky specs. // See `./debounce.js` for more details. -export default (fn) => fn; +export { default } from './debounce'; diff --git a/spec/frontend/blame/streaming/index_spec.js b/spec/frontend/blame/streaming/index_spec.js new file mode 100644 index 00000000000..a5069f8a7d8 --- /dev/null +++ b/spec/frontend/blame/streaming/index_spec.js @@ -0,0 +1,110 @@ +import waitForPromises from 'helpers/wait_for_promises'; +import { renderBlamePageStreams } from '~/blame/streaming'; +import { setHTMLFixture } from 'helpers/fixtures'; +import { renderHtmlStreams } from '~/streaming/render_html_streams'; +import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests'; +import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link'; +import { toPolyfillReadable } from '~/streaming/polyfills'; +import { createAlert } from '~/flash'; + +jest.mock('~/streaming/render_html_streams'); +jest.mock('~/streaming/rate_limit_stream_requests'); +jest.mock('~/streaming/handle_streamed_anchor_link'); +jest.mock('~/streaming/polyfills'); +jest.mock('~/sentry'); +jest.mock('~/flash'); + +global.fetch = jest.fn(); + +describe('renderBlamePageStreams', () => { + let stopAnchor; + const PAGES_URL = 'https://example.com/'; + const findStreamContainer = () => document.querySelector('#blame-stream-container'); + const findStreamLoadingIndicator = () => document.querySelector('#blame-stream-loading'); + + const setupHtml = (totalExtraPages = 0) => { + setHTMLFixture(` + <div id="blob-content-holder" + data-total-extra-pages="${totalExtraPages}" + data-pages-url="${PAGES_URL}" + ></div> + <div id="blame-stream-container"></div> + <div id="blame-stream-loading"></div> + `); + }; + + handleStreamedAnchorLink.mockImplementation(() => stopAnchor); + rateLimitStreamRequests.mockImplementation(({ factory, total }) => { + return Array.from({ length: total }, (_, i) => { + return Promise.resolve(factory(i)); + }); + }); + toPolyfillReadable.mockImplementation((obj) => obj); + + beforeEach(() => { + stopAnchor = jest.fn(); + fetch.mockClear(); + }); + + it('does nothing for an empty page', async () => { + await renderBlamePageStreams(); + + expect(handleStreamedAnchorLink).not.toHaveBeenCalled(); + expect(renderHtmlStreams).not.toHaveBeenCalled(); + }); + + it('renders a single stream', async () => { + let res; + const stream = new Promise((resolve) => { + res = resolve; + }); + renderHtmlStreams.mockImplementationOnce(() => stream); + setupHtml(); + + renderBlamePageStreams(stream); + + expect(handleStreamedAnchorLink).toHaveBeenCalledTimes(1); + expect(stopAnchor).toHaveBeenCalledTimes(0); + expect(renderHtmlStreams).toHaveBeenCalledWith([stream], findStreamContainer()); + expect(findStreamLoadingIndicator()).not.toBe(null); + + res(); + await waitForPromises(); + + expect(stopAnchor).toHaveBeenCalledTimes(1); + expect(findStreamLoadingIndicator()).toBe(null); + }); + + it('renders rest of the streams', async () => { + const stream = Promise.resolve(); + const stream2 = Promise.resolve({ body: null }); + fetch.mockImplementationOnce(() => stream2); + setupHtml(1); + + await renderBlamePageStreams(stream); + + expect(fetch.mock.calls[0][0].toString()).toBe(`${PAGES_URL}?page=3`); + expect(renderHtmlStreams).toHaveBeenCalledWith([stream, stream2], findStreamContainer()); + }); + + it('shows an error message when failed', async () => { + const stream = Promise.resolve(); + const error = new Error(); + renderHtmlStreams.mockImplementationOnce(() => Promise.reject(error)); + setupHtml(); + + try { + await renderBlamePageStreams(stream); + } catch (err) { + expect(err).toBe(error); + } + + expect(createAlert).toHaveBeenCalledWith({ + message: 'Blame could not be loaded as a single page.', + primaryButton: { + text: 'View blame as separate pages', + clickHandler: expect.any(Function), + }, + }); + }); +}); diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js index f6d5833edee..ce43e648b43 100644 --- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js @@ -1,7 +1,9 @@ -import { mount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue'; +import { stubComponent } from 'helpers/stub_component'; import { createStore } from '~/ide/stores'; import { file } from '../../helpers'; @@ -12,9 +14,10 @@ const TEST_FILE_PATH = 'test/file/path'; describe('IDE commit editor header', () => { let wrapper; let store; + const showMock = jest.fn(); const createComponent = (fileProps = {}) => { - wrapper = mount(EditorHeader, { + wrapper = shallowMount(EditorHeader, { store, propsData: { activeFile: { @@ -23,22 +26,17 @@ describe('IDE commit editor header', () => { ...fileProps, }, }, + stubs: { + GlModal: stubComponent(GlModal, { + methods: { show: showMock }, + }), + }, }); }; const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' }); const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' }); - beforeEach(() => { - store = createStore(); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each` fileProps | shouldExist ${{ staged: false, changed: false }} | ${false} @@ -52,20 +50,19 @@ describe('IDE commit editor header', () => { }); describe('discard button', () => { - beforeEach(() => { + it('opens a dialog confirming discard', () => { createComponent(); + findDiscardButton().vm.$emit('click'); - const modal = findDiscardModal(); - jest.spyOn(modal.vm, 'show'); - - findDiscardButton().trigger('click'); - }); - - it('opens a dialog confirming discard', () => { - expect(findDiscardModal().vm.show).toHaveBeenCalled(); + expect(showMock).toHaveBeenCalled(); }); it('calls discardFileChanges if dialog result is confirmed', () => { + store = createStore(); + jest.spyOn(store, 'dispatch').mockImplementation(); + + createComponent(); + expect(store.dispatch).not.toHaveBeenCalled(); findDiscardModal().vm.$emit('primary'); diff --git a/spec/frontend/streaming/chunk_writer_spec.js b/spec/frontend/streaming/chunk_writer_spec.js new file mode 100644 index 00000000000..2aadb332838 --- /dev/null +++ b/spec/frontend/streaming/chunk_writer_spec.js @@ -0,0 +1,214 @@ +import { ChunkWriter } from '~/streaming/chunk_writer'; +import { RenderBalancer } from '~/streaming/render_balancer'; + +jest.mock('~/streaming/render_balancer'); + +describe('ChunkWriter', () => { + let accumulator = ''; + let write; + let close; + let abort; + let config; + let render; + + const createChunk = (text) => { + const encoder = new TextEncoder(); + return encoder.encode(text); + }; + + const createHtmlStream = () => { + write = jest.fn((part) => { + accumulator += part; + }); + close = jest.fn(); + abort = jest.fn(); + return { + write, + close, + abort, + }; + }; + + const createWriter = () => { + return new ChunkWriter(createHtmlStream(), config); + }; + + const pushChunks = (...chunks) => { + const writer = createWriter(); + chunks.forEach((chunk) => { + writer.write(createChunk(chunk)); + }); + writer.close(); + }; + + afterAll(() => { + global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined; + }); + + beforeEach(() => { + global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = 100; + accumulator = ''; + config = undefined; + render = jest.fn((cb) => { + while (cb()) { + // render until 'false' + } + }); + RenderBalancer.mockImplementation(() => ({ render })); + }); + + describe('when chunk length must be "1"', () => { + beforeEach(() => { + config = { minChunkSize: 1, maxChunkSize: 1 }; + }); + + it('splits big chunks into smaller ones', () => { + const text = 'foobar'; + pushChunks(text); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(text.length); + }); + + it('handles small emoji chunks', () => { + const text = 'foo👀bar👨👩👧baz👧👧🏻👧🏼👧🏽👧🏾👧🏿'; + pushChunks(text); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(createChunk(text).length); + }); + }); + + describe('when chunk length must not be lower than "5" and exceed "10"', () => { + beforeEach(() => { + config = { minChunkSize: 5, maxChunkSize: 10 }; + }); + + it('joins small chunks', () => { + const text = '12345'; + pushChunks(...text.split('')); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(1); + expect(close).toHaveBeenCalledTimes(1); + }); + + it('handles overflow with small chunks', () => { + const text = '123456789'; + pushChunks(...text.split('')); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(2); + expect(close).toHaveBeenCalledTimes(1); + }); + + it('calls flush on small chunks', () => { + global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined; + const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator'); + const text = '1'; + pushChunks(text); + expect(accumulator).toBe(text); + expect(flushAccumulator).toHaveBeenCalledTimes(1); + }); + + it('calls flush on large chunks', () => { + const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator'); + const text = '1234567890123'; + const writer = createWriter(); + writer.write(createChunk(text)); + jest.runAllTimers(); + expect(accumulator).toBe(text); + expect(flushAccumulator).toHaveBeenCalledTimes(1); + }); + }); + + describe('chunk balancing', () => { + let increase; + let decrease; + let renderOnce; + + beforeEach(() => { + render = jest.fn((cb) => { + let next = true; + renderOnce = () => { + if (!next) return; + next = cb(); + }; + }); + RenderBalancer.mockImplementation(({ increase: inc, decrease: dec }) => { + increase = jest.fn(inc); + decrease = jest.fn(dec); + return { + render, + }; + }); + }); + + describe('when frame time exceeds low limit', () => { + beforeEach(() => { + config = { + minChunkSize: 1, + maxChunkSize: 5, + balanceRate: 10, + }; + }); + + it('increases chunk size', () => { + const text = '111222223'; + const writer = createWriter(); + const chunk = createChunk(text); + + writer.write(chunk); + + renderOnce(); + increase(); + renderOnce(); + renderOnce(); + + writer.close(); + + expect(accumulator).toBe(text); + expect(write.mock.calls).toMatchObject([['111'], ['22222'], ['3']]); + expect(close).toHaveBeenCalledTimes(1); + }); + }); + + describe('when frame time exceeds high limit', () => { + beforeEach(() => { + config = { + minChunkSize: 1, + maxChunkSize: 10, + balanceRate: 2, + }; + }); + + it('decreases chunk size', () => { + const text = '1111112223345'; + const writer = createWriter(); + const chunk = createChunk(text); + + writer.write(chunk); + + renderOnce(); + decrease(); + + renderOnce(); + decrease(); + + renderOnce(); + decrease(); + + renderOnce(); + renderOnce(); + + writer.close(); + + expect(accumulator).toBe(text); + expect(write.mock.calls).toMatchObject([['111111'], ['222'], ['33'], ['4'], ['5']]); + expect(close).toHaveBeenCalledTimes(1); + }); + }); + }); + + it('calls abort on htmlStream', () => { + const writer = createWriter(); + writer.abort(); + expect(abort).toHaveBeenCalledTimes(1); + }); +}); diff --git a/spec/frontend/streaming/handle_streamed_anchor_link_spec.js b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js new file mode 100644 index 00000000000..ef17957b2fc --- /dev/null +++ b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js @@ -0,0 +1,132 @@ +import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures'; +import waitForPromises from 'helpers/wait_for_promises'; +import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link'; +import { scrollToElement } from '~/lib/utils/common_utils'; +import LineHighlighter from '~/blob/line_highlighter'; +import { TEST_HOST } from 'spec/test_constants'; + +jest.mock('~/lib/utils/common_utils'); +jest.mock('~/blob/line_highlighter'); + +describe('handleStreamedAnchorLink', () => { + const ANCHOR_START = 'L100'; + const ANCHOR_END = '300'; + const findRoot = () => document.querySelector('#root'); + + afterEach(() => { + resetHTMLFixture(); + }); + + describe('when single line anchor is given', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(`${TEST_HOST}#${ANCHOR_START}`); + }); + + describe('when element is present', () => { + beforeEach(() => { + setHTMLFixture(`<div id="root"><div id="${ANCHOR_START}"></div></div>`); + handleStreamedAnchorLink(findRoot()); + }); + + it('does nothing', async () => { + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + + describe('when element is streamed', () => { + let stop; + const insertElement = () => { + findRoot().insertAdjacentHTML('afterbegin', `<div id="${ANCHOR_START}"></div>`); + }; + + beforeEach(() => { + setHTMLFixture('<div id="root"></div>'); + stop = handleStreamedAnchorLink(findRoot()); + }); + + afterEach(() => { + stop = undefined; + }); + + it('scrolls to the anchor when inserted', async () => { + insertElement(); + await waitForPromises(); + expect(scrollToElement).toHaveBeenCalledTimes(1); + expect(LineHighlighter).toHaveBeenCalledTimes(1); + }); + + it("doesn't scroll to the anchor when destroyed", async () => { + stop(); + insertElement(); + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when line range anchor is given', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(`${TEST_HOST}#${ANCHOR_START}-${ANCHOR_END}`); + }); + + describe('when last element is present', () => { + beforeEach(() => { + setHTMLFixture(`<div id="root"><div id="L${ANCHOR_END}"></div></div>`); + handleStreamedAnchorLink(findRoot()); + }); + + it('does nothing', async () => { + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + + describe('when last element is streamed', () => { + let stop; + const insertElement = () => { + findRoot().insertAdjacentHTML( + 'afterbegin', + `<div id="${ANCHOR_START}"></div><div id="L${ANCHOR_END}"></div>`, + ); + }; + + beforeEach(() => { + setHTMLFixture('<div id="root"></div>'); + stop = handleStreamedAnchorLink(findRoot()); + }); + + afterEach(() => { + stop = undefined; + }); + + it('scrolls to the anchor when inserted', async () => { + insertElement(); + await waitForPromises(); + expect(scrollToElement).toHaveBeenCalledTimes(1); + expect(LineHighlighter).toHaveBeenCalledTimes(1); + }); + + it("doesn't scroll to the anchor when destroyed", async () => { + stop(); + insertElement(); + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when anchor is not given', () => { + beforeEach(() => { + setHTMLFixture(`<div id="root"></div>`); + handleStreamedAnchorLink(findRoot()); + }); + + it('does nothing', async () => { + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/streaming/html_stream_spec.js b/spec/frontend/streaming/html_stream_spec.js new file mode 100644 index 00000000000..115a9ddc803 --- /dev/null +++ b/spec/frontend/streaming/html_stream_spec.js @@ -0,0 +1,46 @@ +import { HtmlStream } from '~/streaming/html_stream'; +import { ChunkWriter } from '~/streaming/chunk_writer'; + +jest.mock('~/streaming/chunk_writer'); + +describe('HtmlStream', () => { + let write; + let close; + let streamingElement; + + beforeEach(() => { + write = jest.fn(); + close = jest.fn(); + jest.spyOn(Document.prototype, 'write').mockImplementation(write); + jest.spyOn(Document.prototype, 'close').mockImplementation(close); + jest.spyOn(Document.prototype, 'querySelector').mockImplementation(() => { + streamingElement = document.createElement('div'); + return streamingElement; + }); + }); + + it('attaches to original document', () => { + // eslint-disable-next-line no-new + new HtmlStream(document.body); + expect(document.body.contains(streamingElement)).toBe(true); + }); + + it('can write to a document', () => { + const htmlStream = new HtmlStream(document.body); + htmlStream.write('foo'); + htmlStream.close(); + expect(write.mock.calls).toEqual([['<streaming-element>'], ['foo'], ['</streaming-element>']]); + expect(close).toHaveBeenCalledTimes(1); + }); + + it('returns chunked writer', () => { + const htmlStream = new HtmlStream(document.body).withChunkWriter(); + expect(htmlStream).toBeInstanceOf(ChunkWriter); + }); + + it('closes on abort', () => { + const htmlStream = new HtmlStream(document.body); + htmlStream.abort(); + expect(close).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/streaming/rate_limit_stream_requests_spec.js b/spec/frontend/streaming/rate_limit_stream_requests_spec.js new file mode 100644 index 00000000000..02e3cf93014 --- /dev/null +++ b/spec/frontend/streaming/rate_limit_stream_requests_spec.js @@ -0,0 +1,155 @@ +import waitForPromises from 'helpers/wait_for_promises'; +import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests'; + +describe('rateLimitStreamRequests', () => { + const encoder = new TextEncoder('utf-8'); + const createStreamResponse = (content = 'foo') => + new ReadableStream({ + pull(controller) { + controller.enqueue(encoder.encode(content)); + controller.close(); + }, + }); + + const createFactory = (content) => { + return jest.fn(() => { + return Promise.resolve(createStreamResponse(content)); + }); + }; + + it('does nothing for zero total requests', () => { + const factory = jest.fn(); + const requests = rateLimitStreamRequests({ + factory, + total: 0, + }); + expect(factory).toHaveBeenCalledTimes(0); + expect(requests.length).toBe(0); + }); + + it('does not exceed total requests', () => { + const factory = createFactory(); + const requests = rateLimitStreamRequests({ + factory, + immediateCount: 100, + maxConcurrentRequests: 100, + total: 2, + }); + expect(factory).toHaveBeenCalledTimes(2); + expect(requests.length).toBe(2); + }); + + it('creates immediate requests', () => { + const factory = createFactory(); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 2, + total: 2, + }); + expect(factory).toHaveBeenCalledTimes(2); + expect(requests.length).toBe(2); + }); + + it('returns correct values', async () => { + const fixture = 'foobar'; + const factory = createFactory(fixture); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 2, + total: 2, + }); + + const decoder = new TextDecoder('utf-8'); + let result = ''; + for await (const stream of requests) { + await stream.pipeTo( + new WritableStream({ + // eslint-disable-next-line no-loop-func + write(content) { + result += decoder.decode(content); + }, + }), + ); + } + + expect(result).toBe(fixture + fixture); + }); + + it('delays rate limited requests', async () => { + const factory = createFactory(); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 2, + total: 3, + }); + expect(factory).toHaveBeenCalledTimes(2); + expect(requests.length).toBe(3); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(3); + }); + + it('runs next request after previous has been fulfilled', async () => { + let res; + const factory = jest + .fn() + .mockImplementationOnce( + () => + new Promise((resolve) => { + res = resolve; + }), + ) + .mockImplementationOnce(() => Promise.resolve(createStreamResponse())); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 1, + total: 2, + }); + expect(factory).toHaveBeenCalledTimes(1); + expect(requests.length).toBe(2); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(1); + + res(createStreamResponse()); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(2); + }); + + it('uses timer to schedule next request', async () => { + let res; + const factory = jest + .fn() + .mockImplementationOnce( + () => + new Promise((resolve) => { + res = resolve; + }), + ) + .mockImplementationOnce(() => Promise.resolve(createStreamResponse())); + const requests = rateLimitStreamRequests({ + factory, + immediateCount: 1, + maxConcurrentRequests: 2, + total: 2, + timeout: 9999, + }); + expect(factory).toHaveBeenCalledTimes(1); + expect(requests.length).toBe(2); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(1); + + jest.runAllTimers(); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(2); + res(createStreamResponse()); + }); +}); diff --git a/spec/frontend/streaming/render_balancer_spec.js b/spec/frontend/streaming/render_balancer_spec.js new file mode 100644 index 00000000000..dae0c98d678 --- /dev/null +++ b/spec/frontend/streaming/render_balancer_spec.js @@ -0,0 +1,69 @@ +import { RenderBalancer } from '~/streaming/render_balancer'; + +const HIGH_FRAME_TIME = 100; +const LOW_FRAME_TIME = 10; + +describe('renderBalancer', () => { + let frameTime = 0; + let frameTimeDelta = 0; + let decrease; + let increase; + + const createBalancer = () => { + decrease = jest.fn(); + increase = jest.fn(); + return new RenderBalancer({ + highFrameTime: HIGH_FRAME_TIME, + lowFrameTime: LOW_FRAME_TIME, + increase, + decrease, + }); + }; + + const renderTimes = (times) => { + const balancer = createBalancer(); + return new Promise((resolve) => { + let counter = 0; + balancer.render(() => { + if (counter === times) { + resolve(counter); + return false; + } + counter += 1; + return true; + }); + }); + }; + + beforeEach(() => { + jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + frameTime += frameTimeDelta; + cb(frameTime); + }); + }); + + afterEach(() => { + window.requestAnimationFrame.mockRestore(); + frameTime = 0; + frameTimeDelta = 0; + }); + + it('renders in a loop', async () => { + const count = await renderTimes(5); + expect(count).toBe(5); + }); + + it('calls decrease', async () => { + frameTimeDelta = 200; + await renderTimes(5); + expect(decrease).toHaveBeenCalled(); + expect(increase).not.toHaveBeenCalled(); + }); + + it('calls increase', async () => { + frameTimeDelta = 1; + await renderTimes(5); + expect(increase).toHaveBeenCalled(); + expect(decrease).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/streaming/render_html_streams_spec.js b/spec/frontend/streaming/render_html_streams_spec.js new file mode 100644 index 00000000000..55cef0ea469 --- /dev/null +++ b/spec/frontend/streaming/render_html_streams_spec.js @@ -0,0 +1,96 @@ +import { ReadableStream } from 'node:stream/web'; +import { renderHtmlStreams } from '~/streaming/render_html_streams'; +import { HtmlStream } from '~/streaming/html_stream'; +import waitForPromises from 'helpers/wait_for_promises'; + +jest.mock('~/streaming/html_stream'); +jest.mock('~/streaming/constants', () => { + return { + HIGH_FRAME_TIME: 0, + LOW_FRAME_TIME: 0, + MAX_CHUNK_SIZE: 1, + MIN_CHUNK_SIZE: 1, + }; +}); + +const firstStreamContent = 'foobar'; +const secondStreamContent = 'bazqux'; + +describe('renderHtmlStreams', () => { + let htmlWriter; + const encoder = new TextEncoder(); + const createSingleChunkStream = (chunk) => { + const encoded = encoder.encode(chunk); + const stream = new ReadableStream({ + pull(controller) { + controller.enqueue(encoded); + controller.close(); + }, + }); + return [stream, encoded]; + }; + + beforeEach(() => { + htmlWriter = { + write: jest.fn(), + close: jest.fn(), + abort: jest.fn(), + }; + jest.spyOn(HtmlStream.prototype, 'withChunkWriter').mockReturnValue(htmlWriter); + }); + + it('renders a single stream', async () => { + const [stream, encoded] = createSingleChunkStream(firstStreamContent); + + await renderHtmlStreams([Promise.resolve(stream)], document.body); + + expect(htmlWriter.write).toHaveBeenCalledWith(encoded); + expect(htmlWriter.close).toHaveBeenCalledTimes(1); + }); + + it('renders stream sequence', async () => { + const [stream1, encoded1] = createSingleChunkStream(firstStreamContent); + const [stream2, encoded2] = createSingleChunkStream(secondStreamContent); + + await renderHtmlStreams([Promise.resolve(stream1), Promise.resolve(stream2)], document.body); + + expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]); + expect(htmlWriter.close).toHaveBeenCalledTimes(1); + }); + + it("doesn't wait for the whole sequence to resolve before streaming", async () => { + const [stream1, encoded1] = createSingleChunkStream(firstStreamContent); + const [stream2, encoded2] = createSingleChunkStream(secondStreamContent); + + let res; + const delayedStream = new Promise((resolve) => { + res = resolve; + }); + + renderHtmlStreams([Promise.resolve(stream1), delayedStream], document.body); + + await waitForPromises(); + + expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1]]); + expect(htmlWriter.close).toHaveBeenCalledTimes(0); + + res(stream2); + await waitForPromises(); + + expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]); + expect(htmlWriter.close).toHaveBeenCalledTimes(1); + }); + + it('closes HtmlStream on error', async () => { + const [stream1] = createSingleChunkStream(firstStreamContent); + const error = new Error(); + + try { + await renderHtmlStreams([Promise.resolve(stream1), Promise.reject(error)], document.body); + } catch (err) { + expect(err).toBe(error); + } + + expect(htmlWriter.abort).toHaveBeenCalledTimes(1); + }); +}); diff --git a/spec/helpers/device_registration_helper_spec.rb b/spec/helpers/device_registration_helper_spec.rb index a8222cddca9..7556d037b3d 100644 --- a/spec/helpers/device_registration_helper_spec.rb +++ b/spec/helpers/device_registration_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe DeviceRegistrationHelper, feature_category: :authentication_and_authorization do +RSpec.describe DeviceRegistrationHelper, feature_category: :system_access do describe "#device_registration_data" do it "returns a hash with device registration properties without initial error" do device_registration_data = helper.device_registration_data( diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index e93d585bc3c..05c9c4bc70c 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -706,48 +706,6 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do end end end - - describe '#search_repos_by_name' do - let(:expected_query) { 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2' } - - it 'searches for repositories based on name' do - expect(client.octokit).to receive(:search_repositories).with(expected_query, {}) - - client.search_repos_by_name('test') - end - - context 'when pagination options present' do - it 'searches for repositories via expected query' do - expect(client.octokit).to receive(:search_repositories).with( - expected_query, { page: 2, per_page: 25 } - ) - - client.search_repos_by_name('test', { page: 2, per_page: 25 }) - end - end - - context 'when Faraday error received from octokit', :aggregate_failures do - let(:error_class) { described_class::CLIENT_CONNECTION_ERROR } - let(:info_params) { { 'error.class': error_class } } - - it 'retries on error and succeeds' do - allow_retry(:search_repositories) - - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once - - expect(client.search_repos_by_name('test')).to eq({}) - end - - it 'retries and does not succeed' do - allow(client.octokit) - .to receive(:search_repositories) - .with(expected_query, {}) - .and_raise(error_class, 'execution expired') - - expect { client.search_repos_by_name('test') }.to raise_error(error_class, 'execution expired') - end - end - end end def allow_retry(method = :pull_request) diff --git a/spec/lib/gitlab/github_import/clients/proxy_spec.rb b/spec/lib/gitlab/github_import/clients/proxy_spec.rb index 0baff7bafcb..5f785ae20b0 100644 --- a/spec/lib/gitlab/github_import/clients/proxy_spec.rb +++ b/spec/lib/gitlab/github_import/clients/proxy_spec.rb @@ -15,54 +15,30 @@ RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category: context 'when remove_legacy_github_client FF is enabled' do let(:client_stub) { instance_double(Gitlab::GithubImport::Client) } - context 'with github_client_fetch_repos_via_graphql FF enabled' do - let(:client_response) do - { - data: { - search: { - nodes: [{ name: 'foo' }, { name: 'bar' }], - pageInfo: { startCursor: 'foo', endCursor: 'bar' } - } + let(:client_response) do + { + data: { + search: { + nodes: [{ name: 'foo' }, { name: 'bar' }], + pageInfo: { startCursor: 'foo', endCursor: 'bar' } } } - end - - it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do - expect(Gitlab::GithubImport::Client) - .to receive(:new).with(access_token).and_return(client_stub) - expect(client_stub) - .to receive(:search_repos_by_name_graphql) - .with(search_text, pagination_options).and_return(client_response) - - expect(client.repos(search_text, pagination_options)).to eq( - { - repos: [{ name: 'foo' }, { name: 'bar' }], - page_info: { startCursor: 'foo', endCursor: 'bar' } - } - ) - end + } end - context 'with github_client_fetch_repos_via_graphql FF disabled' do - let(:client_response) do - { items: [{ name: 'foo' }, { name: 'bar' }] } - end + it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do + expect(Gitlab::GithubImport::Client) + .to receive(:new).with(access_token).and_return(client_stub) + expect(client_stub) + .to receive(:search_repos_by_name_graphql) + .with(search_text, pagination_options).and_return(client_response) - before do - stub_feature_flags(github_client_fetch_repos_via_graphql: false) - end - - it 'fetches repos with Gitlab::GithubImport::Client (REST API)' do - expect(Gitlab::GithubImport::Client) - .to receive(:new).with(access_token).and_return(client_stub) - expect(client_stub) - .to receive(:search_repos_by_name) - .with(search_text, pagination_options).and_return(client_response) - - expect(client.repos(search_text, pagination_options)).to eq( - { repos: [{ name: 'foo' }, { name: 'bar' }] } - ) - end + expect(client.repos(search_text, pagination_options)).to eq( + { + repos: [{ name: 'foo' }, { name: 'bar' }], + page_info: { startCursor: 'foo', endCursor: 'bar' } + } + ) end end diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index f7cee6beb58..80c1af1b913 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -331,7 +331,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do include_context 'server metrics call' context 'when a worker has a feature category' do - let(:worker_category) { 'authentication_and_authorization' } + let(:worker_category) { 'system_access' } it 'uses that category for metrics' do expect(completion_seconds_metric).to receive(:observe).with(a_hash_including(feature_category: worker_category), anything) diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb index 1b6cd7ac5fb..4fbc64a45d6 100644 --- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb @@ -123,7 +123,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do context 'when the feature category is already set in the surrounding block' do it 'takes the feature category from the worker, not the caller' do - Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do + Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do TestWithContextWorker.bulk_perform_async_with_contexts( %w(job1 job2), arguments_proc: -> (name) { [name, 1, 2, 3] }, @@ -139,7 +139,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do end it 'takes the feature category from the caller if the worker is not owned' do - Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do + Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts( %w(job1 job2), arguments_proc: -> (name) { [name, 1, 2, 3] }, @@ -150,8 +150,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do job1 = TestNotOwnedWithContextWorker.job_for_args(['job1', 1, 2, 3]) job2 = TestNotOwnedWithContextWorker.job_for_args(['job2', 1, 2, 3]) - expect(job1['meta.feature_category']).to eq('authentication_and_authorization') - expect(job2['meta.feature_category']).to eq('authentication_and_authorization') + expect(job1['meta.feature_category']).to eq('system_access') + expect(job2['meta.feature_category']).to eq('system_access') end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb index 2deab3064eb..eb077a0371c 100644 --- a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb @@ -69,7 +69,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do context 'feature category' do it 'takes the feature category from the worker' do - Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do + Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do TestWorker.perform_async('identifier', 1) end @@ -78,11 +78,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do context 'when the worker is not owned' do it 'takes the feature category from the surrounding context' do - Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do + Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do NotOwnedWorker.perform_async('identifier', 1) end - expect(NotOwnedWorker.contexts['identifier']).to include('meta.feature_category' => 'authentication_and_authorization') + expect(NotOwnedWorker.contexts['identifier']).to include('meta.feature_category' => 'system_access') end end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 65a407b0346..003ca2512dc 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -1297,9 +1297,9 @@ RSpec.describe GroupPolicy, feature_category: :system_access do end end - context 'create_runner_workflow_for_admin flag enabled' do + context 'create_runner_workflow_for_namespace flag enabled' do before do - stub_feature_flags(create_runner_workflow_for_admin: true) + stub_feature_flags(create_runner_workflow_for_namespace: [group]) end context 'admin' do @@ -1380,11 +1380,13 @@ RSpec.describe GroupPolicy, feature_category: :system_access do end end - context 'with create_runner_workflow_for_admin flag disabled' do + context 'with create_runner_workflow_for_namespace flag disabled' do before do - stub_feature_flags(create_runner_workflow_for_admin: false) + stub_feature_flags(create_runner_workflow_for_namespace: [other_group]) end + let_it_be(:other_group) { create(:group) } + context 'admin' do let(:current_user) { admin } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index bc58171f27b..e7b548b8f3b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -2740,9 +2740,9 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do end describe 'create_project_runners' do - context 'create_runner_workflow_for_admin flag enabled' do + context 'create_runner_workflow_for_namespace flag enabled' do before do - stub_feature_flags(create_runner_workflow_for_admin: true) + stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace]) end context 'admin' do @@ -2810,9 +2810,9 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do end end - context 'create_runner_workflow_for_admin flag disabled' do + context 'create_runner_workflow_for_namespace flag disabled' do before do - stub_feature_flags(create_runner_workflow_for_admin: false) + stub_feature_flags(create_runner_workflow_for_namespace: [group]) end context 'admin' do diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb index 1ecd1edd99e..dd4171b257a 100644 --- a/spec/requests/api/admin/ci/variables_spec.rb +++ b/spec/requests/api/admin/ci/variables_spec.rb @@ -51,7 +51,9 @@ RSpec.describe ::API::Admin::Ci::Variables do end describe 'POST /admin/ci/variables' do - it_behaves_like 'POST request permissions for admin mode', { key: 'KEY', value: 'VALUE' } + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { key: 'KEY', value: 'VALUE' } } + end context 'authorized user with proper permissions' do it 'creates variable for admins', :aggregate_failures do @@ -137,19 +139,21 @@ RSpec.describe ::API::Admin::Ci::Variables do describe 'PUT /admin/ci/variables/:key' do let_it_be(:path) { "/admin/ci/variables/#{variable.key}" } + let_it_be(:params) do + { + variable_type: 'file', + value: 'VALUE_1_UP', + protected: true, + masked: true, + raw: true + } + end it_behaves_like 'PUT request permissions for admin mode' context 'authorized user with proper permissions' do it 'updates variable data', :aggregate_failures do - put api(path, admin, admin_mode: true), - params: { - variable_type: 'file', - value: 'VALUE_1_UP', - protected: true, - masked: true, - raw: true - } + put api(path, admin, admin_mode: true), params: params expect(variable.reload.value).to eq('VALUE_1_UP') expect(variable.reload).to be_protected diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb index 84ebe68b732..0a72f404e89 100644 --- a/spec/requests/api/admin/instance_clusters_spec.rb +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -181,20 +181,9 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man } end - it_behaves_like 'POST request permissions for admin mode', - { - name: 'test-instance-cluster', - domain: 'domain.example.com', - managed: false, - enabled: false, - namespace_per_environment: false, - clusterable: Clusters::Instance.new, - platform_kubernetes_attributes: { - api_url: 'https://example.com', - token: 'sample-token', - authorization_type: 'rbac' - } - } + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { cluster_params } + end include_examples ':certificate_based_clusters feature flag API responses' do let(:subject) { post api(path, admin_user, admin_mode: true), params: cluster_params } @@ -319,7 +308,9 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man let(:path) { "/admin/clusters/#{cluster.id}" } - it_behaves_like 'PUT request permissions for admin mode' + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { update_params } + end include_examples ':certificate_based_clusters feature flag API responses' do let(:subject) { put api(path, admin_user, admin_mode: true), params: update_params } diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb index 4f170a1c86d..dffe062c031 100644 --- a/spec/requests/api/admin/plan_limits_spec.rb +++ b/spec/requests/api/admin/plan_limits_spec.rb @@ -84,7 +84,9 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d end describe 'PUT /application/plan_limits' do - it_behaves_like 'PUT request permissions for admin mode', { 'plan_name': 'default' } + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { 'plan_name': 'default' } } + end context 'as an admin user' do context 'correct params' do diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb index c08ecae28e8..3550e51d585 100644 --- a/spec/requests/api/appearance_spec.rb +++ b/spec/requests/api/appearance_spec.rb @@ -36,7 +36,9 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do end describe "PUT /application/appearance" do - it_behaves_like 'PUT request permissions for admin mode', { title: "Test" } + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { title: "Test" } } + end context 'as an admin user' do context "instance basics" do diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb index 933be87d680..5b07bded82c 100644 --- a/spec/requests/api/applications_spec.rb +++ b/spec/requests/api/applications_spec.rb @@ -10,7 +10,9 @@ RSpec.describe API::Applications, :api, feature_category: :system_access do let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: scopes) } describe 'POST /applications' do - it_behaves_like 'POST request permissions for admin mode', { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' } + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' } } + end context 'authenticated and authorized user' do it 'creates and returns an OAuth application' do diff --git a/spec/requests/api/graphql/project/flow_metrics_spec.rb b/spec/requests/api/graphql/project/flow_metrics_spec.rb index 0bdf7bad8db..3b5758b3a2e 100644 --- a/spec/requests/api/graphql/project/flow_metrics_spec.rb +++ b/spec/requests/api/graphql/project/flow_metrics_spec.rb @@ -6,14 +6,18 @@ RSpec.describe 'getting project flow metrics', feature_category: :value_stream_m include GraphqlHelpers let_it_be(:group) { create(:group) } - let_it_be(:project1) { create(:project, group: group) } + let_it_be(:project1) { create(:project, :repository, group: group) } # This is done so we can use the same count expectations in the shared examples and # reuse the shared example for the group-level test. let_it_be(:project2) { project1 } + let_it_be(:production_environment1) { create(:environment, :production, project: project1) } + let_it_be(:production_environment2) { production_environment1 } let_it_be(:current_user) { create(:user, maintainer_projects: [project1]) } - it_behaves_like 'value stream analytics flow metrics issueCount examples' do - let(:full_path) { project1.full_path } - let(:context) { :project } - end + let(:full_path) { project1.full_path } + let(:context) { :project } + + it_behaves_like 'value stream analytics flow metrics issueCount examples' + + it_behaves_like 'value stream analytics flow metrics deploymentCount examples' end diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb index 046036c40ba..fa78ddf206a 100644 --- a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb +++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb @@ -122,3 +122,84 @@ RSpec.shared_examples 'value stream analytics flow metrics issueCount examples' end end end + +RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examples' do + let_it_be(:deployment1) do + create(:deployment, :success, environment: production_environment1, finished_at: 5.days.ago) + end + + let_it_be(:deployment2) do + create(:deployment, :success, environment: production_environment2, finished_at: 10.days.ago) + end + + let_it_be(:deployment3) do + create(:deployment, :success, environment: production_environment2, finished_at: 15.days.ago) + end + + let(:variables) do + { + path: full_path, + from: 12.days.ago.iso8601, + to: 3.days.ago.iso8601 + } + end + + let(:query) do + <<~QUERY + query($path: ID!, $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + deploymentCount(from: $from, to: $to) { + value + unit + identifier + title + } + } + } + } + QUERY + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'deploymentCount') + end + + it 'returns the correct count' do + expect(result).to eq({ + 'identifier' => 'deploys', + 'unit' => nil, + 'value' => 2, + 'title' => n_('Deploy', 'Deploys', 2) + }) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 20.days.ago.iso8601, + to: 18.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to eq({ + 'identifier' => 'deploys', + 'unit' => nil, + 'value' => 0, + 'title' => n_('Deploy', 'Deploys', 0) + }) + end + end +end diff --git a/spec/support/shared_examples/features/real_time_merge_widget_shared_examples.rb b/spec/support/shared_examples/features/real_time_merge_widget_shared_examples.rb new file mode 100644 index 00000000000..d76d0f74e98 --- /dev/null +++ b/spec/support/shared_examples/features/real_time_merge_widget_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updates merge widget in real-time' do + specify do + wait_for_requests + + # Simulate a real-time update of merge widget + trigger_action + + expect(find('.mr-state-widget')).to have_content(widget_text) + end +end diff --git a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb index 7e5ad751247..2225291661c 100644 --- a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb +++ b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb @@ -4,14 +4,14 @@ RSpec.shared_examples 'GET request permissions for admin mode' do it_behaves_like 'GET request permissions for admin mode when admin' end -RSpec.shared_examples 'PUT request permissions for admin mode' do |params| - it_behaves_like 'PUT request permissions for admin mode when user', params - it_behaves_like 'PUT request permissions for admin mode when admin', params +RSpec.shared_examples 'PUT request permissions for admin mode' do + it_behaves_like 'PUT request permissions for admin mode when user' + it_behaves_like 'PUT request permissions for admin mode when admin' end -RSpec.shared_examples 'POST request permissions for admin mode' do |params| - it_behaves_like 'POST request permissions for admin mode when user', params - it_behaves_like 'POST request permissions for admin mode when admin', params +RSpec.shared_examples 'POST request permissions for admin mode' do + it_behaves_like 'POST request permissions for admin mode when user' + it_behaves_like 'POST request permissions for admin mode when admin' end RSpec.shared_examples 'DELETE request permissions for admin mode' do |success_status_code = :no_content| @@ -37,7 +37,7 @@ RSpec.shared_examples 'GET request permissions for admin mode when admin' do it_behaves_like 'admin mode on', false, :forbidden end -RSpec.shared_examples 'PUT request permissions for admin mode when user' do |params| +RSpec.shared_examples 'PUT request permissions for admin mode when user' do subject { put api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:user) } @@ -46,7 +46,7 @@ RSpec.shared_examples 'PUT request permissions for admin mode when user' do |par it_behaves_like 'admin mode on', false, :forbidden end -RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |params| +RSpec.shared_examples 'PUT request permissions for admin mode when admin' do subject { put api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:admin) } @@ -55,7 +55,7 @@ RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |pa it_behaves_like 'admin mode on', false, :forbidden end -RSpec.shared_examples 'POST request permissions for admin mode when user' do |params| +RSpec.shared_examples 'POST request permissions for admin mode when user' do subject { post api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:user) } @@ -64,7 +64,7 @@ RSpec.shared_examples 'POST request permissions for admin mode when user' do |pa it_behaves_like 'admin mode on', false, :forbidden end -RSpec.shared_examples 'POST request permissions for admin mode when admin' do |params| +RSpec.shared_examples 'POST request permissions for admin mode when admin' do subject { post api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:admin) } diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb index 22f36309a7c..33f4bca4c85 100644 --- a/spec/tasks/gitlab/feature_categories_rake_spec.rb +++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb @@ -21,7 +21,7 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ ) ), 'api_endpoints' => a_hash_including( - 'authentication_and_authorization' => a_collection_including( + 'system_access' => a_collection_including( klass: 'API::AccessRequests', action: '/groups/:id/access_requests', source_location: [ diff --git a/yarn.lock b/yarn.lock index 45ddeebeceb..2b810df506e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1832,6 +1832,11 @@ resolved "https://registry.yarnpkg.com/@linaria/core/-/core-3.0.0-beta.13.tgz#049c5be5faa67e341e413a0f6b641d5d78d91056" integrity sha512-3zEi5plBCOsEzUneRVuQb+2SAx3qaC1dj0FfFAI6zIJQoDWu0dlSwKijMRack7oO9tUWrchfj3OkKQAd1LBdVg== +"@mattiasbuelens/web-streams-adapter@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz#607b5a25682f4ae2741da7ba6df39302505336b3" + integrity sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA== + "@miragejs/pretender-node-polyfill@^0.1.0": version "0.1.2" resolved "https://registry.yarnpkg.com/@miragejs/pretender-node-polyfill/-/pretender-node-polyfill-0.1.2.tgz#d26b6b7483fb70cd62189d05c95d2f67153e43f2" |