Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ac34bff890
commit
4c61af12af
|
|
@ -16,7 +16,7 @@ variables:
|
|||
# Retry failed specs in separate process
|
||||
QA_RETRY_FAILED_SPECS: "true"
|
||||
# Helm chart ref used by test-on-cng pipeline
|
||||
GITLAB_HELM_CHART_REF: "0e35aaeaaf6a85ab70a89c67c5f39bc08f11d669"
|
||||
GITLAB_HELM_CHART_REF: "5613c3aaf1cacdc6df7e05b96de980be74742116"
|
||||
# Specific ref for cng-mirror project to trigger builds for
|
||||
GITLAB_CNG_MIRROR_REF: "df7aafcccafdbab732a7cf757efb3b7b74c851dd"
|
||||
# Makes sure some of the common scripts from pipeline-common use bundler to execute commands
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
42c36c24adc64b5d1b0bb467873d67c923c8a612
|
||||
b285b9aba37c9b2605d693275f6a0a8dfbbca160
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
{"name":"devise","version":"4.9.4","platform":"ruby","checksum":"920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8"},
|
||||
{"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"},
|
||||
{"name":"diff-lcs","version":"1.5.0","platform":"ruby","checksum":"49b934001c8c6aedb37ba19daec5c634da27b318a7a3c654ae979d6ba1929b67"},
|
||||
{"name":"diffy","version":"3.4.3","platform":"ruby","checksum":"4264b9e7db00d1cd426fcd32e36565779163cedc2340a95b0e6f025e71f9aaa7"},
|
||||
{"name":"diffy","version":"3.4.4","platform":"ruby","checksum":"79384ab5ca82d0e115b2771f0961e27c164c456074bd2ec46b637ebf7b6e47e3"},
|
||||
{"name":"digest-crc","version":"0.6.5","platform":"ruby","checksum":"5ca456f3352dc5ff17eb95deb3dd5a79dc79f8bf751d8005abca5b7b9b252124"},
|
||||
{"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"},
|
||||
{"name":"domain_name","version":"0.5.20190701","platform":"ruby","checksum":"000a600454cb4a344769b2f10b531765ea7bd3a304fe47ed12e5ca1eab969851"},
|
||||
|
|
@ -218,7 +218,7 @@
|
|||
{"name":"gitaly","version":"18.1.0.pre.rc1","platform":"ruby","checksum":"8f65a0c5bb3694c91c9fa4bfa7ceabfc131846b78feed8ee32a744aaacf6e70a"},
|
||||
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
|
||||
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.14.0","platform":"ruby","checksum":"a5f75d8891b8e61dcb380069fced461d71fba8105875c19bcf5921f9a4b8d8cc"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.15.0","platform":"ruby","checksum":"19c45cd38e0d8721c61809bb05a4d593a365854bb60bb7e78ad765613d668193"},
|
||||
{"name":"gitlab-crystalball","version":"1.0.0","platform":"ruby","checksum":"74f56646345a5bc130da64ee5c2a90fad1bd70b26b551928676030fddaf76201"},
|
||||
{"name":"gitlab-dangerfiles","version":"4.9.2","platform":"ruby","checksum":"d5c050f685d8720f6e70191a7d1216854d860dbdea5b455f87abe7542e005798"},
|
||||
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ GEM
|
|||
railties (~> 7.0)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.0)
|
||||
diffy (3.4.3)
|
||||
diffy (3.4.4)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
|
|
@ -744,7 +744,7 @@ GEM
|
|||
terminal-table (>= 1.5.1)
|
||||
gitlab-chronic (0.10.6)
|
||||
numerizer (~> 0.2)
|
||||
gitlab-cloud-connector (1.14.0)
|
||||
gitlab-cloud-connector (1.15.0)
|
||||
activesupport (~> 7.0)
|
||||
jwt (~> 2.9.3)
|
||||
gitlab-crystalball (1.0.0)
|
||||
|
|
@ -2429,4 +2429,4 @@ DEPENDENCIES
|
|||
yard (~> 0.9)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.5
|
||||
2.6.9
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
{"name":"devise","version":"4.9.4","platform":"ruby","checksum":"920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8"},
|
||||
{"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"},
|
||||
{"name":"diff-lcs","version":"1.5.0","platform":"ruby","checksum":"49b934001c8c6aedb37ba19daec5c634da27b318a7a3c654ae979d6ba1929b67"},
|
||||
{"name":"diffy","version":"3.4.3","platform":"ruby","checksum":"4264b9e7db00d1cd426fcd32e36565779163cedc2340a95b0e6f025e71f9aaa7"},
|
||||
{"name":"diffy","version":"3.4.4","platform":"ruby","checksum":"79384ab5ca82d0e115b2771f0961e27c164c456074bd2ec46b637ebf7b6e47e3"},
|
||||
{"name":"digest-crc","version":"0.6.5","platform":"ruby","checksum":"5ca456f3352dc5ff17eb95deb3dd5a79dc79f8bf751d8005abca5b7b9b252124"},
|
||||
{"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"},
|
||||
{"name":"domain_name","version":"0.5.20190701","platform":"ruby","checksum":"000a600454cb4a344769b2f10b531765ea7bd3a304fe47ed12e5ca1eab969851"},
|
||||
|
|
@ -218,7 +218,7 @@
|
|||
{"name":"gitaly","version":"18.1.0.pre.rc1","platform":"ruby","checksum":"8f65a0c5bb3694c91c9fa4bfa7ceabfc131846b78feed8ee32a744aaacf6e70a"},
|
||||
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
|
||||
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.14.0","platform":"ruby","checksum":"a5f75d8891b8e61dcb380069fced461d71fba8105875c19bcf5921f9a4b8d8cc"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.15.0","platform":"ruby","checksum":"19c45cd38e0d8721c61809bb05a4d593a365854bb60bb7e78ad765613d668193"},
|
||||
{"name":"gitlab-crystalball","version":"1.0.0","platform":"ruby","checksum":"74f56646345a5bc130da64ee5c2a90fad1bd70b26b551928676030fddaf76201"},
|
||||
{"name":"gitlab-dangerfiles","version":"4.9.2","platform":"ruby","checksum":"d5c050f685d8720f6e70191a7d1216854d860dbdea5b455f87abe7542e005798"},
|
||||
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ GEM
|
|||
railties (~> 7.0)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.0)
|
||||
diffy (3.4.3)
|
||||
diffy (3.4.4)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
|
|
@ -738,7 +738,7 @@ GEM
|
|||
terminal-table (>= 1.5.1)
|
||||
gitlab-chronic (0.10.6)
|
||||
numerizer (~> 0.2)
|
||||
gitlab-cloud-connector (1.14.0)
|
||||
gitlab-cloud-connector (1.15.0)
|
||||
activesupport (~> 7.0)
|
||||
jwt (~> 2.9.3)
|
||||
gitlab-crystalball (1.0.0)
|
||||
|
|
@ -2424,4 +2424,4 @@ DEPENDENCIES
|
|||
yard (~> 0.9)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.5
|
||||
2.6.9
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@
|
|||
query boardsGetGroupProjects($fullPath: ID!, $search: String, $after: String) {
|
||||
group(fullPath: $fullPath) {
|
||||
id
|
||||
projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
|
||||
projects(
|
||||
search: $search
|
||||
after: $after
|
||||
first: 100
|
||||
includeSubgroups: true
|
||||
withIssuesEnabled: true
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-alert variant="success" class="gl-mb-5" @dismiss="setToken(null)">
|
||||
<gl-alert
|
||||
data-testid="new-access-token"
|
||||
variant="success"
|
||||
class="gl-mb-5"
|
||||
@dismiss="setToken(null)"
|
||||
>
|
||||
<input-copy-toggle-visibility
|
||||
:copy-button-title="s__('AccessTokens|Copy token')"
|
||||
:label="s__('AccessTokens|Your token')"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { GlSingleStat } from '@gitlab/ui/dist/charts';
|
|||
import { mapActions, mapState } from 'pinia';
|
||||
import { useAccessTokens } from '../stores/access_tokens';
|
||||
|
||||
import { slugify } from '../../../lib/utils/text_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
|
|
@ -20,6 +22,10 @@ export default {
|
|||
this.setPage(1);
|
||||
this.fetchTokens();
|
||||
},
|
||||
slugifyStat(stat) {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return `${slugify(stat)}-count`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -27,7 +33,12 @@ export default {
|
|||
<template>
|
||||
<div class="gl-my-5 gl-grid gl-gap-4 sm:gl-grid-cols-2 lg:gl-grid-cols-4">
|
||||
<gl-card v-for="statistic in statistics" :key="statistic.title">
|
||||
<gl-single-stat class="!gl-p-0" :title="statistic.title" :value="statistic.value" />
|
||||
<gl-single-stat
|
||||
class="!gl-p-0"
|
||||
:data-testid="slugifyStat(statistic.title)"
|
||||
:title="statistic.title"
|
||||
:value="statistic.value"
|
||||
/>
|
||||
<gl-button
|
||||
class="mt-2"
|
||||
:title="statistic.tooltipTitle"
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<gl-table
|
||||
data-testid="access-token-table"
|
||||
:items="tokens"
|
||||
:fields="$options.fields"
|
||||
:empty-text="s__('AccessTokens|No access tokens')"
|
||||
|
|
@ -279,6 +280,7 @@ export default {
|
|||
<template #cell(options)="{ item }">
|
||||
<gl-disclosure-dropdown
|
||||
v-if="item.active"
|
||||
data-testid="access-token-options"
|
||||
icon="ellipsis_v"
|
||||
:no-caret="true"
|
||||
:disabled="busy"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ module Types
|
|||
class Base < BasePermissionType
|
||||
graphql_name 'NamespacePermissions'
|
||||
|
||||
abilities :admin_label
|
||||
abilities :admin_label, :admin_issue, :create_work_item,
|
||||
:import_issues, :read_crm_contact, :read_crm_organization
|
||||
|
||||
ability_field :read_namespace
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,3 +34,5 @@ module Types
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Types::WorkItems::NegatedWorkItemFilterInputType.prepend_mod_with('Types::WorkItems::NegatedWorkItemFilterInputType')
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ module Types
|
|||
description: 'Whether the current user is subscribed to notifications on the work item.'
|
||||
|
||||
def subscribed
|
||||
object.work_item.subscribed?(current_user, object.work_item.project)
|
||||
object.work_item.subscribed?(current_user, object.work_item.project, cache_enforced: false)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Graphql/AuthorizeTypes
|
||||
|
|
|
|||
|
|
@ -17,20 +17,21 @@ module Subscribable
|
|||
scope :explicitly_unsubscribed, ->(user) { joins(:subscriptions).where(subscriptions: { user_id: user.id, subscribed: false }) }
|
||||
end
|
||||
|
||||
def subscribed?(user, project = nil)
|
||||
def subscribed?(user, project = nil, cache_enforced: true)
|
||||
return false unless user
|
||||
|
||||
if (subscription = lazy_subscription(user, project)&.itself)
|
||||
if (subscription = lazy_subscription(user, project, cache_enforced: cache_enforced)&.itself)
|
||||
subscription.subscribed
|
||||
else
|
||||
subscribed_without_subscriptions?(user, project)
|
||||
end
|
||||
end
|
||||
|
||||
def lazy_subscription(user, project = nil)
|
||||
def lazy_subscription(user, project = nil, cache_enforced: true)
|
||||
return unless user
|
||||
|
||||
BatchLoader.for(id: id, subscribable_type: subscribable_type, project_id: project&.id).batch do |items, loader|
|
||||
BatchLoader.for(id: id, subscribable_type: subscribable_type, project_id: project&.id)
|
||||
.batch(cache: cache_enforced) do |items, loader|
|
||||
values = items.each_with_object({ ids: Set.new, subscribable_types: Set.new, project_ids: Set.new }) do |item, result|
|
||||
result[:ids] << item[:id]
|
||||
result[:subscribable_types] << item[:subscribable_type]
|
||||
|
|
|
|||
|
|
@ -1103,10 +1103,6 @@ class Group < Namespace
|
|||
feature_flag_enabled_for_self_or_ancestor?(:continue_indented_text, type: :wip)
|
||||
end
|
||||
|
||||
def markdown_placeholders_feature_flag_enabled?
|
||||
feature_flag_enabled_for_self_or_ancestor?(:markdown_placeholders, type: :gitlab_com_derisk)
|
||||
end
|
||||
|
||||
def glql_integration_feature_flag_enabled?
|
||||
feature_flag_enabled_for_self_or_ancestor?(:glql_integration)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3438,10 +3438,6 @@ class Project < ApplicationRecord
|
|||
group&.continue_indented_text_feature_flag_enabled? || Feature.enabled?(:continue_indented_text, self, type: :wip)
|
||||
end
|
||||
|
||||
def markdown_placeholders_feature_flag_enabled?
|
||||
group&.markdown_placeholders_feature_flag_enabled? || Feature.enabled?(:markdown_placeholders, self, type: :gitlab_com_derisk)
|
||||
end
|
||||
|
||||
def enqueue_record_project_target_platforms
|
||||
return unless Gitlab.com?
|
||||
|
||||
|
|
|
|||
|
|
@ -57,14 +57,25 @@ module Notes
|
|||
update_params[:spend_time][:note_id] = note.id
|
||||
end
|
||||
|
||||
service_response = execute_update_service(note, update_params)
|
||||
execute_triggers(note, update_params)
|
||||
execute_update_service(note, update_params)
|
||||
|
||||
service_response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def execute_triggers(note, params)
|
||||
# This is overridden in EE
|
||||
trigger_work_item_updated(note, params)
|
||||
end
|
||||
|
||||
def trigger_work_item_updated(note, params)
|
||||
GraphqlTriggers.work_item_updated(note.noteable) if quick_action_requires_subscription_update?(params) &&
|
||||
note.for_work_item?
|
||||
end
|
||||
|
||||
def quick_action_requires_subscription_update?(update_params)
|
||||
update_params.has_key?(:subscription_event)
|
||||
end
|
||||
|
||||
def execute_update_service(note, params)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,18 @@ module Packages
|
|||
class GenerateDistributionKeyService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
NEWLINE_REGEX = /\R/
|
||||
INVALID_PASSPHRASE_ERROR = ServiceResponse.error(
|
||||
message: 'Passphrase contains invalid characters', reason: :invalid_passphrase
|
||||
).freeze
|
||||
|
||||
def initialize(params: {})
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
return INVALID_PASSPHRASE_ERROR if invalid_passphrase?
|
||||
|
||||
using_pinentry do |ctx|
|
||||
# Generate key
|
||||
ctx.generate_key generate_key_params
|
||||
|
|
@ -29,12 +36,14 @@ module Packages
|
|||
data.seek 0
|
||||
public_key = data.read
|
||||
|
||||
{
|
||||
private_key: private_key,
|
||||
public_key: public_key,
|
||||
passphrase: passphrase,
|
||||
fingerprint: fingerprint
|
||||
}
|
||||
ServiceResponse.success(
|
||||
payload: {
|
||||
private_key: private_key,
|
||||
public_key: public_key,
|
||||
passphrase: passphrase,
|
||||
fingerprint: fingerprint
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -93,6 +102,10 @@ module Packages
|
|||
}.map { |k, v| "#{k}: #{v}\n" }.join +
|
||||
'</GnupgKeyParms>'
|
||||
end
|
||||
|
||||
def invalid_passphrase?
|
||||
params[:passphrase]&.match?(NEWLINE_REGEX)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ module Packages
|
|||
Installer-Menu-Item
|
||||
].freeze
|
||||
|
||||
GenerateDistributionError = Class.new(StandardError)
|
||||
|
||||
def initialize(distribution)
|
||||
@distribution = distribution
|
||||
@oldest_kept_generated_at = nil
|
||||
|
|
@ -202,7 +204,8 @@ module Packages
|
|||
end
|
||||
|
||||
def generate_release
|
||||
@distribution.key || @distribution.create_key(GenerateDistributionKeyService.new.execute)
|
||||
generate_distribution_key unless @distribution.key
|
||||
|
||||
@distribution.file = CarrierWaveStringFile.new(release_content)
|
||||
@distribution.file_signature = SignDistributionService.new(@distribution, release_content, detach: true).execute
|
||||
@distribution.signed_file = CarrierWaveStringFile.new(
|
||||
|
|
@ -272,6 +275,14 @@ module Packages
|
|||
def lease_timeout
|
||||
DEFAULT_LEASE_TIMEOUT
|
||||
end
|
||||
|
||||
def generate_distribution_key
|
||||
response = GenerateDistributionKeyService.new.execute
|
||||
|
||||
raise GenerateDistributionError, response.message unless response.success?
|
||||
|
||||
@distribution.create_key(response.payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: duo_code_review_full_file
|
||||
description:
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/510266
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189314
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/537556
|
||||
milestone: '18.0'
|
||||
group: group::code review
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: markdown_placeholders
|
||||
description:
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/14389
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186028
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/544860
|
||||
milestone: '18.1'
|
||||
group: group::knowledge
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -5,4 +5,4 @@ feature_category: package_registry
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174312
|
||||
milestone: '17.7'
|
||||
queued_migration_version: 20241201164238
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: '20250601231655'
|
||||
|
|
|
|||
|
|
@ -9,14 +9,6 @@ description: Tracks past and present on-call shifts
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49423
|
||||
milestone: '13.8'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
desired_sharding_key:
|
||||
project_id:
|
||||
references: projects
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: rotation_id
|
||||
table: incident_management_oncall_rotations
|
||||
sharding_key: project_id
|
||||
belongs_to: rotation
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
desired_sharding_key_migration_job_name: BackfillIncidentManagementOncallShiftsProjectId
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeHkUpdateStatusForDeprecatedNpmPackages < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'UpdateStatusForDeprecatedNpmPackages',
|
||||
table_name: :packages_npm_metadata,
|
||||
column_name: :package_id,
|
||||
job_arguments: [],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIncidentManagementOncallShiftsProjectIdNotNull < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint :incident_management_oncall_shifts, :project_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :incident_management_oncall_shifts, :project_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
a656fc6add5b7e261a8f05f0a559ac3bdaabd59d723a7490386e0c290b116e36
|
||||
|
|
@ -0,0 +1 @@
|
|||
35a5bf92efe206f7e709919069942482a4619d307e988d478c1f1818e479177b
|
||||
|
|
@ -15701,7 +15701,8 @@ CREATE TABLE incident_management_oncall_shifts (
|
|||
participant_id bigint NOT NULL,
|
||||
starts_at timestamp with time zone NOT NULL,
|
||||
ends_at timestamp with time zone NOT NULL,
|
||||
project_id bigint
|
||||
project_id bigint,
|
||||
CONSTRAINT check_a37955f387 CHECK ((project_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE incident_management_oncall_shifts_id_seq
|
||||
|
|
|
|||
|
|
@ -29781,7 +29781,7 @@ Returns [`WorkItemStateCountsType`](#workitemstatecountstype).
|
|||
| <a id="groupworkitemstatecountsdueafter"></a>`dueAfter` | [`Time`](#time) | Work items due after the timestamp. |
|
||||
| <a id="groupworkitemstatecountsduebefore"></a>`dueBefore` | [`Time`](#time) | Work items due before the timestamp. |
|
||||
| <a id="groupworkitemstatecountsexcludeprojects"></a>`excludeProjects` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Introduced** in GitLab 17.5. **Status**: Experiment. Exclude work items from projects within the group. |
|
||||
| <a id="groupworkitemstatecountshealthstatus"></a>`healthStatus` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="groupworkitemstatecountshealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="groupworkitemstatecountsiid"></a>`iid` | [`String`](#string) | IID of the work item. For example, "1". |
|
||||
| <a id="groupworkitemstatecountsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of work items. For example, `["1", "2"]`. |
|
||||
| <a id="groupworkitemstatecountsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
|
||||
|
|
@ -29808,6 +29808,7 @@ Returns [`WorkItemStateCountsType`](#workitemstatecountstype).
|
|||
| <a id="groupworkitemstatecountsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Work items updated before the timestamp. |
|
||||
| <a id="groupworkitemstatecountsverificationstatuswidget"></a>`verificationStatusWidget` | [`VerificationStatusFilterInput`](#verificationstatusfilterinput) | Input for verification status widget filter. Ignored if `work_items_alpha` is disabled. |
|
||||
| <a id="groupworkitemstatecountsweight"></a>`weight` | [`String`](#string) | Weight applied to the work item, "none" and "any" values are supported. |
|
||||
| <a id="groupworkitemstatecountsweightwildcardid"></a>`weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. |
|
||||
|
||||
##### `Group.workItemTypes`
|
||||
|
||||
|
|
@ -29858,7 +29859,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="groupworkitemsdueafter"></a>`dueAfter` | [`Time`](#time) | Work items due after the timestamp. |
|
||||
| <a id="groupworkitemsduebefore"></a>`dueBefore` | [`Time`](#time) | Work items due before the timestamp. |
|
||||
| <a id="groupworkitemsexcludeprojects"></a>`excludeProjects` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Introduced** in GitLab 17.5. **Status**: Experiment. Exclude work items from projects within the group. |
|
||||
| <a id="groupworkitemshealthstatus"></a>`healthStatus` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="groupworkitemshealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="groupworkitemsiid"></a>`iid` | [`String`](#string) | IID of the work item. For example, "1". |
|
||||
| <a id="groupworkitemsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of work items. For example, `["1", "2"]`. |
|
||||
| <a id="groupworkitemsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
|
||||
|
|
@ -29885,6 +29886,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="groupworkitemsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Work items updated before the timestamp. |
|
||||
| <a id="groupworkitemsverificationstatuswidget"></a>`verificationStatusWidget` | [`VerificationStatusFilterInput`](#verificationstatusfilterinput) | Input for verification status widget filter. Ignored if `work_items_alpha` is disabled. |
|
||||
| <a id="groupworkitemsweight"></a>`weight` | [`String`](#string) | Weight applied to the work item, "none" and "any" values are supported. |
|
||||
| <a id="groupworkitemsweightwildcardid"></a>`weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. |
|
||||
|
||||
##### `Group.workspacesClusterAgents`
|
||||
|
||||
|
|
@ -34198,8 +34200,13 @@ Represents a namespace-cluster-agent mapping.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="namespacepermissionsadminissue"></a>`adminIssue` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_issue` on this resource. |
|
||||
| <a id="namespacepermissionsadminlabel"></a>`adminLabel` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_label` on this resource. |
|
||||
| <a id="namespacepermissionscreateworkitem"></a>`createWorkItem` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_work_item` on this resource. |
|
||||
| <a id="namespacepermissionsgeneratedescription"></a>`generateDescription` | [`Boolean!`](#boolean) | If `true`, the user can perform `generate_description` on this resource. |
|
||||
| <a id="namespacepermissionsimportissues"></a>`importIssues` | [`Boolean!`](#boolean) | If `true`, the user can perform `import_issues` on this resource. |
|
||||
| <a id="namespacepermissionsreadcrmcontact"></a>`readCrmContact` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_crm_contact` on this resource. |
|
||||
| <a id="namespacepermissionsreadcrmorganization"></a>`readCrmOrganization` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_crm_organization` on this resource. |
|
||||
| <a id="namespacepermissionsreadnamespace"></a>`readNamespace` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_namespace` on this resource. |
|
||||
|
||||
### `NamespaceSidebar`
|
||||
|
|
@ -35710,6 +35717,7 @@ Project-level settings for product analytics provider.
|
|||
| <a id="projectcontainerprotectiontagrules"></a>`containerProtectionTagRules` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleConnection`](#containerprotectiontagruleconnection) | **Introduced** in GitLab 17.8. **Status**: Experiment. Container repository tag protection rules for the project. |
|
||||
| <a id="projectcontainerregistryenabled"></a>`containerRegistryEnabled` | [`Boolean`](#boolean) | Indicates if Container registry is enabled for the current user. |
|
||||
| <a id="projectcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the project. |
|
||||
| <a id="projectcontainerscanningforregistryenabled"></a>`containerScanningForRegistryEnabled` | [`Boolean`](#boolean) | Indicates whether Container Scanning for Registry is enabled or not for the project. Returns `null` if unauthorized. |
|
||||
| <a id="projectcontainertagsexpirationpolicy"></a>`containerTagsExpirationPolicy` | [`ContainerTagsExpirationPolicy`](#containertagsexpirationpolicy) | Container tags expiration policy of the project. |
|
||||
| <a id="projectcorpuses"></a>`corpuses` | [`CoverageFuzzingCorpusConnection`](#coveragefuzzingcorpusconnection) | Find corpuses of the project. (see [Connections](#connections)) |
|
||||
| <a id="projectcreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of the project creation. |
|
||||
|
|
@ -38003,7 +38011,7 @@ Returns [`WorkItemStateCountsType`](#workitemstatecountstype).
|
|||
| <a id="projectworkitemstatecountscustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. |
|
||||
| <a id="projectworkitemstatecountsdueafter"></a>`dueAfter` | [`Time`](#time) | Work items due after the timestamp. |
|
||||
| <a id="projectworkitemstatecountsduebefore"></a>`dueBefore` | [`Time`](#time) | Work items due before the timestamp. |
|
||||
| <a id="projectworkitemstatecountshealthstatus"></a>`healthStatus` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="projectworkitemstatecountshealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="projectworkitemstatecountsiid"></a>`iid` | [`String`](#string) | IID of the work item. For example, "1". |
|
||||
| <a id="projectworkitemstatecountsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of work items. For example, `["1", "2"]`. |
|
||||
| <a id="projectworkitemstatecountsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
|
||||
|
|
@ -38027,6 +38035,7 @@ Returns [`WorkItemStateCountsType`](#workitemstatecountstype).
|
|||
| <a id="projectworkitemstatecountsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Work items updated before the timestamp. |
|
||||
| <a id="projectworkitemstatecountsverificationstatuswidget"></a>`verificationStatusWidget` | [`VerificationStatusFilterInput`](#verificationstatusfilterinput) | Input for verification status widget filter. Ignored if `work_items_alpha` is disabled. |
|
||||
| <a id="projectworkitemstatecountsweight"></a>`weight` | [`String`](#string) | Weight applied to the work item, "none" and "any" values are supported. |
|
||||
| <a id="projectworkitemstatecountsweightwildcardid"></a>`weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. |
|
||||
|
||||
##### `Project.workItemTypes`
|
||||
|
||||
|
|
@ -38076,7 +38085,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="projectworkitemscustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. |
|
||||
| <a id="projectworkitemsdueafter"></a>`dueAfter` | [`Time`](#time) | Work items due after the timestamp. |
|
||||
| <a id="projectworkitemsduebefore"></a>`dueBefore` | [`Time`](#time) | Work items due before the timestamp. |
|
||||
| <a id="projectworkitemshealthstatus"></a>`healthStatus` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="projectworkitemshealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the work item, "none" and "any" values are supported. |
|
||||
| <a id="projectworkitemsiid"></a>`iid` | [`String`](#string) | IID of the work item. For example, "1". |
|
||||
| <a id="projectworkitemsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of work items. For example, `["1", "2"]`. |
|
||||
| <a id="projectworkitemsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
|
||||
|
|
@ -38100,6 +38109,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="projectworkitemsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Work items updated before the timestamp. |
|
||||
| <a id="projectworkitemsverificationstatuswidget"></a>`verificationStatusWidget` | [`VerificationStatusFilterInput`](#verificationstatusfilterinput) | Input for verification status widget filter. Ignored if `work_items_alpha` is disabled. |
|
||||
| <a id="projectworkitemsweight"></a>`weight` | [`String`](#string) | Weight applied to the work item, "none" and "any" values are supported. |
|
||||
| <a id="projectworkitemsweightwildcardid"></a>`weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. |
|
||||
|
||||
### `ProjectCiCdSetting`
|
||||
|
||||
|
|
@ -50864,12 +50874,14 @@ A year and month input for querying product analytics usage data.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="negatedworkitemfilterinputassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users not assigned to the work item. |
|
||||
| <a id="negatedworkitemfilterinputauthorusername"></a>`authorUsername` | [`[String!]`](#string) | Username of a user who didn't author the work item. |
|
||||
| <a id="negatedworkitemfilterinputhealthstatusfilter"></a>`healthStatusFilter` | [`[HealthStatus!]`](#healthstatus) | Health status not applied to the work items. Includes work items where health status is not set. |
|
||||
| <a id="negatedworkitemfilterinputlabelname"></a>`labelName` | [`[String!]`](#string) | Labels not applied to the work item. |
|
||||
| <a id="negatedworkitemfilterinputmilestonetitle"></a>`milestoneTitle` | [`[String!]`](#string) | Milestone not applied to the work item. |
|
||||
| <a id="negatedworkitemfilterinputmilestonewildcardid"></a>`milestoneWildcardId` | [`NegatedMilestoneWildcardId`](#negatedmilestonewildcardid) | Filter by negated milestone wildcard values. |
|
||||
| <a id="negatedworkitemfilterinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji not applied by the current user. |
|
||||
| <a id="negatedworkitemfilterinputreleasetag"></a>`releaseTag` | [`[String!]`](#string) | Release tag not associated with the work items's milestone. Ignored when parent is a group. |
|
||||
| <a id="negatedworkitemfilterinputtypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter out work items by the given types. |
|
||||
| <a id="negatedworkitemfilterinputweight"></a>`weight` | [`String`](#string) | Weight not applied to the work items. |
|
||||
|
||||
### `OncallRotationActivePeriodInputType`
|
||||
|
||||
|
|
|
|||
|
|
@ -732,6 +732,109 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
### List all SAML users
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium, Ultimate
|
||||
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193748) in GitLab 18.1.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Lists all SAML users for a given top-level group.
|
||||
|
||||
Use the `page` and `per_page` [pagination parameters](rest/_index.md#offset-based-pagination) to filter the results.
|
||||
|
||||
```plaintext
|
||||
GET /groups/:id/saml_users
|
||||
```
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-----------------|:---------------|:---------|:------------|
|
||||
| `id` | integer/string | yes | ID or [URL-encoded path](rest/_index.md#namespaced-paths) of a top-level group. |
|
||||
| `username` | string | no | Return a user with a given username. |
|
||||
| `search` | string | no | Return users with a matching name, email, or username. Use partial values to increase results. |
|
||||
| `active` | boolean | no | Return only active users. |
|
||||
| `blocked` | boolean | no | Return only blocked users. |
|
||||
| `created_after` | datetime | no | Return users created after the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). |
|
||||
| `created_before` | datetime | no | Return users created before the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/saml_users"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 66,
|
||||
"username": "user22",
|
||||
"name": "Sidney Jones22",
|
||||
"state": "active",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/xxx?s=80&d=identicon",
|
||||
"web_url": "http://my.gitlab.com/user22",
|
||||
"created_at": "2021-09-10T12:48:22.381Z",
|
||||
"bio": "",
|
||||
"location": null,
|
||||
"public_email": "",
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
"twitter": "",
|
||||
"website_url": "",
|
||||
"organization": null,
|
||||
"job_title": "",
|
||||
"pronouns": null,
|
||||
"bot": false,
|
||||
"work_information": null,
|
||||
"followers": 0,
|
||||
"following": 0,
|
||||
"local_time": null,
|
||||
"last_sign_in_at": null,
|
||||
"confirmed_at": "2021-09-10T12:48:22.330Z",
|
||||
"last_activity_on": null,
|
||||
"email": "user22@example.org",
|
||||
"theme_id": 1,
|
||||
"color_scheme_id": 1,
|
||||
"projects_limit": 100000,
|
||||
"current_sign_in_at": null,
|
||||
"identities": [
|
||||
{
|
||||
"provider": "group_saml",
|
||||
"extern_uid": "2435223452345",
|
||||
"saml_provider_id": 1
|
||||
}
|
||||
],
|
||||
"can_create_group": true,
|
||||
"can_create_project": true,
|
||||
"two_factor_enabled": false,
|
||||
"external": false,
|
||||
"private_profile": false,
|
||||
"commit_email": "user22@example.org",
|
||||
"shared_runners_minutes_limit": null,
|
||||
"extra_shared_runners_minutes_limit": null,
|
||||
"scim_identities": [
|
||||
{
|
||||
"extern_uid": "2435223452345",
|
||||
"group_id": 1,
|
||||
"active": true
|
||||
}
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### List provisioned users
|
||||
|
||||
{{< details >}}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ title: Project badges API
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
## Placeholder tokens
|
||||
Use this API to manage and review [project badges](../user/project/badges.md).
|
||||
|
||||
[Badges](../user/project/badges.md) support placeholders that are replaced in real-time in both the link and image URL. The allowed placeholders are:>
|
||||
Badges can include placeholders that are replaced in both the
|
||||
link and image URL. The available placeholders are:
|
||||
|
||||
- `%{project_path}`: Replaced by the project path.
|
||||
- `%{project_title}`: Replaced by the project title.
|
||||
|
|
@ -38,7 +39,7 @@ GET /projects/:id/badges
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `name` | string | no | Name of the badges to return (case-sensitive). |
|
||||
|
||||
```shell
|
||||
|
|
@ -81,7 +82,7 @@ GET /projects/:id/badges/:badge_id
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `badge_id` | integer | yes | The badge ID |
|
||||
|
||||
```shell
|
||||
|
|
@ -113,7 +114,7 @@ POST /projects/:id/badges
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `link_url` | string | yes | URL of the badge link |
|
||||
| `image_url` | string | yes | URL of the badge image |
|
||||
| `name` | string | no | Name of the badge |
|
||||
|
|
@ -149,7 +150,7 @@ PUT /projects/:id/badges/:badge_id
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `badge_id` | integer | yes | The badge ID |
|
||||
| `link_url` | string | no | URL of the badge link |
|
||||
| `image_url` | string | no | URL of the badge image |
|
||||
|
|
@ -185,7 +186,7 @@ DELETE /projects/:id/badges/:badge_id
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `badge_id` | integer | yes | The badge ID |
|
||||
|
||||
```shell
|
||||
|
|
@ -204,7 +205,7 @@ GET /projects/:id/badges/render
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `link_url` | string | yes | URL of the badge link|
|
||||
| `image_url` | string | yes | URL of the badge image |
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,21 @@ title: Protected branches API
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
Use this API to manage [branch protections](../user/project/repository/branches/protected.md)
|
||||
for your repositories.
|
||||
|
||||
GitLab Premium and GitLab Ultimate support more granular protections for pushing to branches.
|
||||
Administrators can grant permission to modify and push to protected branches only to deploy keys,
|
||||
instead of specific users.
|
||||
|
||||
## Valid access levels
|
||||
|
||||
The access levels are defined in the `ProtectedRefAccess.allowed_access_levels` method. The following levels are recognized:
|
||||
The `ProtectedRefAccess.allowed_access_levels` method defines the following access levels:
|
||||
|
||||
```plaintext
|
||||
0 => No access
|
||||
30 => Developer access
|
||||
40 => Maintainer access
|
||||
60 => Admin access
|
||||
```
|
||||
- `0`: No access
|
||||
- `30`: Developer role
|
||||
- `40`: Maintainer role
|
||||
- `60`: Administrator
|
||||
|
||||
## List protected branches
|
||||
|
||||
|
|
@ -41,7 +46,7 @@ GET /projects/:id/protected_branches
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `search` | string | no | Name or part of the name of protected branches to be searched for |
|
||||
|
||||
In the following example, the project ID is `5`.
|
||||
|
|
@ -168,7 +173,7 @@ GET /projects/:id/protected_branches/:name
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `name` | string | yes | The name of the branch or wildcard |
|
||||
|
||||
In the following example, the project ID is `5` and branch name is `main`:
|
||||
|
|
@ -252,7 +257,7 @@ POST /projects/:id/protected_branches
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| -------------------------------------------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
|
||||
| `name` | string | yes | The name of the branch or wildcard. |
|
||||
| `allow_force_push` | boolean | no | When enabled, members who can push to this branch can also force push. (default: `false`) |
|
||||
| `allowed_to_merge` | array | no | Array of merge access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. |
|
||||
|
|
@ -585,7 +590,7 @@ DELETE /projects/:id/protected_branches/:name
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `name` | string | yes | The name of the branch |
|
||||
|
||||
In the following example, the project ID is `5` and branch name is `*-stable`.
|
||||
|
|
@ -612,7 +617,7 @@ PATCH /projects/:id/protected_branches/:name
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| -------------------------------------------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
|
||||
| `name` | string | yes | The name of the branch or wildcard. |
|
||||
| `allow_force_push` | boolean | no | When enabled, members who can push to this branch can also force push. |
|
||||
| `allowed_to_merge` | array | no | Array of merge access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. |
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ title: Protected tags API
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
Use this API to manage [protected tags](../user/project/protected_tags.md) for your repositories.
|
||||
|
||||
## Valid access levels
|
||||
|
||||
These access levels are recognized:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ title: Repositories API
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
Use this API to manage your [GitLab repository](../user/project/repository/_index.md).
|
||||
|
||||
## List repository tree
|
||||
|
||||
Get a list of repository files and directories in a project. This endpoint can
|
||||
|
|
@ -25,7 +27,7 @@ in the Git internals documentation.
|
|||
|
||||
{{< alert type="warning" >}}
|
||||
|
||||
In version 17.7, the error handling behavior when a requested path is not found is updated.
|
||||
GitLab version 17.7 changes the error handling behavior when a requested path is not found.
|
||||
The endpoint now returns a status code `404 Not Found`. Previously, the status code was `200 OK`.
|
||||
|
||||
If your implementation relies on receiving a `200` status code with an empty array for
|
||||
|
|
@ -122,7 +124,7 @@ Supported attributes:
|
|||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
|
||||
| `sha` | string | yes | The blob SHA. |
|
||||
|
||||
## Raw blob content
|
||||
## Get raw blob content
|
||||
|
||||
Get the raw file contents for a blob, by blob SHA. This endpoint can be accessed
|
||||
without authentication if the repository is publicly accessible.
|
||||
|
|
@ -240,7 +242,7 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Contributors
|
||||
## Get contributor list
|
||||
|
||||
{{< history >}}
|
||||
|
||||
|
|
@ -291,7 +293,7 @@ Example response:
|
|||
}]
|
||||
```
|
||||
|
||||
## Merge Base
|
||||
## Get merge base
|
||||
|
||||
Get the common ancestor for 2 or more refs, such as commit SHAs, branch names, or tags.
|
||||
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ GET /users?with_custom_attributes=true
|
|||
|
||||
You can use the `created_by` parameter to see if a user account was created:
|
||||
|
||||
- [Manually by an administrator](../user/profile/account/create_accounts.md#create-users-in-admin-area).
|
||||
- [Manually by an administrator](../user/profile/account/create_accounts.md#create-a-user-in-the-admin-area).
|
||||
- As a [project bot user](../user/project/settings/project_access_tokens.md#bot-users-for-projects).
|
||||
|
||||
If the returned value is `null`, the account was created by a user who registered an account themselves.
|
||||
|
|
@ -568,7 +568,7 @@ see the `scim_identities` parameter:
|
|||
|
||||
Administrators can use the `created_by` parameter to see if a user account was created:
|
||||
|
||||
- [Manually by an administrator](../user/profile/account/create_accounts.md#create-users-in-admin-area).
|
||||
- [Manually by an administrator](../user/profile/account/create_accounts.md#create-a-user-in-the-admin-area).
|
||||
- As a [project bot user](../user/project/settings/project_access_tokens.md#bot-users-for-projects).
|
||||
|
||||
If the returned value is `null`, the account was created by a user who registered an account themselves.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ You can reset user passwords by using the UI, a Rake task, a Rails console, or t
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- You must be an administrator of GitLab Self-Managed.
|
||||
- The new password must meet all [password requirements](../user/profile/user_passwords.md#password-requirements).
|
||||
- You must be an administrator for the instance.
|
||||
- The password must meet all [password requirements](../user/profile/user_passwords.md#password-requirements).
|
||||
|
||||
## Use the UI
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ To reset a user password in the UI:
|
|||
1. In the **Password** section, enter and confirm a new password.
|
||||
1. Select **Save changes**.
|
||||
|
||||
A confirmation is displayed.
|
||||
GitLab updates the user password.
|
||||
|
||||
## Use a Rake task
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ Prerequisites:
|
|||
|
||||
- You must know the associated username, user ID, or email address.
|
||||
|
||||
1. Open a [Rails console](../administration/operations/rails_console.md).
|
||||
1. Start a [Rails console session](../administration/operations/rails_console.md#starting-a-rails-console-session).
|
||||
1. Find the user:
|
||||
|
||||
- By username:
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ You can also use the [users API endpoint](../../../api/users.md#create-a-user) t
|
|||
|
||||
Choose the right method based on your organization's size, security requirements, and workflows.
|
||||
|
||||
## Create users on sign-in page
|
||||
## Create a user on the sign-in page
|
||||
|
||||
By default, any user visiting your GitLab instance can register for an account.
|
||||
If you have previously [disabled this setting](../../../administration/settings/sign_up_restrictions.md#disable-new-sign-ups), you must turn it back on.
|
||||
|
|
@ -38,46 +38,37 @@ Users can create their own accounts by either:
|
|||
- Selecting the **Register now** link on the sign-in page.
|
||||
- Navigating to your GitLab instance's sign-up link (for example: `https://gitlab.example.com/users/sign_up`).
|
||||
|
||||
## Create users in Admin area
|
||||
## Create a user in the Admin area
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
- You must be an administrator for the instance.
|
||||
|
||||
To create a user manually:
|
||||
To create a user:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Overview > Users**.
|
||||
1. Select **New user**.
|
||||
1. Complete the required fields, such as name, username, and email.
|
||||
1. In the **Account** section, enter the required account information.
|
||||
1. Optional. In the **Access** section, configure any project limits or user type settings.
|
||||
1. Select **Create user**.
|
||||
|
||||
A reset link is sent to the user's email, and they are required to set their password when they first sign in.
|
||||
GitLab sends an email to the user with a sign-in link, and the user must create a password when
|
||||
they first sign in. You can also directly [set a password](../../../security/reset_user_password.md#use-the-ui)
|
||||
for the user.
|
||||
|
||||
### Set user password
|
||||
|
||||
To set a user's password without relying on the email confirmation, after you create a user:
|
||||
|
||||
1. Select the user.
|
||||
1. Select **Edit**.
|
||||
1. Complete the password and password confirmation fields.
|
||||
1. Select **Save changes**.
|
||||
|
||||
The user can now sign in with the new username and password,
|
||||
and they are required to change the password you set up for them.
|
||||
|
||||
## Create users through authentication integrations
|
||||
## Create a user with an authentication integration
|
||||
|
||||
GitLab can automatically create user accounts through authentication integrations.
|
||||
Users are created when they:
|
||||
|
||||
- Are provisioned through [SCIM](../../group/saml_sso/scim_setup.md) in the identity provider.
|
||||
- Sign in for the first time with:
|
||||
- [LDAP](../../../administration/auth/ldap/_index.md)
|
||||
- [Group SAML](../../group/saml_sso/_index.md)
|
||||
- An [OmniAuth provider](../../../integration/omniauth.md) that has the setting `allow_single_sign_on` turned on
|
||||
- Are provisioned through [SCIM](../../group/saml_sso/scim_setup.md) in the identity provider.
|
||||
|
||||
## Create users through the Rails console
|
||||
## Create a user through the Rails console
|
||||
|
||||
{{< alert type="warning" >}}
|
||||
|
||||
|
|
@ -88,7 +79,7 @@ Always run commands in a test environment first and have a backup instance ready
|
|||
|
||||
To create a user through the Rails console:
|
||||
|
||||
1. [Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session).
|
||||
1. Start a [Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session).
|
||||
1. Run the command according to your GitLab version:
|
||||
|
||||
{{< tabs >}}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ Prerequisites:
|
|||
|
||||
To delete the root account:
|
||||
|
||||
1. In the **Admin** area, [create a new user with administrator access](create_accounts.md#create-users-in-admin-area). This ensures that you maintain administrator access to the instance whilst mitigating the risks associated with deleting the root account.
|
||||
1. In the **Admin** area, [create a new user with administrator access](create_accounts.md#create-a-user-in-the-admin-area). This ensures that you maintain administrator access to the instance whilst mitigating the risks associated with deleting the root account.
|
||||
1. [Delete the root account](#delete-users-and-user-contributions).
|
||||
|
||||
### Use the GitLab Rails console
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ module Banzai
|
|||
const_get("#{name.to_s.camelize}Filter", false)
|
||||
end
|
||||
|
||||
def self.filter_item_limit_exceeded?(count, limit: FILTER_ITEM_LIMIT)
|
||||
count >= limit
|
||||
def self.filter_item_limit_exceeded?(count)
|
||||
count >= FILTER_ITEM_LIMIT
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ module Banzai
|
|||
|
||||
# Allow data-escaped-chars span attribute
|
||||
allowlist[:attributes]['span'].push('data-escaped-chars')
|
||||
allowlist[:attributes]['span'].push('data-placeholder')
|
||||
|
||||
# Allow html5 details/summary elements
|
||||
allowlist[:elements].push('details')
|
||||
|
|
@ -52,11 +51,9 @@ module Banzai
|
|||
allowlist[:attributes]['a'].push('name')
|
||||
|
||||
allowlist[:attributes]['a'].push('data-wikilink')
|
||||
allowlist[:attributes]['a'].push('data-placeholder')
|
||||
|
||||
allowlist[:attributes]['img'].push('data-diagram')
|
||||
allowlist[:attributes]['img'].push('data-diagram-src')
|
||||
allowlist[:attributes]['img'].push('data-placeholder')
|
||||
|
||||
# Allow any protocol in `a` elements
|
||||
# and then remove links with unsafe protocols in SanitizeLinkFilter
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ module Banzai
|
|||
math_code: true,
|
||||
math_dollars: true,
|
||||
multiline_block_quotes: true,
|
||||
placeholder_detection: true,
|
||||
relaxed_autolinks: true,
|
||||
sourcepos: true,
|
||||
smart: false,
|
||||
|
|
@ -51,18 +50,13 @@ module Banzai
|
|||
|
||||
def render_options
|
||||
return MINIMUM_MARKDOWN if minimum_markdown_enabled?
|
||||
|
||||
unless sourcepos_disabled? || headers_disabled? || autolink_disabled? || raw_html_disabled? ||
|
||||
placeholders_disabled?
|
||||
return OPTIONS
|
||||
end
|
||||
return OPTIONS unless sourcepos_disabled? || headers_disabled? || autolink_disabled? || raw_html_disabled?
|
||||
|
||||
OPTIONS.merge(
|
||||
sourcepos: !sourcepos_disabled?,
|
||||
header_ids: headers_disabled? ? nil : OPTIONS[:header_ids],
|
||||
autolink: !autolink_disabled?,
|
||||
relaxed_autolinks: !autolink_disabled?,
|
||||
placeholder_detection: !placeholders_disabled?,
|
||||
unsafe: !raw_html_disabled?
|
||||
)
|
||||
end
|
||||
|
|
@ -82,13 +76,6 @@ module Banzai
|
|||
def minimum_markdown_enabled?
|
||||
context[:minimum_markdown]
|
||||
end
|
||||
|
||||
def placeholders_disabled?
|
||||
return true unless context[:project]&.markdown_placeholders_feature_flag_enabled? ||
|
||||
context[:group]&.markdown_placeholders_feature_flag_enabled?
|
||||
|
||||
context[:disable_placeholders] || context[:broadcast_message_placeholders]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module Filter
|
||||
# Replaces previously identified dynamic placeholders with current values.
|
||||
# By performing this as a post-processing filter, we can show current
|
||||
# information.
|
||||
class PlaceholdersPostFilter < HTML::Pipeline::Filter
|
||||
prepend Concerns::TimeoutFilterHandler
|
||||
prepend Concerns::PipelineTimingCheck
|
||||
|
||||
# gitlab-glfm-markdown will detect possible placeholder values, and mark them with a
|
||||
# `<span data-placeholder>` for text, or add `data-placeholder` to link or images.
|
||||
# This allows us to specifically search for those nodes.
|
||||
#
|
||||
# The syntax used is `%{PLACEHOLDER}`. Markdown processing ignores
|
||||
# this syntax, so even links with embedded placeholders will get
|
||||
# preserved.
|
||||
CSS = '[data-placeholder]'
|
||||
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
|
||||
FILTER_ITEM_LIMIT = 100
|
||||
|
||||
# Variables that can be replaced. We handle them all dynamically as there
|
||||
# is a _possibility_ that their value could change in the future.
|
||||
#
|
||||
# While it might be rare for something like `gitlab_server`, we
|
||||
# error on the side of safety right now. If needed in the future,
|
||||
# we can add a `PlaceholderPreFilter` that would insert the values in the
|
||||
# main pipeline. Downside of not caching is that
|
||||
# the value is not cached in the DB and has to be filled in on each display
|
||||
PLACEHOLDERS = {
|
||||
'gitlab_server' => ->(_context) { Gitlab.config.gitlab.host },
|
||||
'gitlab_pages_domain' => ->(_context) { Gitlab.config.pages.host },
|
||||
'project_path' => ->(context) do
|
||||
context[:project]&.full_path if Ability.allowed?(context[:current_user], :read_project, context[:project])
|
||||
end,
|
||||
'project_name' => ->(context) do
|
||||
context[:project]&.path if Ability.allowed?(context[:current_user], :read_project, context[:project])
|
||||
end,
|
||||
'project_id' => ->(context) do
|
||||
context[:project]&.id.to_s if Ability.allowed?(context[:current_user], :read_project, context[:project])
|
||||
end,
|
||||
'project_namespace' => ->(context) do
|
||||
if Ability.allowed?(context[:current_user], :read_project, context[:project])
|
||||
context[:project]&.project_namespace&.to_param
|
||||
end
|
||||
end,
|
||||
'project_title' => ->(context) do
|
||||
context[:project]&.title if Ability.allowed?(context[:current_user], :read_project, context[:project])
|
||||
end,
|
||||
'group_name' => ->(context) do
|
||||
group = context[:project]&.group || context[:group]
|
||||
group.name if Ability.allowed?(context[:current_user], :read_group, group)
|
||||
end,
|
||||
'default_branch' => ->(context) do
|
||||
if context[:project]&.repository_exists? &&
|
||||
Ability.allowed?(context[:current_user], :read_code, context[:project])
|
||||
context[:project]&.default_branch
|
||||
end
|
||||
end,
|
||||
'commit_sha' => ->(context) do
|
||||
if context[:project]&.repository_exists? &&
|
||||
Ability.allowed?(context[:current_user], :read_code, context[:project])
|
||||
context[:project]&.commit&.sha
|
||||
end
|
||||
end,
|
||||
'latest_tag' => ->(context) do
|
||||
if context[:project]&.repository_exists? &&
|
||||
Ability.allowed?(context[:current_user], :read_code, context[:project])
|
||||
TagsFinder.new(context[:project].repository, per_page: 1, sort: 'updated_desc')
|
||||
&.execute&.first&.name
|
||||
end
|
||||
end
|
||||
}.freeze
|
||||
|
||||
PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/
|
||||
PLACEHOLDERS_FULL_REGEX = ::Gitlab::StringPlaceholderReplacer.placeholder_full_regex(PLACEHOLDERS_REGEX)
|
||||
|
||||
def call
|
||||
return doc unless context[:project]&.markdown_placeholders_feature_flag_enabled? ||
|
||||
context[:group]&.markdown_placeholders_feature_flag_enabled?
|
||||
|
||||
return doc if context[:disable_placeholders] || context[:broadcast_message_placeholders]
|
||||
|
||||
doc.xpath(XPATH).each_with_index do |node, index|
|
||||
break if Banzai::Filter.filter_item_limit_exceeded?(index, limit: FILTER_ITEM_LIMIT)
|
||||
|
||||
case node.name
|
||||
when 'span'
|
||||
replace_text_placeholders(node)
|
||||
when 'a'
|
||||
replace_link_placeholders(node)
|
||||
when 'img'
|
||||
replace_image_placeholders(node)
|
||||
else
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
doc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def replace_text_placeholders(node)
|
||||
content = node.to_html
|
||||
|
||||
html_content =
|
||||
Gitlab::StringPlaceholderReplacer.replace_string_placeholders(content, PLACEHOLDERS_REGEX) do |arg|
|
||||
# project_title is user supplied, but has a validation limiting the characters allowed.
|
||||
# Sanitization is not necessary
|
||||
replace_placeholder_action(PLACEHOLDERS[arg])
|
||||
end
|
||||
|
||||
node.replace(html_content) if content != html_content
|
||||
|
||||
node
|
||||
end
|
||||
|
||||
def replace_link_placeholders(node)
|
||||
href = link_href(node)
|
||||
|
||||
new_href =
|
||||
Gitlab::StringPlaceholderReplacer.replace_string_placeholders(href, PLACEHOLDERS_REGEX) do |arg|
|
||||
# project_title is a user supplied value, and doesn't really belong in a url
|
||||
if arg != 'project_title'
|
||||
replace_placeholder_action(PLACEHOLDERS[arg])
|
||||
else
|
||||
"%{#{arg}}"
|
||||
end
|
||||
end
|
||||
|
||||
node['href'] = new_href
|
||||
sanitize_link(node)
|
||||
end
|
||||
|
||||
def replace_image_placeholders(node)
|
||||
url = img_src(node)
|
||||
|
||||
new_url = Gitlab::StringPlaceholderReplacer.replace_string_placeholders(url, PLACEHOLDERS_REGEX) do |arg|
|
||||
# project_title is a user supplied value, and doesn't really belong in a url
|
||||
if arg != 'project_title'
|
||||
replace_placeholder_action(PLACEHOLDERS[arg])
|
||||
else
|
||||
"%{#{arg}}"
|
||||
end
|
||||
end
|
||||
|
||||
adjust_image_node(node, url, new_url)
|
||||
|
||||
node
|
||||
end
|
||||
|
||||
def adjust_image_node(node, url, new_url)
|
||||
if node['data-canonical-src']
|
||||
# most likely generated by the asset proxy
|
||||
node['src'] = new_url
|
||||
node.remove_attribute('data-canonical-src')
|
||||
node.remove_attribute('data-src')
|
||||
|
||||
asset = Banzai::Filter::AssetProxyFilter.new(node.to_html, context).call
|
||||
asset_node = asset&.children&.first
|
||||
|
||||
node['src'] = asset_node['src'] if asset_node && asset_node['data-canonical-src']
|
||||
else
|
||||
node['src'] = new_url
|
||||
end
|
||||
|
||||
node['data-src'] = new_url
|
||||
node['data-canonical-src'] = url
|
||||
|
||||
sanitize_link(node)
|
||||
|
||||
if node['class']&.include?('lazy')
|
||||
node['data-src'] = node['src'] if node['src'].present?
|
||||
node['src'] = LazyImageTagHelper.placeholder_image
|
||||
end
|
||||
|
||||
return unless node.parent&.name == 'a'
|
||||
|
||||
# since we wrap images with a link, we need to update it's href
|
||||
node.parent['href'] = node['data-src'] || node['src']
|
||||
end
|
||||
|
||||
def link_href(node)
|
||||
node.name == 'a' && node['href']
|
||||
end
|
||||
|
||||
def img_src(node)
|
||||
node.name == 'img' && (node['data-canonical-src'] || node['data-src'] || node['src'])
|
||||
end
|
||||
|
||||
# The action param represents the Proc to call in order to retrieve the value
|
||||
def replace_placeholder_action(action)
|
||||
action.call(context) || ''
|
||||
end
|
||||
|
||||
def sanitize_link(node)
|
||||
Banzai::Filter::SanitizeLinkFilter.new(node).call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,6 @@ module Banzai
|
|||
def self.filters
|
||||
@filters ||= FilterArray[
|
||||
Filter::TruncateVisibleFilter,
|
||||
Filter::PlaceholdersPostFilter,
|
||||
*internal_link_filters,
|
||||
Filter::AbsoluteLinkFilter,
|
||||
Filter::BroadcastMessagePlaceholdersFilter
|
||||
|
|
@ -29,8 +28,6 @@ module Banzai
|
|||
context.merge(
|
||||
post_process: true
|
||||
)
|
||||
|
||||
Filter::AssetProxyFilter.transform_context(context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ module Gitlab
|
|||
provider: provider, scope: scope)
|
||||
end
|
||||
|
||||
conditions.inject(:&).present?
|
||||
conditions.present? && conditions.all?
|
||||
end
|
||||
|
||||
def build_flow(provider:, session:, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE)
|
||||
|
|
|
|||
|
|
@ -46301,6 +46301,9 @@ msgstr ""
|
|||
msgid "Please select a Jira project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select a group for your trial."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select a valid target branch"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -52045,6 +52048,9 @@ msgstr ""
|
|||
msgid "Restricted shift times are not available for hourly shifts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resubmit request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Results limit reached"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -64905,6 +64911,9 @@ msgstr ""
|
|||
msgid "Trending"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial registration unsuccessful"
|
||||
msgstr ""
|
||||
|
||||
msgid "TrialWidget|%{daysLeft} days left in trial"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -68793,6 +68802,9 @@ msgstr ""
|
|||
msgid "We're sorry, your trial could not be created because our system did not respond successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "We're sorry, your trial could not be created. Please resubmit below to complete your registration."
|
||||
msgstr ""
|
||||
|
||||
msgid "We've detected some unusual activity"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -72220,6 +72232,9 @@ msgstr ""
|
|||
msgid "Your top-level group is over the user limit and has been placed in a read-only state."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your trial will be applied to this group."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your update failed. You can only upload one design when dropping onto an existing design."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -12,212 +12,431 @@ RSpec.describe 'User Settings > Personal access tokens', :with_current_organizat
|
|||
execute: ServiceResponse.error(message: 'error', payload: { personal_access_token: PersonalAccessToken.new }))
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(migrate_user_access_tokens_ui: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe "token creation" do
|
||||
it "allows creation of a personal access token" do
|
||||
name = 'My PAT'
|
||||
description = 'My PAT description'
|
||||
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens_counter).to have_text('0')
|
||||
|
||||
click_button 'Add new token'
|
||||
fill_in "Token name", with: name
|
||||
|
||||
fill_in "Token description", with: description
|
||||
|
||||
# Set date to 1st of next month
|
||||
find_field("Expiration date").click
|
||||
find(".pika-next").click
|
||||
click_on "1"
|
||||
|
||||
# Scopes
|
||||
check "read_api"
|
||||
check "read_user"
|
||||
|
||||
click_on "Create personal access token"
|
||||
wait_for_all_requests
|
||||
|
||||
expect(active_access_tokens).to have_text(name)
|
||||
expect(active_access_tokens).to have_text(description)
|
||||
expect(active_access_tokens).to have_text('in')
|
||||
expect(active_access_tokens).to have_text('read_api')
|
||||
expect(active_access_tokens).to have_text('read_user')
|
||||
expect(created_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
describe "migrate_user_access_tokens_ui feature flag on" do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context "when creation fails" do
|
||||
it "displays an error message" do
|
||||
number_tokens_before = PersonalAccessToken.count
|
||||
describe "token creation" do
|
||||
it "allows creation of a personal access token" do
|
||||
name = 'My PAT'
|
||||
description = 'My PAT description'
|
||||
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens_count).to have_text('0')
|
||||
|
||||
click_button 'Add new token'
|
||||
fill_in "Token name", with: 'My PAT'
|
||||
fill_in "Token name", with: name
|
||||
|
||||
fill_in "Description", with: description
|
||||
|
||||
# Set date to 1st of next month
|
||||
find_field("Expiration date").click
|
||||
find(".pika-next").click
|
||||
click_on "1"
|
||||
|
||||
# Scopes
|
||||
check "read_api"
|
||||
check "read_user"
|
||||
|
||||
click_on "Create token"
|
||||
wait_for_all_requests
|
||||
|
||||
expect(access_token_table).to have_text(name)
|
||||
expect(access_token_table).to have_text(description)
|
||||
expect(access_token_table).to have_text('in')
|
||||
expect(access_token_table).to have_text('read_api')
|
||||
expect(access_token_table).to have_text('read_user')
|
||||
expect(new_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_count).to have_text('1')
|
||||
end
|
||||
|
||||
context "when creation fails" do
|
||||
it "displays an error message" do
|
||||
number_tokens_before = PersonalAccessToken.count
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
click_button 'Add new token'
|
||||
fill_in "Token name", with: 'My PAT'
|
||||
|
||||
click_on "Create token"
|
||||
wait_for_all_requests
|
||||
|
||||
expect(number_tokens_before).to equal(PersonalAccessToken.count)
|
||||
expect(page).to have_content(_("At least one scope is required."))
|
||||
expect(page).to have_content("No access tokens")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'active tokens' do
|
||||
let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it 'only shows personal access tokens' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(access_token_table).to have_text(personal_access_token.name)
|
||||
expect(access_token_table).not_to have_text(impersonation_token.name)
|
||||
end
|
||||
|
||||
context 'when User#time_display_relative is false' do
|
||||
before do
|
||||
user.update!(time_display_relative: false)
|
||||
end
|
||||
|
||||
it 'shows absolute times for expires_at' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(access_token_table).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/547507
|
||||
# context 'when token has no Last Used IPs' do
|
||||
# it 'shows "Never" as the value' do
|
||||
# visit user_settings_personal_access_tokens_path
|
||||
|
||||
# expect(last_used_ip).to have_text('Never')
|
||||
# end
|
||||
# end
|
||||
|
||||
# context 'when token has Last Used IPs' do
|
||||
# let(:current_ip_address) { '127.0.0.1' }
|
||||
|
||||
# before do
|
||||
# personal_access_token.last_used_ips << Authn::PersonalAccessTokenLastUsedIp.new(
|
||||
# organization: personal_access_token.organization,
|
||||
# ip_address: current_ip_address)
|
||||
# end
|
||||
|
||||
# it 'shows the current_ip_address in last_used_ips' do
|
||||
# visit user_settings_personal_access_tokens_path
|
||||
|
||||
# expect(last_used_ip).to have_text(current_ip_address)
|
||||
# end
|
||||
# end
|
||||
end
|
||||
|
||||
describe "inactive tokens" do
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it "allows revocation of an active token" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
access_token_options.click
|
||||
accept_gl_confirm(button_text: 'Revoke') { click_on "Revoke" }
|
||||
|
||||
expect(access_token_table).to have_text("No access tokens")
|
||||
end
|
||||
|
||||
it "removes expired tokens from 'active' section" do
|
||||
personal_access_token.update!(expires_at: 5.days.ago)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(access_token_table).to have_text("No access tokens")
|
||||
end
|
||||
|
||||
context "when revocation fails" do
|
||||
it "displays an error message" do
|
||||
allow_next_instance_of(PersonalAccessTokens::RevokeService) do |instance|
|
||||
allow(instance).to receive(:revocation_permitted?).and_return(false)
|
||||
end
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
access_token_options.click
|
||||
accept_gl_confirm(button_text: "Revoke") { click_on "Revoke" }
|
||||
expect(access_token_table).to have_text(personal_access_token.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "rotating tokens" do
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it "displays the newly created token" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
expect(active_access_tokens_count).to have_text('1')
|
||||
access_token_options.click
|
||||
accept_gl_confirm(button_text: s_('AccessTokens|Rotate')) { click_on s_('AccessTokens|Rotate') }
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content("Make sure you save it - you won't be able to access it again.")
|
||||
expect(access_token_table).to have_text(personal_access_token.name)
|
||||
expect(new_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_count).to have_text('1')
|
||||
end
|
||||
|
||||
context "when rotation fails" do
|
||||
it "displays an error message" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
access_token_options.click
|
||||
accept_gl_confirm(button_text: s_('AccessTokens|Rotate')) do
|
||||
personal_access_token.revoke!
|
||||
click_on s_('AccessTokens|Rotate')
|
||||
end
|
||||
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content(s_('AccessTokens|Token already revoked'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "prefills token details" do
|
||||
name = 'My PAT'
|
||||
description = 'My PAT description'
|
||||
scopes = 'api,read_user'
|
||||
|
||||
visit user_settings_personal_access_tokens_path({ name: name, scopes: scopes, description: description })
|
||||
|
||||
expect(page).to have_field("Token name", with: name)
|
||||
expect(page).to have_field("Description", with: description)
|
||||
expect(find_field('api', with: 'api')).to be_checked
|
||||
expect(find_field('read_user')).to be_checked
|
||||
end
|
||||
|
||||
describe "feed token" do
|
||||
def feed_token_description
|
||||
"Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar\
|
||||
application loads a personalized calendar. It is visible in those feed URLs. It cannot be used to access\
|
||||
any other data."
|
||||
end
|
||||
|
||||
context "when enabled" do
|
||||
it "displays feed token" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
within_testid('feed-token-container') do
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(page).to have_field('Feed token', with: user.feed_token)
|
||||
expect(page).to have_content(feed_token_description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when disabled" do
|
||||
it "does not display feed token" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(page).not_to have_content(feed_token_description)
|
||||
expect(page).not_to have_field('Feed token')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "migrate_user_access_tokens_ui feature flag off" do
|
||||
before do
|
||||
stub_feature_flags(migrate_user_access_tokens_ui: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe "token creation" do
|
||||
it "allows creation of a personal access token" do
|
||||
name = 'My PAT'
|
||||
description = 'My PAT description'
|
||||
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens_counter).to have_text('0')
|
||||
|
||||
click_button 'Add new token'
|
||||
fill_in "Token name", with: name
|
||||
|
||||
fill_in "Token description", with: description
|
||||
|
||||
# Set date to 1st of next month
|
||||
find_field("Expiration date").click
|
||||
find(".pika-next").click
|
||||
click_on "1"
|
||||
|
||||
# Scopes
|
||||
check "read_api"
|
||||
check "read_user"
|
||||
|
||||
click_on "Create personal access token"
|
||||
wait_for_all_requests
|
||||
|
||||
expect(number_tokens_before).to equal(PersonalAccessToken.count)
|
||||
expect(page).to have_content(_("Scopes can't be blank"))
|
||||
expect(page).not_to have_selector("[data-testid='new-access-tokens']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'active tokens' do
|
||||
let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it 'only shows personal access tokens' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_text(personal_access_token.name)
|
||||
expect(active_access_tokens).not_to have_text(impersonation_token.name)
|
||||
end
|
||||
|
||||
context 'when User#time_display_relative is false' do
|
||||
before do
|
||||
user.update!(time_display_relative: false)
|
||||
expect(active_access_tokens).to have_text(name)
|
||||
expect(active_access_tokens).to have_text(description)
|
||||
expect(active_access_tokens).to have_text('in')
|
||||
expect(active_access_tokens).to have_text('read_api')
|
||||
expect(active_access_tokens).to have_text('read_user')
|
||||
expect(created_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
end
|
||||
|
||||
it 'shows absolute times for expires_at' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
context "when creation fails" do
|
||||
it "displays an error message" do
|
||||
number_tokens_before = PersonalAccessToken.count
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
|
||||
end
|
||||
end
|
||||
click_button 'Add new token'
|
||||
fill_in "Token name", with: 'My PAT'
|
||||
|
||||
context 'when token has no Last Used IPs' do
|
||||
it 'shows "-" as the value' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
click_on "Create personal access token"
|
||||
wait_for_all_requests
|
||||
|
||||
expect(active_access_tokens).to have_selector('td[data-label="Last Used IPs"]', text: '-')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token has Last Used IPs' do
|
||||
let(:current_ip_address) { '127.0.0.1' }
|
||||
|
||||
before do
|
||||
personal_access_token.last_used_ips << Authn::PersonalAccessTokenLastUsedIp.new(
|
||||
organization: personal_access_token.organization,
|
||||
ip_address: current_ip_address)
|
||||
end
|
||||
|
||||
it 'shows the current_ip_address in last_used_ips' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_selector('td[data-label="Last Used IPs"]', text: current_ip_address)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "inactive tokens" do
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it "allows revocation of an active token" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
accept_gl_confirm(button_text: 'Revoke') { click_on "Revoke" }
|
||||
|
||||
expect(active_access_tokens).to have_text("This user has no active personal access tokens.")
|
||||
end
|
||||
|
||||
it "removes expired tokens from 'active' section" do
|
||||
personal_access_token.update!(expires_at: 5.days.ago)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_text("This user has no active personal access tokens.")
|
||||
end
|
||||
|
||||
context "when revocation fails" do
|
||||
it "displays an error message" do
|
||||
allow_next_instance_of(PersonalAccessTokens::RevokeService) do |instance|
|
||||
allow(instance).to receive(:revocation_permitted?).and_return(false)
|
||||
expect(number_tokens_before).to equal(PersonalAccessToken.count)
|
||||
expect(page).to have_content(_("Scopes can't be blank"))
|
||||
expect(page).not_to have_selector("[data-testid='new-access-tokens']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'active tokens' do
|
||||
let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it 'only shows personal access tokens' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
accept_gl_confirm(button_text: "Revoke") { click_on "Revoke" }
|
||||
expect(active_access_tokens).to have_text(personal_access_token.name)
|
||||
expect(active_access_tokens).not_to have_text(impersonation_token.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "rotating tokens" do
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it "displays the newly created token" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
accept_gl_confirm(button_text: s_('AccessTokens|Rotate')) { click_on s_('AccessTokens|Rotate') }
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content("Your new personal access token has been created.")
|
||||
expect(active_access_tokens).to have_text(personal_access_token.name)
|
||||
expect(created_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
end
|
||||
|
||||
context "when rotation fails" do
|
||||
it "displays an error message" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
accept_gl_confirm(button_text: s_('AccessTokens|Rotate')) do
|
||||
personal_access_token.revoke!
|
||||
click_on s_('AccessTokens|Rotate')
|
||||
context 'when User#time_display_relative is false' do
|
||||
before do
|
||||
user.update!(time_display_relative: false)
|
||||
end
|
||||
|
||||
it 'shows absolute times for expires_at' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token has no Last Used IPs' do
|
||||
it 'shows "-" as the value' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_selector('td[data-label="Last Used IPs"]', text: '-')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token has Last Used IPs' do
|
||||
let(:current_ip_address) { '127.0.0.1' }
|
||||
|
||||
before do
|
||||
personal_access_token.last_used_ips << Authn::PersonalAccessTokenLastUsedIp.new(
|
||||
organization: personal_access_token.organization,
|
||||
ip_address: current_ip_address)
|
||||
end
|
||||
|
||||
it 'shows the current_ip_address in last_used_ips' do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_selector('td[data-label="Last Used IPs"]', text: current_ip_address)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "inactive tokens" do
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it "allows revocation of an active token" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
accept_gl_confirm(button_text: 'Revoke') { click_on "Revoke" }
|
||||
|
||||
expect(active_access_tokens).to have_text("This user has no active personal access tokens.")
|
||||
end
|
||||
|
||||
it "removes expired tokens from 'active' section" do
|
||||
personal_access_token.update!(expires_at: 5.days.ago)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_text("This user has no active personal access tokens.")
|
||||
end
|
||||
|
||||
context "when revocation fails" do
|
||||
it "displays an error message" do
|
||||
allow_next_instance_of(PersonalAccessTokens::RevokeService) do |instance|
|
||||
allow(instance).to receive(:revocation_permitted?).and_return(false)
|
||||
end
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
accept_gl_confirm(button_text: "Revoke") { click_on "Revoke" }
|
||||
expect(active_access_tokens).to have_text(personal_access_token.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "rotating tokens" do
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it "displays the newly created token" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
accept_gl_confirm(button_text: s_('AccessTokens|Rotate')) { click_on s_('AccessTokens|Rotate') }
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content(s_('AccessTokens|Token already revoked'))
|
||||
expect(page).to have_content("Your new personal access token has been created.")
|
||||
expect(active_access_tokens).to have_text(personal_access_token.name)
|
||||
expect(created_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "feed token" do
|
||||
def feed_token_description
|
||||
"Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar\
|
||||
application loads a personalized calendar. It is visible in those feed URLs."
|
||||
end
|
||||
context "when rotation fails" do
|
||||
it "displays an error message" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
context "when enabled" do
|
||||
it "displays feed token" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
accept_gl_confirm(button_text: s_('AccessTokens|Rotate')) do
|
||||
personal_access_token.revoke!
|
||||
click_on s_('AccessTokens|Rotate')
|
||||
end
|
||||
|
||||
within_testid('feed-token-container') do
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(page).to have_field('Feed token', with: user.feed_token)
|
||||
expect(page).to have_content(feed_token_description)
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content(s_('AccessTokens|Token already revoked'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when disabled" do
|
||||
it "does not display feed token" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
it "prefills token details" do
|
||||
name = 'My PAT'
|
||||
description = 'My PAT description'
|
||||
scopes = 'api,read_user'
|
||||
|
||||
expect(page).not_to have_content(feed_token_description)
|
||||
expect(page).not_to have_field('Feed token')
|
||||
visit user_settings_personal_access_tokens_path({ name: name, scopes: scopes, description: description })
|
||||
|
||||
expect(page).to have_field("Token name", with: name)
|
||||
expect(page).to have_field("Token description", with: description)
|
||||
expect(find("#personal_access_token_scopes_api")).to be_checked
|
||||
expect(find("#personal_access_token_scopes_read_user")).to be_checked
|
||||
end
|
||||
|
||||
describe "feed token" do
|
||||
def feed_token_description
|
||||
"Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar\
|
||||
application loads a personalized calendar. It is visible in those feed URLs. It cannot be used to access\
|
||||
any other data."
|
||||
end
|
||||
|
||||
context "when enabled" do
|
||||
it "displays feed token" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
within_testid('feed-token-container') do
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(page).to have_field('Feed token', with: user.feed_token)
|
||||
expect(page).to have_content(feed_token_description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when disabled" do
|
||||
it "does not display feed token" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(page).not_to have_content(feed_token_description)
|
||||
expect(page).not_to have_field('Feed token')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "prefills token details" do
|
||||
name = 'My PAT'
|
||||
description = 'My PAT description'
|
||||
scopes = 'api,read_user'
|
||||
|
||||
visit user_settings_personal_access_tokens_path({ name: name, scopes: scopes, description: description })
|
||||
|
||||
expect(page).to have_field("Token name", with: name)
|
||||
expect(page).to have_field("Token description", with: description)
|
||||
expect(find("#personal_access_token_scopes_api")).to be_checked
|
||||
expect(find("#personal_access_token_scopes_read_user")).to be_checked
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Types::PermissionTypes::Namespaces::Base, feature_category: :groups_and_projects do
|
||||
specify do
|
||||
expected_permissions = [:admin_label, :read_namespace]
|
||||
expected_permissions = [:admin_label, :read_namespace, :admin_issue, :create_work_item,
|
||||
:import_issues, :read_crm_contact, :read_crm_organization]
|
||||
|
||||
expected_permissions.each do |permission|
|
||||
expect(described_class).to have_graphql_field(permission)
|
||||
|
|
|
|||
|
|
@ -56,46 +56,4 @@ RSpec.describe Banzai::Filter::MarkdownEngines::GlfmMarkdown, feature_category:
|
|||
|
||||
expect(engine.render('http://example.com _emphasis_ $x + y$')).to eq expected
|
||||
end
|
||||
|
||||
describe 'placeholder detection' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
it 'turns off placeholder detection when :markdown_placeholders disabled' do
|
||||
stub_feature_flags(markdown_placeholders: false)
|
||||
|
||||
engine = described_class.new({ project: project, no_sourcepos: true })
|
||||
expected = <<~TEXT
|
||||
<p>%{test}</p>
|
||||
TEXT
|
||||
|
||||
expect(engine.render('%{test}')).to eq expected
|
||||
end
|
||||
|
||||
it 'defaults to on' do
|
||||
engine = described_class.new({ project: project, no_sourcepos: true })
|
||||
expected = <<~TEXT
|
||||
<p><span data-placeholder>%{test}</span></p>
|
||||
TEXT
|
||||
|
||||
expect(engine.render('%{test}')).to eq expected
|
||||
end
|
||||
|
||||
it 'turns off placeholder detection when :disable_placeholders' do
|
||||
engine = described_class.new({ disable_placeholders: true, project: project, no_sourcepos: true })
|
||||
expected = <<~TEXT
|
||||
<p>%{test}</p>
|
||||
TEXT
|
||||
|
||||
expect(engine.render('%{test}')).to eq expected
|
||||
end
|
||||
|
||||
it 'turns off placeholder detection when :broadcast_message_placeholders' do
|
||||
engine = described_class.new({ broadcast_message_placeholders: true, project: project, no_sourcepos: true })
|
||||
expected = <<~TEXT
|
||||
<p>%{test}</p>
|
||||
TEXT
|
||||
|
||||
expect(engine.render('%{test}')).to eq expected
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,420 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Banzai::Filter::PlaceholdersPostFilter, feature_category: :markdown do
|
||||
include FilterSpecHelper
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
def run_pipeline(text, context = { project: project })
|
||||
stub_commonmark_sourcepos_disabled
|
||||
|
||||
Banzai.render_and_post_process(text, context)
|
||||
end
|
||||
|
||||
def run_filter(text, context = { project: project })
|
||||
stub_commonmark_sourcepos_disabled
|
||||
|
||||
text = Banzai::Filter::MarkdownFilter.new(text, context).call
|
||||
filter(text, context).to_html
|
||||
end
|
||||
|
||||
let_it_be(:gitlab_server) { "<span data-placeholder>#{Gitlab.config.gitlab.host}</span>" }
|
||||
let_it_be(:gitlab_pages_domain) { "<span data-placeholder>#{Gitlab.config.pages.host}</span>" }
|
||||
let!(:project_path) { "<span data-placeholder>#{project.full_path}</span>" }
|
||||
let!(:project_name) { "<span data-placeholder>#{project.path}</span>" }
|
||||
let!(:project_id) { "<span data-placeholder>#{project.id}</span>" }
|
||||
let!(:project_namespace) { "<span data-placeholder>#{project.project_namespace.to_param}</span>" }
|
||||
let!(:project_title) { "<span data-placeholder>#{project.title}</span>" }
|
||||
let!(:group_name) { "<span data-placeholder>#{project.group&.name}</span>" }
|
||||
let!(:default_branch) { "<span data-placeholder>#{project.default_branch}</span>" }
|
||||
let!(:commit_sha) { "<span data-placeholder>#{project.commit&.sha}</span>" }
|
||||
let!(:latest_tag) { "<span data-placeholder>#{project_tag}</span>" }
|
||||
let!(:empty_span) { "<span data-placeholder></span>" }
|
||||
let!(:project_tag) do
|
||||
if project.repository_exists?
|
||||
TagsFinder.new(project.repository, per_page: 1, sort: 'updated_desc')&.execute&.first&.name
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'placeholders with no access' do
|
||||
where(:markdown, :expected) do
|
||||
'%{gitlab_server}' | ref(:gitlab_server)
|
||||
'%{gitlab_pages_domain}' | ref(:gitlab_pages_domain)
|
||||
'%{project_path}' | ref(:empty_span)
|
||||
'%{project_name}' | ref(:empty_span)
|
||||
'%{project_title}' | ref(:empty_span)
|
||||
'%{project_id}' | ref(:empty_span)
|
||||
'%{project_namespace}' | ref(:empty_span)
|
||||
'%{group_name}' | ref(:group_name)
|
||||
'%{default_branch}' | ref(:empty_span)
|
||||
'%{commit_sha}' | ref(:empty_span)
|
||||
'%{latest_tag}' | ref(:empty_span)
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'replaces placeholder' do
|
||||
expect(run_pipeline(markdown, project: project, current_user: user)).to eq "<p dir=\"auto\">#{expected}</p>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'placeholders with no access, no group' do
|
||||
where(:markdown, :expected) do
|
||||
'%{gitlab_server}' | ref(:gitlab_server)
|
||||
'%{gitlab_pages_domain}' | ref(:gitlab_pages_domain)
|
||||
'%{project_path}' | ref(:empty_span)
|
||||
'%{project_name}' | ref(:empty_span)
|
||||
'%{project_title}' | ref(:empty_span)
|
||||
'%{project_id}' | ref(:empty_span)
|
||||
'%{project_namespace}' | ref(:empty_span)
|
||||
'%{group_name}' | ref(:empty_span)
|
||||
'%{default_branch}' | ref(:empty_span)
|
||||
'%{commit_sha}' | ref(:empty_span)
|
||||
'%{latest_tag}' | ref(:empty_span)
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'replaces placeholder' do
|
||||
expect(run_pipeline(markdown, project: project, current_user: user)).to eq "<p dir=\"auto\">#{expected}</p>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'placeholders with access' do
|
||||
where(:markdown, :expected) do
|
||||
'%{gitlab_server}' | ref(:gitlab_server)
|
||||
'%{gitlab_pages_domain}' | ref(:gitlab_pages_domain)
|
||||
'%{project_path}' | ref(:project_path)
|
||||
'%{project_name}' | ref(:project_name)
|
||||
'%{project_title}' | ref(:project_title)
|
||||
'%{project_id}' | ref(:project_id)
|
||||
'%{project_namespace}' | ref(:project_namespace)
|
||||
'%{group_name}' | ref(:group_name)
|
||||
'%{default_branch}' | ref(:default_branch)
|
||||
'%{commit_sha}' | ref(:commit_sha)
|
||||
'%{latest_tag}' | ref(:latest_tag)
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'replaces placeholder' do
|
||||
expect(run_pipeline(markdown, project: project, current_user: user)).to eq "<p dir=\"auto\">#{expected}</p>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'placeholders with access, no code access' do
|
||||
where(:markdown, :expected) do
|
||||
'%{gitlab_server}' | ref(:gitlab_server)
|
||||
'%{gitlab_pages_domain}' | ref(:gitlab_pages_domain)
|
||||
'%{project_path}' | ref(:project_path)
|
||||
'%{project_name}' | ref(:project_name)
|
||||
'%{project_title}' | ref(:project_title)
|
||||
'%{project_id}' | ref(:project_id)
|
||||
'%{project_namespace}' | ref(:project_namespace)
|
||||
'%{group_name}' | ref(:group_name)
|
||||
'%{default_branch}' | ref(:empty_span)
|
||||
'%{commit_sha}' | ref(:empty_span)
|
||||
'%{latest_tag}' | ref(:empty_span)
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'replaces placeholder' do
|
||||
expect(run_pipeline(markdown, project: project, current_user: user)).to eq "<p dir=\"auto\">#{expected}</p>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `markdown_placeholders` feature flag is disabled' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :small_repo, :public, group: group) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(markdown_placeholders: false)
|
||||
project.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
it 'does not replace placeholders' do
|
||||
expect(run_pipeline('%{gitlab_server}', project: project, current_user: user))
|
||||
.to eq '<p dir="auto">%{gitlab_server}</p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disabled' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :small_repo, :public, group: group) }
|
||||
|
||||
before do
|
||||
project.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
it 'does not replace placeholders when :disable_placeholders' do
|
||||
result = run_pipeline('%{gitlab_server}', project: project, current_user: user, disable_placeholders: true)
|
||||
|
||||
expect(result).to eq '<p dir="auto">%{gitlab_server}</p>'
|
||||
end
|
||||
|
||||
it 'does not replace placeholders when :broadcast_message_placeholders' do
|
||||
result = run_pipeline('%{gitlab_server}', project: project, current_user: user,
|
||||
broadcast_message_placeholders: true)
|
||||
|
||||
expect(result).to eq '<p dir="auto">%{gitlab_server}</p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when private project' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project, reload: true) { create(:project, :small_repo, group: group, create_tag: 'test') }
|
||||
|
||||
context 'with no access' do
|
||||
it_behaves_like 'placeholders with no access'
|
||||
end
|
||||
|
||||
context 'with guest access' do
|
||||
before do
|
||||
project.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
it_behaves_like 'placeholders with access, no code access'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when public project' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project, reload: true) { create(:project, :small_repo, :public, group: group, create_tag: 'test') }
|
||||
|
||||
context 'with no access' do
|
||||
it_behaves_like 'placeholders with access'
|
||||
end
|
||||
|
||||
context 'with guest access' do
|
||||
before do
|
||||
project.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
it_behaves_like 'placeholders with access'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when private group, private project' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group, reload: true) { create(:group, :private) }
|
||||
let_it_be(:project, reload: true) { create(:project, :small_repo, group: group, create_tag: 'test') }
|
||||
|
||||
context 'with no access' do
|
||||
it_behaves_like 'placeholders with no access, no group'
|
||||
end
|
||||
|
||||
context 'with guest access' do
|
||||
before do
|
||||
group.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
it_behaves_like 'placeholders with access, no code access'
|
||||
end
|
||||
|
||||
context 'with reporter access' do
|
||||
before do
|
||||
group.add_member(user, Gitlab::Access::REPORTER)
|
||||
end
|
||||
|
||||
it_behaves_like 'placeholders with access'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has a disabled repository' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project, reload: true) { create(:project, :public, :repository_disabled, group: group) }
|
||||
|
||||
it_behaves_like 'placeholders with access, no code access'
|
||||
end
|
||||
|
||||
context 'when project has no repository' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project, reload: true) { create(:project, :public, group: group) }
|
||||
|
||||
it_behaves_like 'placeholders with access, no code access'
|
||||
end
|
||||
|
||||
context 'when placeholders in text' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :small_repo, group: group) }
|
||||
|
||||
# if the validation is ever changed to allow characters such as `<`, then
|
||||
# we will need to sanitize the project_title
|
||||
it 'verifes project_title is limited in characters' do
|
||||
project.name = '<script>'
|
||||
|
||||
expect { project.save! }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
|
||||
project.name = 'script:'
|
||||
|
||||
expect { project.save! }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when placeholders in link' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :small_repo, group: group) }
|
||||
|
||||
before do
|
||||
project.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
it 'does not allow project_title in a link href' do
|
||||
markdown = '[test](%{gitlab_server}/%{project_title}/foo.png)'
|
||||
expected = '<p dir="auto"><a href="localhost/%{project_title}/foo.png" ' \
|
||||
'data-placeholder rel="nofollow noreferrer noopener" target="_blank">test</a></p>'
|
||||
|
||||
expect(run_pipeline(markdown)).to eq expected
|
||||
end
|
||||
|
||||
it 'does not recognize unknown placeholder in a link href' do
|
||||
markdown = '[test](%{gitlab_server}/%{foo}/foo.png)'
|
||||
expected = '<p dir="auto"><a href="localhost/%%7Bfoo%7D/foo.png" ' \
|
||||
'data-placeholder rel="nofollow noreferrer noopener" target="_blank">test</a></p>'
|
||||
|
||||
expect(run_pipeline(markdown)).to eq expected
|
||||
end
|
||||
|
||||
it 'does not allow placeholders in link text (parser limitation)' do
|
||||
markdown = '[%{gitlab_server}](%{gitlab_server})'
|
||||
expected = '<p dir="auto"><a href="localhost" data-placeholder rel="nofollow noreferrer noopener" ' \
|
||||
'target="_blank">%{gitlab_server}</a></p>'
|
||||
|
||||
expect(run_pipeline(markdown)).to eq expected
|
||||
end
|
||||
|
||||
context 'when replacing href' do
|
||||
let_it_be(:project) { create(:project, :small_repo, :public, group: group, path: 'script') }
|
||||
|
||||
it 'sanitizes the link' do
|
||||
expect(Banzai::Filter::SanitizeLinkFilter).to receive(:new).twice.and_call_original
|
||||
|
||||
markdown = '[foo](java%{project_name})'
|
||||
|
||||
expect(run_pipeline(markdown)).to include "<a href=\"/#{project.full_path}/-/blob/master/javascript\""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when placeholders in image' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :small_repo, :public, path: 'project-img', group: group) }
|
||||
|
||||
before do
|
||||
project.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
context 'when placeholder in the `src` attribute' do
|
||||
it 'generates the correct attributes' do
|
||||
markdown = ''
|
||||
expected = "<p><img src=\"https://#{Gitlab.config.gitlab.host}/#{project.path}/foo.png\" " \
|
||||
'data-placeholder alt="" ' \
|
||||
"data-src=\"https://#{Gitlab.config.gitlab.host}/#{project.path}/foo.png\" " \
|
||||
'data-canonical-src="https://%%7Bgitlab_server%7D/%%7Bproject_name%7D/foo.png"></p>'
|
||||
|
||||
# can't use the pipeline because the image_link_filter modifies `src`
|
||||
expect(run_filter(markdown)).to eq expected
|
||||
end
|
||||
|
||||
it 'does not allow project_title in a src' do
|
||||
markdown = ''
|
||||
expected = "<p><img src=\"https://#{Gitlab.config.gitlab.host}/%{project_title}/foo.png\" " \
|
||||
'data-placeholder alt="" ' \
|
||||
"data-src=\"https://#{Gitlab.config.gitlab.host}/%{project_title}/foo.png\" " \
|
||||
'data-canonical-src="https://%%7Bgitlab_server%7D/%%7Bproject_title%7D/foo.png"></p>'
|
||||
|
||||
expect(run_filter(markdown)).to eq expected
|
||||
end
|
||||
end
|
||||
|
||||
context 'when asset proxy is disabled' do
|
||||
before do
|
||||
stub_asset_proxy_setting(enabled: false)
|
||||
end
|
||||
|
||||
it 'generates the correct attributes' do
|
||||
markdown = ''
|
||||
|
||||
result = run_pipeline(markdown)
|
||||
|
||||
expect(result).to include "href=\"https://#{Gitlab.config.gitlab.host}/#{project.path}/foo.png\""
|
||||
expect(result).to include "data-src=\"https://#{Gitlab.config.gitlab.host}/#{project.path}/foo.png\""
|
||||
expect(result).to include 'data-canonical-src="https://%%7Bgitlab_server%7D/%%7Bproject_name%7D/foo.png"'
|
||||
expect(result)
|
||||
.to include '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when asset proxy is enabled' do
|
||||
before do
|
||||
stub_asset_proxy_setting(enabled: true)
|
||||
stub_asset_proxy_setting(secret_key: 'shared-secret')
|
||||
stub_asset_proxy_setting(url: 'https://assets.example.com')
|
||||
stub_asset_proxy_setting(allowlist: %w[gitlab.com *.mydomain.com])
|
||||
end
|
||||
|
||||
it 'generates the correct attributes' do
|
||||
markdown = ''
|
||||
|
||||
result = run_pipeline(markdown)
|
||||
|
||||
expect(result).to include 'href="https://assets.example.com/8641679ca136e7fe8cf9415852374bbe4595f929/68747470733a2f2f6578616d706c652e636f6d2f70726f6a6563742d696d672f666f6f2e706e67"'
|
||||
expect(result).to include 'f6f2e706e67" data-canonical-src="https://example.com/%%7Bproject_name%7D/foo.png"'
|
||||
expect(result).to include 'data-src="https://assets.example.com/8641679ca136e7fe8cf9415852374bbe4595f929/68747470733a2f2f6578616d706c652e636f6d2f70726f6a6563742d696d672f666f6f2e706e67"'
|
||||
expect(result)
|
||||
.to include '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when placeholders in an unsupported node' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :small_repo, :public, path: 'project-img', group: group) }
|
||||
|
||||
before do
|
||||
project.add_member(user, Gitlab::Access::GUEST)
|
||||
end
|
||||
|
||||
it 'does not replace it' do
|
||||
markdown = '<code data-placeholder>%{gitlab-server}</code>'
|
||||
|
||||
expect(run_filter(markdown)).to include '<code data-placeholder>%{gitlab-server}</code>'
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline timing check' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
end
|
||||
|
||||
it_behaves_like 'a filter timeout' do
|
||||
let_it_be(:project_tag) { nil }
|
||||
let(:text) { 'text' }
|
||||
end
|
||||
|
||||
it_behaves_like 'limits the number of filtered items' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let(:text) do
|
||||
'<span data-placeholder>%{gitlab_server}</span> <span data-placeholder>%{gitlab_server}</span> ' \
|
||||
'<span data-placeholder>%{gitlab_server}</span>'
|
||||
end
|
||||
|
||||
let(:ends_with) { '<span data-placeholder>%{gitlab_server}</span>' }
|
||||
|
||||
before do
|
||||
stub_const('Banzai::Filter::PlaceholdersPostFilter::FILTER_ITEM_LIMIT', 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -149,24 +149,6 @@ RSpec.describe Banzai::Filter::SanitizationFilter, feature_category: :markdown d
|
|||
expect(filter(html).to_html).to eq(output)
|
||||
end
|
||||
|
||||
it 'allows `data-placeholder` attributes on `span`, `a` and `img` elements' do
|
||||
html = <<-HTML
|
||||
<span class="code" data-placeholder="true">something</span>
|
||||
<a href="example.com" class="code" data-placeholder="true">something</a>
|
||||
<img src="example.com" class="code" data-placeholder="true" />
|
||||
<div data-placeholder-text="true" data-placeholder="true">something</div>
|
||||
HTML
|
||||
|
||||
output = <<-HTML
|
||||
<span data-placeholder="true">something</span>
|
||||
<a href="example.com" data-placeholder="true">something</a>
|
||||
<img src="example.com" data-placeholder="true">
|
||||
<div>something</div>
|
||||
HTML
|
||||
|
||||
expect(filter(html).to_html).to eq(output)
|
||||
end
|
||||
|
||||
it 'allows the `data-sourcepos` attribute globally' do
|
||||
exp = %q(<p data-sourcepos="1:1-1:10">foo/bar.md</p>)
|
||||
act = filter(exp)
|
||||
|
|
|
|||
|
|
@ -8,9 +8,5 @@ RSpec.describe Banzai::Filter, feature_category: :markdown do
|
|||
expect(described_class.filter_item_limit_exceeded?(described_class::FILTER_ITEM_LIMIT - 1)).to be_falsey
|
||||
expect(described_class.filter_item_limit_exceeded?(described_class::FILTER_ITEM_LIMIT + 1)).to be_truthy
|
||||
end
|
||||
|
||||
it 'allows limit top be specified' do
|
||||
expect(described_class.filter_item_limit_exceeded?(2, limit: 1)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline, feature_category: :markdow
|
|||
end
|
||||
|
||||
let(:doc) { HTML::Pipeline.parse(html) }
|
||||
let(:non_related_xpath_calls) { 2 }
|
||||
let(:non_related_xpath_calls) { 1 }
|
||||
|
||||
it 'searches for attributes only once' do
|
||||
expect(doc).to receive(:xpath).exactly(non_related_xpath_calls + 1).times
|
||||
|
|
|
|||
|
|
@ -49,6 +49,17 @@ RSpec.describe Subscribable, 'Subscribable' do
|
|||
expect(resource.public_send(method, user_1, project)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cache_enforced: false' do
|
||||
let(:batch_loader) { instance_double(BatchLoader) }
|
||||
|
||||
it 'calls batch loader with correct params' do
|
||||
expect(batch_loader).to receive(:batch).with(hash_including(cache: false))
|
||||
expect(BatchLoader).to receive(:for).and_return(batch_loader)
|
||||
|
||||
resource.lazy_subscription(user_1, project, cache_enforced: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#subscribed?' do
|
||||
|
|
|
|||
|
|
@ -2542,9 +2542,15 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
|
|||
|
||||
control = ActiveRecord::QueryRecorder.new { perform_request(user) }
|
||||
|
||||
create(:merge_request, :closed, source_project: project, source_branch: 'master', target_branch: 'feature')
|
||||
# 5 queries are intermittently seen to update user_preferences, and select scan_result_policy_violations
|
||||
query_buffer = 5
|
||||
|
||||
expect { perform_request(user) }.not_to exceed_query_limit(control)
|
||||
# Make query_buffer + 1 new merge requests to ensure an n + 1 introduces more than query_buffer queries
|
||||
(query_buffer + 1).times do
|
||||
create(:merge_request, :closed, source_project: project, source_branch: 'master', target_branch: 'feature')
|
||||
end
|
||||
|
||||
expect { perform_request(user) }.not_to exceed_query_limit(control).with_threshold(query_buffer)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -748,6 +748,36 @@ RSpec.describe Notes::QuickActionsService, feature_category: :text_editors do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '/subscribe or /unsubscribe' do
|
||||
shared_examples 'when applying to work_item' do
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note)).to be_empty
|
||||
end
|
||||
|
||||
it 'triggers work item updated subscription' do
|
||||
expect(GraphqlTriggers).to receive(:work_item_updated).with(work_item)
|
||||
|
||||
execute(note)
|
||||
end
|
||||
end
|
||||
|
||||
describe '/subscribe' do
|
||||
let_it_be(:note_text) { '/subscribe' }
|
||||
|
||||
it_behaves_like 'when applying to work_item'
|
||||
end
|
||||
|
||||
describe '/unsubscribe' do
|
||||
let_it_be(:note_text) { '/unsubscribe' }
|
||||
|
||||
before do
|
||||
work_item.subscribe(maintainer, project)
|
||||
end
|
||||
|
||||
it_behaves_like 'when applying to work_item'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,24 @@ RSpec.describe Packages::Debian::GenerateDistributionKeyService, feature_categor
|
|||
expect(GPGME::Ctx).to receive(:new).with(armor: true, offline: true).and_call_original
|
||||
expect(User).to receive(:random_password).with(no_args).and_call_original
|
||||
|
||||
expect(response).to be_a Hash
|
||||
expect(response.keys).to contain_exactly(:private_key, :public_key, :fingerprint, :passphrase)
|
||||
expect(response[:private_key]).to start_with('-----BEGIN PGP PRIVATE KEY BLOCK-----')
|
||||
expect(response[:public_key]).to start_with('-----BEGIN PGP PUBLIC KEY BLOCK-----')
|
||||
expect(response[:fingerprint].length).to eq(40)
|
||||
expect(response[:passphrase].length).to be > 10
|
||||
payload = response.payload
|
||||
expect(response).to be_a ServiceResponse
|
||||
expect(payload.keys).to contain_exactly(:private_key, :public_key, :fingerprint, :passphrase)
|
||||
expect(payload[:private_key]).to start_with('-----BEGIN PGP PRIVATE KEY BLOCK-----')
|
||||
expect(payload[:public_key]).to start_with('-----BEGIN PGP PUBLIC KEY BLOCK-----')
|
||||
expect(payload[:fingerprint].length).to eq(40)
|
||||
expect(payload[:passphrase].length).to be > 10
|
||||
end
|
||||
|
||||
context 'with invalid passphrase parameter' do
|
||||
let(:params) { { passphrase: "k\n%pubring /tmp/file" } }
|
||||
|
||||
it 'returns the error', :aggregate_failures do
|
||||
expect(GPGME::Ctx).not_to receive(:new)
|
||||
expect(response).to be_error.and have_attributes(
|
||||
message: 'Passphrase contains invalid characters',
|
||||
reason: :invalid_passphrase
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module Features
|
||||
module AccessTokenHelpers
|
||||
# Remove after migrate_user_access_tokens_ui feature flag removal
|
||||
def active_access_tokens
|
||||
find_by_testid('active-tokens')
|
||||
end
|
||||
|
|
@ -13,8 +14,32 @@ module Features
|
|||
end
|
||||
end
|
||||
|
||||
# Keep after migrate_user_access_tokens_ui feature flag removal
|
||||
def new_access_token
|
||||
within_testid('new-access-token') do
|
||||
find_by_testid('toggle-visibility-button').click
|
||||
find_field('access-token-field').value
|
||||
end
|
||||
end
|
||||
|
||||
def active_access_tokens_counter
|
||||
find_by_testid('active-token-count')
|
||||
end
|
||||
|
||||
def access_token_table
|
||||
find_by_testid('access-token-table')
|
||||
end
|
||||
|
||||
def active_access_tokens_count
|
||||
find_by_testid('active-tokens-count')
|
||||
end
|
||||
|
||||
def last_used_ip
|
||||
find_by_testid('field-last-used')
|
||||
end
|
||||
|
||||
def access_token_options
|
||||
find_by_testid('access-token-options')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -274,4 +274,17 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the distribution key cannot be generated' do
|
||||
it 'raises the error' do
|
||||
allow(::Packages::Debian::GenerateDistributionKeyService).to receive(:new).and_wrap_original do |m, _|
|
||||
m.call(params: { passphrase: "k\n%pubring /tmp/file" })
|
||||
end
|
||||
|
||||
expect { subject }.to raise_error(
|
||||
::Packages::Debian::GenerateDistributionService::GenerateDistributionError,
|
||||
'Passphrase contains invalid characters'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue