Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-02-12 12:12:56 +00:00
parent 45e2205d7c
commit 357b66a2be
128 changed files with 2126 additions and 1061 deletions

View File

@ -20,7 +20,7 @@
<!-- Any further tasks that need to be completed after the main work of the issue is done, such as announcing the changes. -->
<!-- This section is optional. -->
<!-- Make sure to add one of the type labels (as per https://handbook.gitlab.com/handbook/engineering/metrics/#work-type-classification):-->
<!-- Make sure to add one of the type labels (as per https://handbook.gitlab.com/handbook/product/groups/product-analysis/engineering/metrics/#work-type-classification):-->
<!-- /label ~"type::bug" ~"type::feature" ~"type::tooling" ~"type::maintenance" -->
/label ~devops::analytics ~"group::analytics instrumentation"

View File

@ -23,8 +23,8 @@
<!-- If you can, link to the line of code that might be responsible for the problem. -->
<!-- Please add a label for the type of bug as per https://about.gitlab.com/handbook/engineering/metrics/#work-type-classification -->
<!-- Please add a label for the type of bug as per https://handbook.gitlab.com/handbook/product/groups/product-analysis/engineering/metrics/#work-type-classification -->
/label ~"type::bug"
/label ~"group::global search"
/label ~"workflow::solution validation"
/milestone %Backlog
/milestone %Backlog

View File

@ -6,8 +6,8 @@
<!-- Use this section to explain the feature and how it will work. It can be helpful to add technical details, design proposals, and links to related epics or issues. -->
<!-- Please add a label for the type of feature as per https://about.gitlab.com/handbook/engineering/metrics/#work-type-classification -->
<!-- Please add a label for the type of feature as per https://handbook.gitlab.com/handbook/product/groups/product-analysis/engineering/metrics/#work-type-classification -->
/label ~"type::feature"
/label ~"group::global search"
/label ~"workflow::solution validation"
/milestone %Backlog
/milestone %Backlog

View File

@ -4,8 +4,8 @@
<!-- Use this section to explain the feature and how it will work. It can be helpful to add technical details, design proposals, and links to related epics or issues. -->
<!-- Please add a label for the type of maintenance as per https://about.gitlab.com/handbook/engineering/metrics/#work-type-classification -->
<!-- Please add a label for the type of maintenance as per https://handbook.gitlab.com/handbook/product/groups/product-analysis/engineering/metrics/#work-type-classification -->
/label ~"type::maintenance"
/label ~"group::global search"
/label ~"workflow::solution validation"
/milestone %Backlog
/milestone %Backlog

View File

@ -56,7 +56,7 @@ If you include multiple screenshots it can be helpful to hide all but the first
<!-- Base labels. -->
/label ~Quality ~QA ~test
<!-- Work classification type label, please apply ignore type label until the investigation is complete and an [issue type](https://about.gitlab.com/handbook/engineering/metrics/#work-type-classification) is determined.-->
<!-- Work classification type label, please apply ignore type label until the investigation is complete and an [issue type](https://handbook.gitlab.com/handbook/product/groups/product-analysis/engineering/metrics/#work-type-classification) is determined.-->
/label ~"type::ignore"
<!-- Test failure type label, please use just one.-->

View File

@ -3203,7 +3203,6 @@ Gitlab/BoundedContexts:
- 'ee/app/services/elastic/index_projects_by_range_service.rb'
- 'ee/app/services/elastic/process_bookkeeping_service.rb'
- 'ee/app/services/elastic/process_initial_bookkeeping_service.rb'
- 'ee/app/services/epic_issues/create_service.rb'
- 'ee/app/services/epic_issues/destroy_service.rb'
- 'ee/app/services/epic_issues/list_service.rb'
- 'ee/app/services/epic_issues/update_service.rb'

View File

@ -39,7 +39,6 @@ Layout/ArrayAlignment:
- 'ee/spec/serializers/user_analytics_entity_spec.rb'
- 'ee/spec/services/audit_events/export_csv_service_spec.rb'
- 'ee/spec/services/ee/boards/issues/list_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/incident_management/issuable_resource_links/zoom_link_service_spec.rb'
- 'ee/spec/services/security/security_orchestration_policies/create_pipeline_service_spec.rb'
- 'ee/spec/services/security/token_revocation_service_spec.rb'

View File

@ -72,7 +72,6 @@ Layout/LineContinuationSpacing:
- 'ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
- 'ee/spec/services/boards/epic_lists/destroy_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/phone_verification/telesign_client/risk_score_service_spec.rb'
- 'ee/spec/services/phone_verification/telesign_client/send_verification_code_service_spec.rb'
- 'ee/spec/workers/ee/issuable_export_csv_worker_spec.rb'

View File

@ -300,7 +300,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/spec/services/ee/members/invite_service_spec.rb'
- 'ee/spec/services/ee/post_receive_service_spec.rb'
- 'ee/spec/services/ee/work_items/related_work_item_links/create_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/geo/container_repository_sync_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/add_on_purchases/create_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/add_on_purchases/update_service_spec.rb'

View File

@ -1696,7 +1696,6 @@ Layout/LineLength:
- 'ee/spec/services/ee/users/destroy_service_spec.rb'
- 'ee/spec/services/ee/users/update_service_spec.rb'
- 'ee/spec/services/elastic/process_initial_bookkeeping_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'ee/spec/services/epics/update_service_spec.rb'
- 'ee/spec/services/external_status_checks/update_service_spec.rb'

View File

@ -4,7 +4,6 @@ Lint/NoReturnInBeginEndBlocks:
- 'app/models/concerns/metric_image_uploading.rb'
- 'app/models/merge_request.rb'
- 'app/services/security/ci_configuration/sast_parser_service.rb'
- 'ee/app/services/epic_issues/create_service.rb'
- 'ee/app/services/gitlab_subscriptions/preview_billable_user_change_service.rb'
- 'ee/app/services/security/token_revocation_service.rb'
- 'ee/lib/api/vulnerability_findings.rb'

View File

@ -6,7 +6,6 @@ Performance/FlatMap:
- 'ee/app/models/burndown.rb'
- 'ee/app/models/geo_node_status.rb'
- 'ee/app/serializers/dashboard_environments_serializer.rb'
- 'ee/app/services/elastic/process_bookkeeping_service.rb'
- 'ee/spec/requests/api/members_spec.rb'
- 'ee/spec/support/helpers/license_scanning_report_helpers.rb'
- 'lib/gitlab/ci/pipeline/chain/ensure_environments.rb'

View File

@ -553,7 +553,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/services/ee/todos/destroy/entity_leave_service_spec.rb'
- 'ee/spec/services/ee/two_factor/destroy_service_spec.rb'
- 'ee/spec/services/ee/work_items/import_csv_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/epic_issues/update_service_spec.rb'
- 'ee/spec/services/epics/close_service_spec.rb'
- 'ee/spec/services/epics/create_service_spec.rb'

View File

@ -97,7 +97,6 @@ RSpec/ExpectChange:
- 'ee/spec/services/ee/users/block_service_spec.rb'
- 'ee/spec/services/ee/users/create_service_spec.rb'
- 'ee/spec/services/ee/users/destroy_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'ee/spec/services/epics/transfer_service_spec.rb'
- 'ee/spec/services/geo/container_repository_registry_removal_service_spec.rb'

View File

@ -902,7 +902,6 @@ RSpec/NamedSubject:
- 'ee/spec/services/ee/users/build_service_spec.rb'
- 'ee/spec/services/ee/work_items/import_csv_service_spec.rb'
- 'ee/spec/services/elastic/metrics_update_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/epic_issues/destroy_service_spec.rb'
- 'ee/spec/services/epic_issues/list_service_spec.rb'
- 'ee/spec/services/epic_issues/update_service_spec.rb'

View File

@ -148,7 +148,6 @@ RSpec/ReceiveMessages:
- 'ee/spec/services/ee/post_receive_service_spec.rb'
- 'ee/spec/services/ee/spam/spam_verdict_service_spec.rb'
- 'ee/spec/services/ee/work_items/related_work_item_links/create_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/geo/container_repository_sync_spec.rb'
- 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb'

View File

@ -48,7 +48,6 @@ RSpec/ScatteredLet:
- 'ee/spec/services/ee/issue_links/create_service_spec.rb'
- 'ee/spec/services/ee/issues/create_service_spec.rb'
- 'ee/spec/services/ee/merge_requests/base_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'ee/spec/services/geo/metrics_update_service_spec.rb'
- 'ee/spec/services/group_saml/saml_provider/create_service_spec.rb'

View File

@ -1279,7 +1279,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/services/ee/users/update_service.rb'
- 'ee/app/services/elastic/index_projects_by_range_service.rb'
- 'ee/app/services/elastic/process_bookkeeping_service.rb'
- 'ee/app/services/epic_issues/create_service.rb'
- 'ee/app/services/epics/strategies/base_dates_strategy.rb'
- 'ee/app/services/epics/strategies/due_date_inherited_strategy.rb'
- 'ee/app/services/epics/strategies/start_date_inherited_strategy.rb'

View File

@ -186,7 +186,6 @@ Style/RedundantSelf:
- 'ee/app/models/push_rule.rb'
- 'ee/app/models/security/orchestration_policy_configuration.rb'
- 'ee/app/models/vulnerabilities/finding.rb'
- 'ee/app/services/elastic/process_bookkeeping_service.rb'
- 'ee/lib/api/dependencies.rb'
- 'ee/lib/ee/gitlab/auth/ldap/sync/groups.rb'
- 'ee/lib/ee/gitlab/auth/ldap/sync/proxy.rb'

View File

@ -2,6 +2,25 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 17.8.2 (2025-02-11)
### Fixed (3 changes)
- [Fix storing incorrect policy index in scan_result_policies](https://gitlab.com/gitlab-org/security/gitlab/-/commit/49e2809628b91b4200b62faf512ae73ba4bfaa27) **GitLab Enterprise Edition**
- [Enable ai tracking even with feature flag disabled](https://gitlab.com/gitlab-org/security/gitlab/-/commit/27526bdc24302c80940356e93928956ea6cb21d3) **GitLab Enterprise Edition**
- [Fix Workhorse failing on 64-bit unaligned access on Raspberry Pi 32-bit](https://gitlab.com/gitlab-org/security/gitlab/-/commit/a026bcb6a7a0a69329ab6cc63cfe2ef51a7d2d83)
### Security (8 changes)
- [Security Duo Chat Escape Unknown Domain Hyperlinks](https://gitlab.com/gitlab-org/security/gitlab/-/commit/44436a9c648b077a89efb5d2b394f36702f0e315) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4706))
- [Hide sensitive workhorse headers and fix route confusion between web and workhorse routes](https://gitlab.com/gitlab-org/security/gitlab/-/commit/80e0601861d797ed6126b999c5830409ee5e8abf) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4760))
- [Do not allow Planner role to update or delete incidents](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3c76c42d1451fea9f74aec4ff31d17483f8c2d14) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4746))
- [Reduce memory allocations on create PAT endpoint](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3183ac5d359b349b248dfb6d094e6791b2cf716a) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4750))
- [Prevent SSRF attacks for Workspaces](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ad1ddf3353d1817d3b7eb583ea333dab0dd3f6a2) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4729))
- [Prevent read code access when repository is disabled](https://gitlab.com/gitlab-org/security/gitlab/-/commit/be2a9c24d18e2735f4d8e640bfd61633851da60e) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4705))
- [Fixes XSS on the target branch in the merge request widget](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3de176b1ee5c0df452d265a9ca39ae950c9553aa) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4735))
- [Unsubscribe from actioncable channel when PAT is revoked](https://gitlab.com/gitlab-org/security/gitlab/-/commit/85760efaf82d85241732360045a1763095740049) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4725))
## 17.8.1 (2025-01-22)
### Security (4 changes)
@ -467,6 +486,19 @@ entry.
- [Remove default on `group_saved_replies_flag feature flag](https://gitlab.com/gitlab-org/gitlab/-/commit/75d49fe13646e1e0d3b68233ac4a965c86853917) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175647))
- [Remove use_actual_plan_in_license_check flag](https://gitlab.com/gitlab-org/gitlab/-/commit/b8c3fe16aedb69c82ff52d1c695d72e933c4b946) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175649))
## 17.7.4 (2025-02-11)
### Security (8 changes)
- [Security Duo Chat Escape Unknown Domain Hyperlinks](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d3eafa571712e6891f16ecccaaefd82b147b75f6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4691))
- [Hide sensitive workhorse headers and fix route confusion between web and workhorse routes](https://gitlab.com/gitlab-org/security/gitlab/-/commit/af871eb34f21f862bce699839af69c88826a3420) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4762))
- [Do not allow Planner role to update or delete incidents](https://gitlab.com/gitlab-org/security/gitlab/-/commit/f5ae9423dbd353f571ffbea5a8ffe2ac77b587d6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4747))
- [Reduce memory allocations on create PAT endpoint](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d443ded9eaed1300b888594125684db884c88e4d) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4751))
- [Prevent SSRF attacks for Workspaces](https://gitlab.com/gitlab-org/security/gitlab/-/commit/03fbdbe7b80e1028098df6bb10abc749b4f4b968) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4730))
- [Prevent read code access when repository is disabled](https://gitlab.com/gitlab-org/security/gitlab/-/commit/fb3eb2135770abcea4951ffe432cebb2065e7d3c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4700))
- [Fixes XSS on the target branch in the merge request widget](https://gitlab.com/gitlab-org/security/gitlab/-/commit/f4fd06e3450f686817104895eb6aca42af4fab11) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4736))
- [Unsubscribe from actioncable channel when PAT is revoked](https://gitlab.com/gitlab-org/security/gitlab/-/commit/972f392e7daa6b60ed8ff03e6651944e1d045b40) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4726))
## 17.7.3 (2025-01-22)
### Fixed (1 change)
@ -1318,6 +1350,18 @@ entry.
- [Finalize migration BackfillMlExperimentMetadataProjectId](https://gitlab.com/gitlab-org/gitlab/-/commit/0768d34e5d66ec56aa9104206120d2b691d3781f) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172003))
- [Finalize migration BackfillDastSiteValidationsProjectId](https://gitlab.com/gitlab-org/gitlab/-/commit/edb777429d66afe879a5bb8d4652a610eb39eb7c) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171199))
## 17.6.5 (2025-02-11)
### Security (7 changes)
- [Security Duo Chat Escape Unknown Domain Hyperlinks](https://gitlab.com/gitlab-org/security/gitlab/-/commit/cdb737c04cdf611b2f6818a294b7157039adcce8) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4692))
- [Hide sensitive workhorse headers and fix route confusion between web and workhorse routes](https://gitlab.com/gitlab-org/security/gitlab/-/commit/dd5fb5b4e217868aa8602acee276883ae8e42126) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4763))
- [Reduce memory allocations on create PAT endpoint](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d86c90fdfee1aef2eaa958ddc9e0ba379f8e221e) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4752))
- [Prevent SSRF attacks for Workspaces](https://gitlab.com/gitlab-org/security/gitlab/-/commit/16659a9efb33ec22055b927fd716f5acc80361e9) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4731))
- [Prevent read code access when repository is disabled](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ff08db2dd2efa55e4e868591c61c144ec3febe32) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4701))
- [Fixes XSS on the target branch in the merge request widget](https://gitlab.com/gitlab-org/security/gitlab/-/commit/1cc0ad7a4f3f0ab44dd959a58b3ed63786037a06) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4737))
- [Unsubscribe from actioncable channel when PAT is revoked](https://gitlab.com/gitlab-org/security/gitlab/-/commit/26fff506ff66eedea4dc911eb1c9f4686d643650) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4727))
## 17.6.4 (2025-01-22)
### Fixed (2 changes)

View File

@ -591,7 +591,7 @@ group :test do
gem 'shoulda-matchers', '~> 5.1.0', require: false, feature_category: :shared
gem 'email_spec', '~> 2.3.0', feature_category: :shared
gem 'webmock', '~> 3.24.0', feature_category: :shared
gem 'webmock', '~> 3.25.0', feature_category: :shared
gem 'rails-controller-testing', feature_category: :shared
gem 'concurrent-ruby', '~> 1.1', feature_category: :shared
gem 'test-prof', '~> 1.4.0', feature_category: :tooling

View File

@ -784,7 +784,7 @@
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
{"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
{"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"},
{"name":"webmock","version":"3.24.0","platform":"ruby","checksum":"be01357f6fc773606337ca79f3ba332b7d52cbe5c27587671abc0572dbec7122"},
{"name":"webmock","version":"3.25.0","platform":"ruby","checksum":"573c23fc4887008c830f22da588db339ca38b6d59856fd57f5a068959474198e"},
{"name":"webrick","version":"1.8.2","platform":"ruby","checksum":"431746a349199546ff9dd272cae10849c865f938216e41c402a6489248f12f21"},
{"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
{"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"},

View File

@ -1959,7 +1959,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.24.0)
webmock (3.25.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -2349,7 +2349,7 @@ DEPENDENCIES
vmstat (~> 2.3.0)
warning (~> 1.3.0)
webauthn (~> 3.0)
webmock (~> 3.24.0)
webmock (~> 3.25.0)
webrick (~> 1.8.1)
wikicloth (= 0.8.1)
yajl-ruby (~> 1.4.3)

View File

@ -797,7 +797,7 @@
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
{"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
{"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"},
{"name":"webmock","version":"3.24.0","platform":"ruby","checksum":"be01357f6fc773606337ca79f3ba332b7d52cbe5c27587671abc0572dbec7122"},
{"name":"webmock","version":"3.25.0","platform":"ruby","checksum":"573c23fc4887008c830f22da588db339ca38b6d59856fd57f5a068959474198e"},
{"name":"webrick","version":"1.8.2","platform":"ruby","checksum":"431746a349199546ff9dd272cae10849c865f938216e41c402a6489248f12f21"},
{"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
{"name":"websocket-driver","version":"0.7.7","platform":"java","checksum":"e2520a6049feb88691e042d631063fa96d50620fb7f53b30180ae6fb2cf75eb1"},

View File

@ -1993,7 +1993,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.24.0)
webmock (3.25.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -2384,7 +2384,7 @@ DEPENDENCIES
vmstat (~> 2.3.0)
warning (~> 1.3.0)
webauthn (~> 3.0)
webmock (~> 3.24.0)
webmock (~> 3.25.0)
webrick (~> 1.8.1)
wikicloth (= 0.8.1)
yajl-ruby (~> 1.4.3)

View File

@ -1,6 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlDisclosureDropdown, GlModalDirective, GlLink } from '@gitlab/ui';
import { GlBreadcrumb, GlDisclosureDropdown, GlModalDirective, GlLink } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
import { joinPaths, escapeFileUrl, buildURLwithRefType } from '~/lib/utils/url_utility';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
@ -12,6 +12,7 @@ import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import featureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { logError } from '~/lib/logger';
const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob';
const NEW_DIRECTORY_MODAL_ID = 'modal-new-directory';
@ -19,6 +20,7 @@ const NEW_DIRECTORY_MODAL_ID = 'modal-new-directory';
export default {
components: {
ClipboardButton,
GlBreadcrumb,
GlDisclosureDropdown,
UploadBlobModal,
NewDirectoryModal,
@ -40,7 +42,10 @@ export default {
},
update: (data) => data.project?.userPermissions,
error(error) {
throw error;
logError(
`Failed to fetch user permissions. See exception details for more information.`,
error,
);
},
},
},
@ -303,8 +308,11 @@ export default {
gfmCopyText() {
return `\`${this.currentPath}\``;
},
showCopyButton() {
return this.glFeatures.blobOverflowMenu && this.currentPath?.trim().length;
doesCurrentPathExist() {
return this.currentPath?.trim().length;
},
crumbs() {
return this.pathLinks.map(({ name, url, ...rest }) => ({ text: name, to: url, ...rest }));
},
},
methods: {
@ -317,6 +325,7 @@ export default {
<template>
<nav
v-if="!glFeatures.blobOverflowMenu"
:aria-label="__('Files breadcrumb')"
:data-current-path="currentDirectoryPath"
class="js-repo-breadcrumbs gl-flex"
@ -339,14 +348,6 @@ export default {
/>
</li>
</ol>
<clipboard-button
v-if="showCopyButton"
:text="currentPath"
:gfm="gfmCopyText"
:title="__('Copy file path')"
category="tertiary"
css-class="gl-mx-2"
/>
<upload-blob-modal
v-if="showUploadModal"
:modal-id="$options.uploadBlobModalId"
@ -367,4 +368,21 @@ export default {
:path="newDirectoryPath"
/>
</nav>
<div v-else class="gl-flex gl-w-full gl-justify-between sm:gl-w-auto">
<gl-breadcrumb
:items="crumbs"
:data-current-path="currentDirectoryPath"
:aria-label="__('Files breadcrumb')"
size="md"
class="breadcrumb-item"
/>
<clipboard-button
v-if="doesCurrentPathExist"
:text="currentPath"
:gfm="gfmCopyText"
:title="__('Copy file path')"
category="tertiary"
css-class="gl-mx-2"
/>
</div>
</template>

View File

@ -25,10 +25,7 @@ export default {
...mapGetters(['hasMissingProjectContext']),
...mapState(['groupInitialJson', 'searchType']),
shouldShowSourceBranchFilter() {
return (
this.glFeatures.searchMrFilterSourceBranch &&
(!this.hasMissingProjectContext || this.groupInitialJson?.id)
);
return !this.hasMissingProjectContext || this.groupInitialJson?.id;
},
isAdvancedSearch() {
return this.searchType === SEARCH_TYPE_ADVANCED;

View File

@ -127,6 +127,11 @@ export default {
type: String,
required: true,
},
isPostMerge: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -322,11 +327,13 @@ export default {
<template v-if="showSourceBranch">
{{ s__('Pipeline|on') }}
<tooltip-on-truncate
v-safe-html="sourceBranchLink"
:title="sourceBranch"
truncate-target="child"
class="label-branch label-truncate ref-container"
/>
>
<template v-if="isPostMerge">{{ sourceBranchLink }}</template>
<span v-else v-safe-html="sourceBranchLink"></span>
</tooltip-on-truncate>
</template>
<template v-if="finishedAt">
<time-ago-tooltip

View File

@ -93,6 +93,7 @@ export default {
:target-project-id="mr.targetProjectId"
:iid="mr.iid"
:target-project-full-path="mr.targetProjectFullPath"
:is-post-merge="isPostMerge"
/>
<template #footer>
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">

View File

@ -7,7 +7,7 @@ export default {
title: 'vue_shared/tooltip_on_truncate/tooltip_on_truncate.vue',
};
const createStory = ({ ...options }) => {
const createStory = ({ ...options } = {}) => {
return (_, { argTypes }) => {
const comp = {
components: { TooltipOnTruncate },

View File

@ -6,11 +6,12 @@ module ApplicationCable
include Gitlab::Auth::AuthFinders
before_subscribe :validate_token_scope
periodically :validate_token_scope, every: 10.minutes
def validate_token_scope
validate_and_save_access_token!(scopes: authorization_scopes)
validate_and_save_access_token!(scopes: authorization_scopes, reset_token: true)
rescue Gitlab::Auth::AuthenticationError
reject
handle_authentication_error
end
def authorization_scopes
@ -19,6 +20,18 @@ module ApplicationCable
private
def client_subscribed?
!subscription_rejected? && subscription_confirmation_sent?
end
def handle_authentication_error
if client_subscribed?
unsubscribe_from_channel
else
reject
end
end
def notification_payload(_)
super.merge!(params: params.except(:channel))
end

View File

@ -12,13 +12,22 @@ class JwksController < Doorkeeper::OpenidConnect::DiscoveryController
def payload
[
Rails.application.credentials.openid_connect_signing_key,
Gitlab::CurrentSettings.ci_jwt_signing_key
].compact.map do |key_data|
OpenSSL::PKey::RSA.new(key_data)
Gitlab::CurrentSettings.ci_jwt_signing_key,
cloud_connector_keys
].flatten.compact.map { |key_data| pem_to_jwk(key_data) }.uniq
end
def cloud_connector_keys
return unless Gitlab.ee?
CloudConnector::Keys.all_as_pem
end
def pem_to_jwk(key_data)
OpenSSL::PKey::RSA.new(key_data)
.public_key
.to_jwk
.slice(:kty, :kid, :e, :n)
.merge(use: 'sig', alg: 'RS256')
end
end
end

View File

@ -15,7 +15,21 @@ module UserSettings
def index
set_index_vars
scopes = params[:scopes].split(',').map(&:squish).select(&:present?).map(&:to_sym) unless params[:scopes].nil?
unless params[:scopes].nil?
scopes = []
# An over engineered solution to generate scopes array without extra object allocations
params[:scopes].split(",", Gitlab::Auth.all_available_scopes.count + 1) do |scope|
scope = scope.squish
next if scope.empty?
scope = scope.to_sym
next if Gitlab::Auth.all_available_scopes.exclude?(scope)
scopes << scope
end
end
@personal_access_token = finder.build(
name: params[:name],
description: params[:description],

View File

@ -48,28 +48,28 @@ class KeysFinder
keys.for_user(params[:users])
end
def by_created_before(tokens)
return tokens unless params[:created_before]
def by_created_before(keys)
return keys unless params[:created_before]
tokens.created_before(params[:created_before])
keys.created_before(params[:created_before])
end
def by_created_after(tokens)
return tokens unless params[:created_after]
def by_created_after(keys)
return keys unless params[:created_after]
tokens.created_after(params[:created_after])
keys.created_after(params[:created_after])
end
def by_expires_before(tokens)
return tokens unless params[:expires_before]
def by_expires_before(keys)
return keys unless params[:expires_before]
tokens.expires_before(params[:expires_before])
keys.expires_before(params[:expires_before])
end
def by_expires_after(tokens)
return tokens unless params[:expires_after]
def by_expires_after(keys)
return keys unless params[:expires_after]
tokens.expires_after(params[:expires_after])
keys.expires_after(params[:expires_after])
end
def by_fingerprint(keys)

View File

@ -16,9 +16,9 @@ module Types
method: :itself
field :id,
type: GraphQL::Types::ID,
type: Types::GlobalIDType[::User],
null: false,
description: 'ID of the user.'
description: 'Global ID of the user.'
field :bot,
type: GraphQL::Types::Boolean,
null: false,

View File

@ -0,0 +1,130 @@
# frozen_string_literal: true
module Ci
module JobToken
class AllowlistMigrationTask
include Gitlab::Utils::StrongMemoize
attr_reader :only_ids, :exclude_ids
INPUT_ID_LIMIT = 1000
def initialize(only_ids: nil, exclude_ids: nil, preview: nil, user: nil, output_stream: $stdout)
@only_ids = parse_project_ids(only_ids)
@exclude_ids = parse_project_ids(exclude_ids)
@preview = !preview.blank?
@user = user
@output_stream = output_stream
end
def execute
if valid_configuration?
@output_stream.puts preview_banner if preview_mode?
@output_stream.puts start_message
migrate!
@output_stream.puts finish_message
else
@output_stream.puts configuration_error_banner
end
end
private
def migrate!
if @only_ids.present?
migrate_batch(@only_ids)
else
ProjectCiCdSetting.each_batch do |batch|
batch = batch.where(inbound_job_token_scope_enabled: false)
project_ids = batch.pluck(:project_id) - @exclude_ids # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- pluck limited to batch size
migrate_batch(project_ids)
end
end
end
def migrate_batch(project_ids)
Project.where(id: project_ids).preload(:ci_cd_settings).find_each do |project|
@output_stream.puts migrate_project(project)
end
end
def migrate_project(project)
if preview_mode?
"Would have migrated project id: #{project.id}."
else
perform_migration!(project)
"Migrated project id: #{project.id}."
end
rescue StandardError => error
"Error migrating project id: #{project.id}, error: #{error.message}"
end
def perform_migration!(project)
service = ::Ci::JobToken::AutopopulateAllowlistService.new(project, @user) # rubocop:disable CodeReuse/ServiceClass -- This class is not an ActiveRecord model
service.unsafe_execute!
end
def valid_configuration?
(@only_ids.empty? || @exclude_ids.empty?) &&
@only_ids.size <= INPUT_ID_LIMIT &&
@exclude_ids.size <= INPUT_ID_LIMIT
end
def preview_mode?
@preview
end
def start_message
if preview_mode?
"\nMigrating project(s) in preview mode...\n\n"
else
"\nMigrating project(s)...\n\n"
end
end
def finish_message
if preview_mode?
"\nMigration complete in preview mode.\n\n"
else
"\nMigration complete.\n\n"
end
end
def preview_banner
banner("PREVIEW MODE ENABLED")
end
def configuration_error_banner
error = if @only_ids.size > INPUT_ID_LIMIT || @exclude_ids.size > INPUT_ID_LIMIT
"must contain less than #{INPUT_ID_LIMIT} items"
else
"cannot both be set"
end
banner("ERROR: ONLY_PROJECT_IDS and EXCLUDE_PROJECT_IDS #{error}, try again.")
end
def parse_project_ids(ids_string)
return [] if ids_string.blank?
ids_string.split(',')
.map(&:strip)
.map(&:to_i)
.uniq
end
def banner(message)
output = []
output << "##########"
output << "#"
output << "# #{message}"
output << "#"
output << "##########"
output.join("\n")
end
end
end
end

View File

@ -7,11 +7,10 @@ module Ci
attr_reader :allowlist_groups, :allowlist_projects
UnexpectedCompactionEntry = Class.new(StandardError)
RedundantCompactionEntry = Class.new(StandardError)
Error = Class.new(StandardError)
def initialize(project_id)
@project_id = project_id
def initialize(project)
@project = project
@allowlist_groups = []
@allowlist_projects = []
end
@ -23,7 +22,7 @@ module Ci
# Collecting id batches to avoid cross-database transactions.
Ci::JobToken::Authorization.where(
accessed_project_id: @project_id
accessed_project_id: @project.id
).each_batch(column: :origin_project_id) do |batch|
origin_project_id_batches << batch.pluck(:origin_project_id) # rubocop:disable Database/AvoidUsingPluckWithoutLimit -- pluck limited by batch size
end
@ -55,6 +54,12 @@ module Ci
@allowlist_groups << namespace
end
end
rescue Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError,
Gitlab::Utils::TraversalIdCompactor::RedundantCompactionEntry,
Gitlab::Utils::TraversalIdCompactor::UnexpectedCompactionEntry => error
raise Error, error.class.name.demodulize
end
def path_already_allowlisted?(namespace_path)
@ -65,7 +70,7 @@ module Ci
end
def existing_links_traversal_ids
allowlist = Ci::JobToken::Allowlist.new(Project.find(@project_id))
allowlist = Ci::JobToken::Allowlist.new(@project)
allowlist.group_link_traversal_ids + allowlist.project_link_traversal_ids
end
strong_memoize_attr :existing_links_traversal_ids

View File

@ -331,14 +331,6 @@ module Ci
end
end
# TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/516929 is closed.
def self.sharded_table_proxy_model
@sharded_table_proxy_class ||= Class.new(self) do
self.table_name = :ci_runners_e59bb2812d
self.primary_key = :id
end
end
def self.taggings_join_model
::Ci::RunnerTagging
end
@ -445,14 +437,6 @@ module Ci
tag_list.any?
end
# TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/516929 is closed.
def ensure_partitioned_runner_record_exists
self.class.sharded_table_proxy_model.insert_all(
[attributes.except('tag_list')], unique_by: [:id, :runner_type],
returning: false, record_timestamps: false
)
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_RUNNER_ID', value: id.to_s)
@ -539,10 +523,6 @@ module Ci
def ensure_manager(system_xid)
# rubocop: disable Performance/ActiveRecordSubtransactionMethods -- This is used only in API endpoints outside of transactions
RunnerManager.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s) do |m|
# Avoid inserting partitioned runner managers that refer to a missing ci_runners partitioned record, since
# the backfill is not yet finalized.
ensure_partitioned_runner_record_exists if Feature.disabled?(:reject_orphaned_runners, Feature.current_request)
m.runner_type = runner_type
m.sharding_key_id = sharding_key_id
end
@ -633,7 +613,7 @@ module Ci
end
def any_project
errors.add(:runner, 'needs to be assigned to at least one project') unless runner_projects.any?
errors.add(:runner, 'needs to be assigned to at least one project') if runner_projects.empty?
end
def exactly_one_group

View File

@ -19,7 +19,7 @@ module Ci
belongs_to :tag, class_name: 'Ci::Tag', optional: false
validates :runner_type, presence: true
validates :sharding_key_id, presence: true
validate :check_sharding_key_id
scope :scoped_runners, -> do
where(arel_table[:runner_id].eq(Ci::Runner.arel_table[Ci::Runner.primary_key]))
@ -32,5 +32,17 @@ module Ci
def set_runner_type
self.runner_type = runner.runner_type
end
def check_sharding_key_id
if runner_type == 'instance_type'
return if sharding_key_id.nil?
errors.add(:sharding_key_id, 'instance_type runners must not have a sharding_key_id')
else
return if sharding_key_id.present?
errors.add(:sharding_key_id, 'non-instance_type runners must have a sharding_key_id')
end
end
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
module Integrations
module Base
module Pivotaltracker
extend ActiveSupport::Concern
API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
class_methods do
def title
'Pivotal Tracker'
end
def description
s_('PivotalTrackerService|Add commit messages as comments to Pivotal Tracker stories.')
end
def help
build_help_page_url(
'user/project/integrations/pivotal_tracker.md',
s_("Add commit messages as comments to Pivotal Tracker stories.")
)
end
def to_param
'pivotaltracker'
end
def supported_events
%w[push]
end
end
included do
validates :token, presence: true, if: :activated?
field :token,
type: :password,
help: -> do
s_('PivotalTrackerService|Pivotal Tracker API token. User must have access to the story. ' \
'All comments are attributed to this user.')
end,
description: -> { _('The Pivotal Tracker token.') },
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
required: true
field :restrict_to_branch,
title: -> { s_('Integrations|Restrict to branch (optional)') },
help: -> do
s_('PivotalTrackerService|Comma-separated list of branches to ' \
'automatically inspect. Leave blank to include all branches.')
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
return unless allowed_branch?(data[:ref])
data[:commits].each do |commit|
message = {
'source_commit' => {
'commit_id' => commit[:id],
'author' => commit[:author][:name],
'url' => commit[:url],
'message' => commit[:message]
}
}
Gitlab::HTTP.post(
API_ENDPOINT,
body: Gitlab::Json.dump(message),
headers: {
'Content-Type' => 'application/json',
'X-TrackerToken' => token
}
)
end
end
def avatar_url
ActionController::Base
.helpers
.image_path('illustrations/third-party-logos/integrations-logos/pivotal-tracker.svg')
end
private
def allowed_branch?(ref)
return true unless ref.present? && restrict_to_branch.present?
branch = Gitlab::Git.ref_name(ref)
allowed_branches = restrict_to_branch.split(',').map(&:strip)
branch.present? && allowed_branches.include?(branch)
end
end
end
end
end

View File

@ -3,7 +3,7 @@
module Integrations
module Instance
class Pivotaltracker < Integration
# To be updated as part of https://gitlab.com/gitlab-org/gitlab/-/issues/474809
include Integrations::Base::Pivotaltracker
end
end
end

View File

@ -2,84 +2,6 @@
module Integrations
class Pivotaltracker < Integration
API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
validates :token, presence: true, if: :activated?
field :token,
type: :password,
help: -> { s_('PivotalTrackerService|Pivotal Tracker API token. User must have access to the story. All comments are attributed to this user.') },
description: -> { _('The Pivotal Tracker token.') },
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
required: true
field :restrict_to_branch,
title: -> { s_('Integrations|Restrict to branch (optional)') },
help: -> do
s_('PivotalTrackerService|Comma-separated list of branches to ' \
'automatically inspect. Leave blank to include all branches.')
end
def self.title
'Pivotal Tracker'
end
def self.description
s_('PivotalTrackerService|Add commit messages as comments to Pivotal Tracker stories.')
end
def self.help
build_help_page_url(
'user/project/integrations/pivotal_tracker.md', s_("Add commit messages as comments to Pivotal Tracker stories.")
)
end
def self.to_param
'pivotaltracker'
end
def self.supported_events
%w[push]
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
return unless allowed_branch?(data[:ref])
data[:commits].each do |commit|
message = {
'source_commit' => {
'commit_id' => commit[:id],
'author' => commit[:author][:name],
'url' => commit[:url],
'message' => commit[:message]
}
}
Gitlab::HTTP.post(
API_ENDPOINT,
body: Gitlab::Json.dump(message),
headers: {
'Content-Type' => 'application/json',
'X-TrackerToken' => token
}
)
end
end
def avatar_url
ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pivotal-tracker.svg')
end
private
def allowed_branch?(ref)
return true unless ref.present? && restrict_to_branch.present?
branch = Gitlab::Git.ref_name(ref)
allowed_branches = restrict_to_branch.split(',').map(&:strip)
branch.present? && allowed_branches.include?(branch)
end
include Integrations::Base::Pivotaltracker
end
end

View File

@ -2,6 +2,7 @@
class ProjectCiCdSetting < ApplicationRecord
include ChronicDurationAttribute
include EachBatch
belongs_to :project, inverse_of: :ci_cd_settings

View File

@ -159,6 +159,15 @@ class IssuePolicy < IssuablePolicy
enable :clone_issue
end
rule { is_incident & ~can?(:reporter_access) }.policy do
prevent :admin_issue
prevent :update_issue
end
rule { is_incident & ~can?(:owner_access) }.policy do
prevent :destroy_issue
end
# IMPORTANT: keep the prevent rules as last rules defined in the policy, as these are based on
# all abilities defined up to this point.
rule { group_issue & ~group_level_issues_license_available }.policy do

View File

@ -32,6 +32,15 @@ class WorkItemPolicy < IssuePolicy
enable :clone_work_item
end
rule { is_incident & ~can?(:reporter_access) }.policy do
prevent :update_work_item
prevent :admin_work_item
end
rule { is_incident & ~can?(:owner_access) }.policy do
prevent :delete_work_item
end
# IMPORTANT: keep the prevent rules as last rules defined in the policy, as these are based on
# all abilities defined up to this point.
rule { group_issue & ~group_level_issues_license_available }.policy do

View File

@ -28,7 +28,9 @@ class IssuableSidebarBasicEntity < Grape::Entity
end
expose :can_edit do |issuable|
can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
subject = issuable.try(:incident_type_issue?) ? issuable : issuable.project
can?(current_user, :"admin_#{issuable.to_ability_name}", subject)
end
expose :can_move do |issuable|

View File

@ -13,32 +13,34 @@ module Ci
@user = user
end
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
def unsafe_execute!
allowlist = Ci::JobToken::Allowlist.new(@project)
groups = compactor.allowlist_groups
projects = compactor.allowlist_projects
ApplicationRecord.transaction do
allowlist.bulk_add_groups!(groups, user: @user, autopopulated: true)
allowlist.bulk_add_projects!(projects, user: @user, autopopulated: true)
allowlist.bulk_add_groups!(groups, user: @user, autopopulated: true) if groups.any?
allowlist.bulk_add_projects!(projects, user: @user, autopopulated: true) if projects.any?
end
enable_enforcement!
ServiceResponse.success
rescue Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError,
Gitlab::Utils::TraversalIdCompactor::RedundantCompactionEntry,
Gitlab::Utils::TraversalIdCompactor::UnexpectedCompactionEntry,
Ci::JobToken::AuthorizationsCompactor::UnexpectedCompactionEntry,
Ci::JobToken::AuthorizationsCompactor::RedundantCompactionEntry => e
rescue Ci::JobToken::AuthorizationsCompactor::Error => e
Gitlab::ErrorTracking.log_exception(e, { project_id: @project.id, user_id: @user.id })
ServiceResponse.error(message: e.message)
end
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
unsafe_execute!
end
private
def compactor
Ci::JobToken::AuthorizationsCompactor.new(@project.id).tap do |compactor|
Ci::JobToken::AuthorizationsCompactor.new(@project).tap do |compactor|
compactor.compact(COMPACTION_LIMIT)
end
end
@ -47,6 +49,10 @@ module Ci
def authorized?
@user.can?(:admin_project, @project)
end
def enable_enforcement!
@project.ci_cd_settings.update!(inbound_job_token_scope_enabled: true)
end
end
end
end

View File

@ -32,6 +32,7 @@ module Import
# We use :owner_access here because it's shared between GroupPolicy and
# NamespacePolicy.
return error_invalid_permissions unless current_user.can?(:owner_access, namespace)
return error_no_source_users if import_source_users.empty?
ServiceResponse.success(payload: csv_data)
end
@ -55,6 +56,13 @@ module Import
reason: :forbidden
)
end
def error_no_source_users
ServiceResponse.error(
message: s_('No placeholder users are awaiting reassignment.'),
reason: :no_source_users
)
end
end
end
end

View File

@ -8,7 +8,7 @@ module IncidentManagement
attr_reader :incident
def allowed?
current_user&.can?(:admin_issue, project)
current_user&.can?(:admin_issue, incident)
end
def success

View File

@ -29,7 +29,7 @@ class JsonSchemaValidator < ActiveModel::EachValidator
value = Gitlab::Json.parse(value.to_s) if options[:parse_json] == true && !value.nil?
if options[:detail_errors]
validator.validate(value).each do |error|
schema.validate(value).each do |error|
message = format_error_message(error)
record.errors.add(attribute, message)
end
@ -38,6 +38,10 @@ class JsonSchemaValidator < ActiveModel::EachValidator
end
end
def schema
@schema ||= JSONSchemer.schema(Pathname.new(schema_path))
end
private
attr_reader :base_directory
@ -69,11 +73,7 @@ class JsonSchemaValidator < ActiveModel::EachValidator
end
def valid_schema?(value)
validator.valid?(value)
end
def validator
@validator ||= JSONSchemer.schema(Pathname.new(schema_path))
schema.valid?(value)
end
def schema_path

View File

@ -21,7 +21,7 @@
- if runner.belongs_to_one_project?
= link_button_to _('Remove runner'), project_runner_path(@project, runner), aria: { label: _('Remove') }, data: { confirm: _("Are you sure?"), 'confirm-btn-variant': 'danger' }, method: :delete, variant: :danger
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord
- runner_project = @project.runner_projects.find_by_runner_id(runner)
= link_button_to _('Disable for this project'), project_runner_project_path(@project, runner_project), aria: { label: _('Disable') }, data: { confirm: _("Are you sure?"), 'confirm-btn-variant': 'danger' }, method: :delete, variant: :danger
- elsif runner.project_type?
= form_for [@project, @project.runner_projects.new] do |f|

View File

@ -2,7 +2,7 @@
- include_private = local_assigns.fetch(:include_private, false)
- params[:scope] ||= []
= gl_tabs_nav({ class: 'js-snippets-nav-tabs gl-border-b-0 gl-overflow-x-auto gl-grow gl-flex-nowrap' }) do
= gl_tabs_nav({ class: 'js-snippets-nav-tabs gl-border-b-0 gl-grow gl-flex-nowrap' }) do
= gl_tab_link_to subject_snippets_path(subject), { item_active: params[:scope].empty? } do
= _('All')
= gl_tab_counter_badge(include_private ? counts[:total] : counts[:are_public_or_internal])

View File

@ -1,9 +0,0 @@
---
name: reject_orphaned_runners
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516862
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180163
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516929
milestone: '17.9'
group: group::runner
type: gitlab_com_derisk
default_enabled: false

View File

@ -99,6 +99,8 @@ class SyncFkReferencingPCiPipelines < Gitlab::Database::Migration[2.2]
def up
FOREIGN_KEYS.each do |options|
next unless foreign_key_exists?(options[:source_table], name: options[:name])
with_lock_retries do
validate_foreign_key(options[:source_table], options[:column], name: options[:name])
end
@ -106,6 +108,9 @@ class SyncFkReferencingPCiPipelines < Gitlab::Database::Migration[2.2]
end
P_FOREIGN_KEYS.each do |options|
new_name = options[:name].to_s.gsub('_tmp', '')
next if foreign_key_exists?(options[:source_table], NEW_REFERENCING_TABLE, name: new_name)
add_concurrent_partitioned_foreign_key(
options[:source_table], NEW_REFERENCING_TABLE,
**with_defaults(options, validate: true)

View File

@ -197,6 +197,7 @@ The following metrics are available:
| `gitlab_rack_attack_throttle_limit` | Gauge | 17.6 | Reports the maximum number of requests that a client can make before Rack Attack throttles them. | `event_name` |
| `gitlab_rack_attack_throttle_period_seconds` | Gauge | 17.6 | Reports the duration over which requests for a client are counted before Rack Attack throttles them. | `event_name` |
| `gitlab_application_rate_limiter_throttle_utilization_ratio` | Histogram | 17.6 | Utilization ratio of a throttle in GitLab Application Rate Limiter. | `throttle_key`, `peek`, `feature_category` |
| `search_zoekt_task_processing_queue_size` | Gauge | 17.9 | Number of tasks waiting to be processed by Zoekt. | |
## Metrics controlled by a feature flag

View File

@ -19202,7 +19202,7 @@ A user with add-on data.
| <a id="addonusergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="addonusergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="addonuserhuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="addonuserid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="addonuserid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="addonuseride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="addonuserjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="addonuserlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -20234,7 +20234,7 @@ Core representation of a GitLab user.
| <a id="autocompletedusergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="autocompletedusergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="autocompleteduserhuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="autocompleteduserid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="autocompleteduserid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="autocompleteduseride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="autocompleteduserjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="autocompleteduserlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -22914,7 +22914,7 @@ The currently authenticated GitLab user.
| <a id="currentusergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="currentusergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="currentuserhuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="currentuserid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="currentuserid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="currentuseride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="currentuserjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="currentuserlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -29085,7 +29085,7 @@ A user assigned to a merge request.
| <a id="mergerequestassigneegroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestassigneegroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestassigneehuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="mergerequestassigneeid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestassigneeid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="mergerequestassigneeide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestassigneejobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="mergerequestassigneelastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -29495,7 +29495,7 @@ The author of the merge request.
| <a id="mergerequestauthorgroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestauthorgroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestauthorhuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="mergerequestauthorid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestauthorid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="mergerequestauthoride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestauthorjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="mergerequestauthorlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -29956,7 +29956,7 @@ A user participating in a merge request.
| <a id="mergerequestparticipantgroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestparticipantgroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestparticipanthuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="mergerequestparticipantid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestparticipantid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="mergerequestparticipantide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestparticipantjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="mergerequestparticipantlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -30385,7 +30385,7 @@ A user assigned to a merge request as a reviewer.
| <a id="mergerequestreviewergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestreviewergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestreviewerhuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="mergerequestreviewerid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestreviewerid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="mergerequestrevieweride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestreviewerjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="mergerequestreviewerlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -37447,7 +37447,7 @@ Core representation of a GitLab user.
| <a id="usercoregroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="usercoregroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="usercorehuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="usercoreid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="usercoreid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="usercoreide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="usercorejobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="usercorelastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
@ -45390,7 +45390,7 @@ Implementations:
| <a id="usergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="usergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="userhuman"></a>`human` | [`Boolean`](#boolean) | Indicates if the user is a regular user. |
| <a id="userid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="userid"></a>`id` | [`UserID!`](#userid) | Global ID of the user. |
| <a id="useride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="userjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
| <a id="userlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |

View File

@ -17,17 +17,21 @@ Use this API to interact with group access tokens. For more information, see [Gr
Lists all group access tokens for a group.
In GitLab 17.2 and later, you can use the `state` attribute to limit the response to group access tokens with a specified state.
```plaintext
GET /groups/:id/access_tokens
GET /groups/:id/access_tokens?state=inactive
```
| Attribute | Type | required | Description |
| --------- | ----------------- | -------- | ----------- |
| `id` | integer or string | yes | ID or [URL-encoded path](rest/_index.md#namespaced-paths) of a group. |
| `state` | string | No | If defined, only returns tokens with the specified state. Possible values: `active` and `inactive`. |
| Attribute | Type | required | Description |
| ------------------ | ------------------- | -------- | ----------- |
| `id` | integer or string | yes | ID or [URL-encoded path](rest/_index.md#namespaced-paths) of a group. |
| `created_after` | datetime (ISO 8601) | No | If defined, returns tokens created after the specified time. |
| `created_before` | datetime (ISO 8601) | No | If defined, returns tokens created before the specified time. |
| `last_used_after` | datetime (ISO 8601) | No | If defined, returns tokens last used after the specified time. |
| `last_used_before` | datetime (ISO 8601) | No | If defined, returns tokens last used before the specified time. |
| `revoked` | boolean | No | If `true`, only returns revoked tokens. |
| `search` | string | No | If defined, returns tokens that include the specified value in the name. |
| `state` | string | No | If defined, returns tokens with the specified state. Possible values: `active` and `inactive`. |
```shell
curl --request GET \
@ -156,13 +160,13 @@ curl --request POST \
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/403042) in GitLab 16.0
> - `expires_at` attribute [added](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6.
Rotates a group access token. This immediately revokes the previous token and creates a new token. Generally, this endpoint rotates a specific group access token by authenticating with a personal access token. You can also use a group access token to rotate itself. For more information, see [Self-rotation](#self-rotation).
Rotates a group access token. This immediately revokes the previous token and creates a new token. Generally, this endpoint rotates a specific group access token by authenticating with a personal access token. You can also use a group access token to rotate itself. For more information, see [Self-rotate](#self-rotate).
If you attempt to use the revoked token later, GitLab immediately revokes the new token. For more information, see [Automatic reuse detection](personal_access_tokens.md#automatic-reuse-detection).
Prerequisites:
- A personal access token with the [`api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes) or a group access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes). See [Self-rotation](#self-rotation).
- A personal access token with the [`api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes) or a group access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes). See [Self-rotate](#self-rotate).
```plaintext
POST /groups/:id/access_tokens/:token_id/rotate
@ -209,14 +213,14 @@ Other possible responses:
- The token has expired.
- The token was revoked.
- You do not have access to the specified token.
- You're using a group access token to rotate another group access token. See [Self-rotate a project access token](#self-rotation) instead.
- You're using a group access token to rotate another group access token. See [Self-rotate](#self-rotate) instead.
- `403: Forbidden` if the token is not allowed to rotate itself.
- `404: Not Found` if the user is an administrator but the token with the specified ID does not exist.
- `404: Not Found` if the user is an administrator but the token does not exist.
- `405: Method Not Allowed` if the token is not an access token.
### Self-rotation
### Self-rotate
Instead of rotating a specific group access token, you can instead rotate the same group access token you used to authenticate the request. To self-rotate a group access token, you must:
Instead of rotating a specific group access token, you can rotate the same group access token you used to authenticate the request. To self-rotate a group access token, you must:
- Rotate a group access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
- Use the `self` keyword in the request URL.

View File

@ -11,24 +11,13 @@ DETAILS:
Use this API to interact with personal access tokens. For more information, see [Personal access tokens](../user/profile/personal_access_tokens.md).
## List personal access tokens
## List all personal access tokens
> - `created_after`, `created_before`, `last_used_after`, `last_used_before`, `revoked`, `search` and `state` filters were [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362248) in GitLab 15.5.
Get all personal access tokens the authenticated user has access to. By default, returns an unfiltered list of:
- Only personal access tokens created by the current user to a non-administrator.
- All personal access tokens to an administrator.
Administrators:
- Can use the `user_id` parameter to filter by a user.
- Can use other filters on all personal access tokens (GitLab 15.5 and later).
Non-administrators:
- Cannot use the `user_id` parameter to filter on any user except themselves, otherwise they receive a `401 Unauthorized` response.
- Can only filter on their own personal access tokens (GitLab 15.5 and later).
Lists all personal access tokens accessible by the authenticating user. For administrators, returns
a list of all personal access tokens in the instance. For non-administrators, returns a list of the
personal access tokens for the current user.
```plaintext
GET /personal_access_tokens
@ -46,50 +35,21 @@ Supported attributes:
| Attribute | Type | Required | Description |
|---------------------|----------------|----------|---------------------|
| `created_after` | datetime (ISO 8601) | No | Limit results to PATs created after specified time. |
| `created_before` | datetime (ISO 8601) | No | Limit results to PATs created before specified time. |
| `last_used_after` | datetime (ISO 8601) | No | Limit results to PATs last used after specified time. |
| `last_used_before` | datetime (ISO 8601) | No | Limit results to PATs last used before specified time. |
| `revoked` | boolean | No | Limit results to PATs with specified revoked state. Valid values are `true` and `false`. |
| `search` | string | No | Limit results to PATs with name containing search string. |
| `state` | string | No | Limit results to PATs with specified state. Valid values are `active` and `inactive`. |
| `user_id` | integer or string | No | Limit results to PATs owned by specified user. |
| `created_after` | datetime (ISO 8601) | No | If defined, returns tokens created after the specified time. |
| `created_before` | datetime (ISO 8601) | No | If defined, returns tokens created before the specified time. |
| `last_used_after` | datetime (ISO 8601) | No | If defined, returns tokens last used after the specified time. |
| `last_used_before` | datetime (ISO 8601) | No | If defined, returns tokens last used before the specified time. |
| `revoked` | boolean | No | If `true`, only returns revoked tokens. |
| `search` | string | No | If defined, returns tokens that include the specified value in the name. |
| `state` | string | No | If defined, returns tokens with the specified state. Possible values: `active` and `inactive`. |
| `user_id` | integer or string | No | If defined, returns tokens owned by the specified user. Non-administrators can only filter their own tokens. |
Example request:
```shell
curl --request GET \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/personal_access_tokens"
```
Example response:
```json
[
{
"id": 4,
"name": "Test Token",
"revoked": false,
"created_at": "2020-07-23T14:31:47.729Z",
"description": "Test Token description",
"scopes": [
"api"
],
"user_id": 24,
"last_used_at": "2021-10-06T17:58:37.550Z",
"active": true,
"expires_at": null
}
]
```
Example request:
```shell
curl --request GET \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/personal_access_tokens?user_id=3"
--url "https://gitlab.example.com/api/v4/personal_access_tokens?user_id=3&created_before=2022-01-01"
```
Example response:
@ -113,54 +73,19 @@ Example response:
]
```
Example request:
If successful, returns a list of tokens.
```shell
curl --request GET \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/personal_access_tokens?revoked=true"
```
Other possible response:
Example response:
- `401: Unauthorized` if a non-administrator uses the `user_id` attribute to filter for other users.
```json
[
{
"id": 41,
"name": "Revoked Test Token",
"revoked": true,
"created_at": "2022-01-01T14:31:47.729Z",
"description": "Test Token description",
"scopes": [
"api"
],
"user_id": 8,
"last_used_at": "2022-05-18T17:58:37.550Z",
"active": false,
"expires_at": null
}
]
```
You can filter by merged attributes with:
```plaintext
GET /personal_access_tokens?revoked=true&created_before=2022-01-01
```
## Get single personal access token
Get a personal access token by either:
- Using the ID of the personal access token.
- Passing it to the API in a header.
### Using a personal access token ID
## Get details on a personal access token
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362239) in GitLab 15.1.
> - `404` HTTP status code [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93650) in GitLab 15.3.
Get a single personal access token by its ID. Users can get their own tokens.
Administrators can get any token.
Gets details on a specified personal access token. Administrators can get details on any token.
Non-administrators can only get details on own tokens.
```plaintext
GET /personal_access_tokens/:id
@ -168,7 +93,7 @@ GET /personal_access_tokens/:id
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | ID of personal access token |
| `id` | integer or string | yes | ID of a personal access token or the keyword `self`. |
```shell
curl --request GET \
@ -176,24 +101,22 @@ curl --request GET \
--url "https://gitlab.example.com/api/v4/personal_access_tokens/<id>"
```
#### Responses
If successful, returns details on the token.
> - `404` HTTP status code [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93650) in GitLab 15.3.
Other possible responses:
- `401: Unauthorized` if either:
- The user doesn't have access to the token with the specified ID.
- The token with the specified ID doesn't exist.
- `404: Not Found` if the user is an administrator but the token with the specified ID doesn't exist.
- The token does not exist.
- You do not have access to the specified token.
- `404: Not Found` if the user is an administrator but the token does not exist.
### Using a request header
### Self-inform
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/373999) in GitLab 15.5
Get a single personal access token and information about that token by passing the token in a header.
```plaintext
GET /personal_access_tokens/self
```
Instead of getting details on a specific personal access token, you can also return details on
the personal access token you used to authenticate the request. To return these details, you must
use the `self` keyword in the request URL.
```shell
curl --request GET \
@ -201,39 +124,24 @@ curl --request GET \
--url "https://gitlab.example.com/api/v4/personal_access_tokens/self"
```
Example response:
## Create a personal access token
```json
{
"id": 4,
"name": "Test Token",
"revoked": false,
"created_at": "2020-07-23T14:31:47.729Z",
"description": "Test Token description",
"scopes": [
"api"
],
"user_id": 3,
"last_used_at": "2021-10-06T17:58:37.550Z",
"active": true,
"expires_at": null
}
```
DETAILS:
**Offering:** GitLab Self-Managed, GitLab Dedicated
You can create personal access tokens with the user tokens API. For more information, see the following endpoints:
- [Create a personal access token](user_tokens.md#create-a-personal-access-token)
- [Create a personal access token for a user](user_tokens.md#create-a-personal-access-token-for-a-user)
## Rotate a personal access token
Rotate a personal access token. Revokes the previous token and creates a new token that expires in one week.
You can either:
- Use the personal access token ID.
- In GitLab 16.10 and later, pass the personal access token to the API in a request header.
### Use a personal access token ID
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/403042) in GitLab 16.0
> - `expires_at` attribute [added](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6.
In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date is subject to the [maximum allowable lifetime limits](../user/profile/personal_access_tokens.md#access-token-expiration).
Rotates a specified personal access token. This revokes the previous token and creates a new token
that expires after one week. Administrators can revoke tokens for any user. Non-administrators can
only revoke their own tokens.
```plaintext
POST /personal_access_tokens/:id/rotate
@ -241,11 +149,8 @@ POST /personal_access_tokens/:id/rotate
| Attribute | Type | Required | Description |
|-----------|-----------|----------|---------------------|
| `id` | integer/string | yes | ID of personal access token |
| `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6. If undefined, the token expires after one week. |
NOTE:
Non-administrators can rotate their own tokens. Administrators can rotate tokens of any user.
| `id` | integer or string | yes | ID of a personal access token or the keyword `self`. |
| `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). The date must be one year or less from the rotation date. If undefined, the token expires after one week. |
```shell
curl --request POST \
@ -271,28 +176,29 @@ Example response:
}
```
#### Responses
If successful, returns `200: OK`.
Other possible responses:
- `200: OK` if the existing token is successfully revoked and the new token successfully created.
- `400: Bad Request` if not rotated successfully.
- `401: Unauthorized` if either the:
- User does not have access to the token with the specified ID.
- Token with the specified ID does not exist.
- `404: Not Found` if the user is an administrator but the token with the specified ID does not exist.
- `401: Unauthorized` if any of the following conditions are true:
- The token does not exist.
- The token has expired.
- The token was revoked.
- You do not have access to the specified token.
- `403: Forbidden` if the token is not allowed to rotate itself.
- `404: Not Found` if the user is an administrator but the token does not exist.
- `405: Method Not Allowed` if the token is not a personal access token.
### Use a request header
### Self-rotate
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/426779) in GitLab 16.10
Requires:
Instead of rotating a specific personal access token, you can also rotate the same personal access
token you used to authenticate the request. To self-rotate a personal access token, you must:
- `api` or `self_rotate` scope.
In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date is subject to the [maximum allowable lifetime limits](../user/profile/personal_access_tokens.md#access-token-expiration).
```plaintext
POST /personal_access_tokens/self/rotate
```
- Rotate a personal access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
- Use the `self` keyword in the request URL.
```shell
curl --request POST \
@ -300,61 +206,25 @@ curl --request POST \
--url "https://gitlab.example.com/api/v4/personal_access_tokens/self/rotate"
```
Example response:
```json
{
"id": 42,
"name": "Rotated Token",
"revoked": false,
"created_at": "2023-08-01T15:00:00.000Z",
"description": "Test Token description",
"scopes": ["api"],
"user_id": 1337,
"last_used_at": null,
"active": true,
"expires_at": "2023-08-08",
"token": "s3cr3t"
}
```
#### Responses
- `200: OK` if the existing token is successfully revoked and the new token successfully created.
- `400: Bad Request` if not rotated successfully.
- `401: Unauthorized` if either:
- The token does not exist.
- The token has expired.
- The token has been revoked.
- `403: Forbidden` if the token is not allowed to rotate itself.
- `405: Method Not Allowed` if the token is not a personal access token.
### Automatic reuse detection
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/395352) in GitLab 16.3
For each rotated token, the previous and now revoked token is referenced. This
chain of references defines a token family. In a token family, only the latest
token is active, and all other tokens in that family are revoked.
When you rotate or revoke a token, GitLab automatically tracks the relationship between the old and
new tokens. Each time a new token is generated, a connection is made to the previous token. These
connected tokens form a token family. Only the newest token can authenticate requests.
When a revoked token from a token family is used in an authentication attempt
for the token rotation endpoint, that attempt fails and the active token from
the token family gets revoked.
This mechanism helps to prevent compromise when a personal access token is
leaked.
If an old token is ever used to authenticate a request, the request fails and GitLab immediately
revokes the newest token in the family.
Automatic reuse detection is enabled for token rotation API requests.
This feature helps secure GitLab if an old token is ever leaked or stolen. By tracking token
relationships and automatically revoking access when old tokens are used, attackers cannot exploit
compromised tokens.
## Revoke a personal access token
Revoke a personal access token by either:
- Using the ID of the personal access token.
- Passing it to the API in a header.
### Using a personal access token ID
Revoke a personal access token using its ID.
Revokes a specified personal access token. Administrators can revoke tokens for any user.
Non-administrators can only revoke their own tokens.
```plaintext
DELETE /personal_access_tokens/:id
@ -362,10 +232,7 @@ DELETE /personal_access_tokens/:id
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | ID of personal access token |
NOTE:
Non-administrators can revoke their own tokens. Administrators can revoke tokens of any user.
| `id` | integer or string | yes | ID of a personal access token or the keyword `self`. |
```shell
curl --request DELETE \
@ -373,26 +240,22 @@ curl --request DELETE \
--url "https://gitlab.example.com/api/v4/personal_access_tokens/<personal_access_token_id>"
```
#### Responses
If successful, returns `204: No Content`.
Other possible responses:
- `204: No Content` if successfully revoked.
- `400: Bad Request` if not revoked successfully.
- `401: Unauthorized` if the access token is invalid.
- `403: Forbidden` if the access token does not have the required permissions.
### Using a request header
### Self-revoke
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350240) in GitLab 15.0. Limited to tokens with `api` scope.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369103) in GitLab 15.4, any token can use this endpoint.
Revokes a personal access token that is passed in using a request header. Requires:
- `api` scope in GitLab 15.0 to GitLab 15.3.
- Any scope in GitLab 15.4 and later.
```plaintext
DELETE /personal_access_tokens/self
```
Instead of revoking a specific personal access token, you can also revoke the same personal access
token you used to authenticate the request. To self-revoke a personal access token, you must use
the `self` keyword in the request URL.
```shell
curl --request DELETE \
@ -400,17 +263,11 @@ curl --request DELETE \
--url "https://gitlab.example.com/api/v4/personal_access_tokens/self"
```
#### Responses
- `204: No Content` if successfully revoked.
- `400: Bad Request` if not revoked successfully.
- `401: Unauthorized` if the access token is invalid.
## List token associations
## List all token associations
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/466046) in GitLab 17.4.
Returns an unfiltered list of all groups, subgroups, and projects the current authenticated user can access.
Lists all groups, subgroups, and projects associated with the personal access token used to authenticate the request.
```plaintext
GET /personal_access_tokens/self/associations
@ -488,20 +345,6 @@ Example response:
}
```
## Create a personal access token (administrator only)
## Related topics
See the [User tokens API](user_tokens.md#create-a-personal-access-token) for information on creating a personal access
token.
## Create a personal access token with limited scopes for the currently authenticated user
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** GitLab Self-Managed, GitLab Dedicated
See the [User tokens API](user_tokens.md#create-a-personal-access-token) for
information on creating a personal access token for the currently authenticated user.
## Troubleshooting access tokens
To troubleshoot access token issues, see the [token troubleshooting guide](../security/tokens/token_troubleshooting.md).
- [Token troubleshooting](../security/tokens/token_troubleshooting.md)

View File

@ -17,17 +17,21 @@ Use this API to interact with project access tokens. For more information, see [
Lists all project access tokens for a specified project.
In GitLab 17.2 and later, you can use the `state` attribute to limit the response to project access tokens with a specified state.
```plaintext
GET projects/:id/access_tokens
GET projects/:id/access_tokens?state=inactive
```
| Attribute | Type | required | Description |
| --------- | ----------------- | -------- | ----------- |
| `id` | integer or string | yes | ID or [URL-encoded path](rest/_index.md#namespaced-paths) of a project. |
| `state` | string | No | If defined, only returns tokens with the specified state. Possible values: `active` and `inactive`. |
| Attribute | Type | required | Description |
| ------------------ | ------------------- | -------- | ----------- |
| `id` | integer or string | yes | ID or [URL-encoded path](rest/_index.md#namespaced-paths) of a project. |
| `created_after` | datetime (ISO 8601) | No | If defined, returns tokens created after the specified time. |
| `created_before` | datetime (ISO 8601) | No | If defined, returns tokens created before the specified time. |
| `last_used_after` | datetime (ISO 8601) | No | If defined, returns tokens last used after the specified time. |
| `last_used_before` | datetime (ISO 8601) | No | If defined, returns tokens last used before the specified time. |
| `revoked` | boolean | No | If `true`, only returns revoked tokens. |
| `search` | string | No | If defined, returns tokens that include the specified value in the name. |
| `state` | string | No | If defined, returns tokens with the specified state. Possible values: `active` and `inactive`. |
```shell
curl --request GET \
@ -153,13 +157,13 @@ curl --request POST \
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/403042) in GitLab 16.0
> - `expires_at` attribute [added](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6.
Rotates a project access token. This immediately revokes the previous token and creates a new token. Generally, this endpoint rotates a specific project access token by authenticating with a personal access token. You can also use a project access token to rotate itself. For more information, see [Self-rotation](#self-rotation).
Rotates a project access token. This immediately revokes the previous token and creates a new token. Generally, this endpoint rotates a specific project access token by authenticating with a personal access token. You can also use a project access token to rotate itself. For more information, see [Self-rotate](#self-rotate).
If you attempt to use the revoked token later, GitLab immediately revokes the new token. For more information, see [Automatic reuse detection](personal_access_tokens.md#automatic-reuse-detection).
Prerequisites:
- A personal access token with the [`api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes) or a project access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes). See [Self-rotation](#self-rotation).
- A personal access token with the [`api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes) or a project access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes). See [Self-rotate](#self-rotate).
```plaintext
POST /projects/:id/access_tokens/:token_id/rotate
@ -206,14 +210,14 @@ Other possible responses:
- The token has expired.
- The token was revoked.
- You do not have access to the specified token.
- You're using a project access token to rotate another project access token. See [Self-rotate a project access token](#self-rotation) instead.
- You're using a project access token to rotate another project access token. See [Self-rotate](#self-rotate) instead.
- `403: Forbidden` if the token is not allowed to rotate itself.
- `404: Not Found` if the user is an administrator but the token with the specified ID does not exist.
- `404: Not Found` if the user is an administrator but the token does not exist.
- `405: Method Not Allowed` if the token is not a project access token.
### Self-rotation
### Self-rotate
Instead of rotating a specific project access token, you can instead rotate the same project access token you used to authenticate the request. To self-rotate a project access token, you must:
Instead of rotating a specific project access token, you can rotate the same project access token you used to authenticate the request. To self-rotate a project access token, you must:
- Rotate a project access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
- Use the `self` keyword in the request URL.

View File

@ -501,3 +501,22 @@ The record can be deleted once the MR is deployed to all the environments:
```shell
/chatops run feature delete <feature-flag-name> --dev --pre --staging --staging-ref --production
```
## Checking feature flag status
You can use the following ChatOps command to see a feature flag's current state:
```shell
/chatops run feature get <feature-flag-name>
```
Since this is a read-only command, you can avoid cluttering the production channels by either:
- Running it in the `#chatops-ops-test` Slack channel
- Sending it as a direct message to the ChatOps bot
The result of this command will display:
- Whether the feature flag exists
- Its current state (enabled/disabled)
- Any percentage rollouts or actor-based gates that are configured

View File

@ -40,7 +40,7 @@ Security incidents related to credentials exposure can vary in severity from low
- Determine the type and scope of the token.
- Identify the token owner and the relevant team based on the token information.
- For personal access tokens, you might be able to use the [personal access token API](../api/personal_access_tokens.md#using-a-request-header) to quickly retrieve token details.
- For personal access tokens, you can use the [personal access token API](../api/personal_access_tokens.md#get-details-on-a-personal-access-token) to quickly retrieve token details.
- [Revoke](../api/personal_access_tokens.md#revoke-a-personal-access-token) or [rotate](../api/group_access_tokens.md#rotate-a-group-access-token) the token after you have assessed its scope and potential impact. Revoking a production token is a balance between the security risk posed by the exposed token, and the availability risk revoking a token might cause. Only revoke the token if you are:
- Confident in your understanding of the potential impact of token revocation.
- Following your company's security incident response guidelines.

View File

@ -57,7 +57,7 @@ entry:
`meta.auth_fail_token_id` indicates that an access token of ID 12 was used.
To find more information about this token, use the [personal access token API](../../api/personal_access_tokens.md#get-single-personal-access-token).
To find more information about this token, use the [personal access token API](../../api/personal_access_tokens.md#get-details-on-a-personal-access-token).
You can also use the API to [rotate the token](../../api/personal_access_tokens.md#rotate-a-personal-access-token).
### Replace expired access tokens
@ -66,7 +66,7 @@ To replace the token:
1. Check where this token may have been used previously, and remove it from any
automation might still use the token.
- For personal access tokens, use the [API](../../api/personal_access_tokens.md#list-personal-access-tokens)
- For personal access tokens, use the [API](../../api/personal_access_tokens.md#list-all-personal-access-tokens)
to list tokens that have expired recently. For example, go to `https://gitlab.com/api/v4/personal_access_tokens`,
and locate tokens with a specific `expires_at` date.
- For project access tokens, use the

View File

@ -162,7 +162,7 @@ The scope determines the actions you can perform when you authenticate with a gr
| `manage_runner` | Grants permission to manage runners in a group. |
| `ai_features` | Grants permission to perform API actions for GitLab Duo. This scope is designed to work with the GitLab Duo Plugin for JetBrains. For all other extensions, see scope requirements. |
| `k8s_proxy` | Grants permission to perform Kubernetes API calls using the agent for Kubernetes in a group. |
| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../../api/personal_access_tokens.md#use-a-request-header). Does not allow rotation of other tokens. |
| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../../api/personal_access_tokens.md#rotate-a-personal-access-token). Does not allow rotation of other tokens. |
## Restrict the creation of group access tokens

View File

@ -207,7 +207,7 @@ A personal access token can perform actions based on the assigned scopes.
| `manage_runner` | Grants permission to manage runners. |
| `ai_features` | This scope:<br>- Grants permission to perform API actions for features like GitLab Duo, Code Suggestions API and Duo Chat API.<br>- Does not work for GitLab Self-Managed versions 16.5, 16.6, and 16.7.<br>For GitLab Duo plugin for JetBrains, this scope:<br>- Supports users with AI features enabled in the GitLab Duo plugin for JetBrains.<br>- Addresses a security vulnerability in JetBrains IDE plugins that could expose personal access tokens.<br>- Is designed to minimize potential risks for GitLab Duo plugin users by limiting the impact of compromised tokens.<br>For all other extensions, see the individual scope requirements in their documentation. |
| `k8s_proxy` | Grants permission to perform Kubernetes API calls using the agent for Kubernetes. |
| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../api/personal_access_tokens.md#use-a-request-header). Does not allow rotation of other tokens. |
| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../api/personal_access_tokens.md#rotate-a-personal-access-token). Does not allow rotation of other tokens. |
| `read_service_ping`| Grant access to download Service Ping payload through the API when authenticated as an admin use. |
WARNING:

View File

@ -165,8 +165,8 @@ Prerequisites:
To revoke a personal access token, use the [personal access tokens API](../../api/personal_access_tokens.md#revoke-a-personal-access-token). You can use either of the following methods:
- Use a [personal access token ID](../../api/personal_access_tokens.md#using-a-personal-access-token-id-1). The token used to perform the revocation must have the [`admin_mode`](personal_access_tokens.md#personal-access-token-scopes) scope.
- Use a [request header](../../api/personal_access_tokens.md#using-a-request-header-1). The token used to perform the request is revoked.
- Use a [personal access token ID](../../api/personal_access_tokens.md#revoke-a-personal-access-token). The token used to perform the revocation must have the [`admin_mode`](personal_access_tokens.md#personal-access-token-scopes) scope.
- Use a [request header](../../api/personal_access_tokens.md#self-revoke). The token used to perform the request is revoked.
### Delete a service account

View File

@ -116,7 +116,7 @@ See the warning in [create a project access token](#create-a-project-access-toke
| `manage_runner` | Grants permission to manage runners in the project. |
| `ai_features` | Grants permission to perform API actions for GitLab Duo. This scope is designed to work with the GitLab Duo Plugin for JetBrains. For all other extensions, see scope requirements. |
| `k8s_proxy` | Grants permission to perform Kubernetes API calls using the agent for Kubernetes in the project. |
| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../../api/personal_access_tokens.md#use-a-request-header). Does not allow rotation of other tokens. |
| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../../api/personal_access_tokens.md#rotate-a-personal-access-token). Does not allow rotation of other tokens. |
## Restrict the creation of project access tokens

View File

@ -12,9 +12,6 @@ module API
JOB_TOKEN_PARAM = :token
LEGACY_SYSTEM_XID = '<legacy>'
# TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/516929 is closed.
UnknownRunnerOwnerError = Class.new(StandardError)
def authenticate_runner!(ensure_runner_manager: true, update_contacted_at: true)
track_runner_authentication
forbidden! unless current_runner
@ -22,10 +19,7 @@ module API
# TODO: Remove in https://gitlab.com/gitlab-org/gitlab/-/issues/504963 (when ci_runners is swapped)
# This is because the new table will have check constraints for these scenarios, and therefore
# any orphaned runners will be missing
if Feature.enabled?(:reject_orphaned_runners, Feature.current_request) &&
current_runner.sharding_key_id.nil? && !current_runner.instance_type?
forbidden!('Runner is orphaned')
end
forbidden!('Runner is orphaned') if current_runner.sharding_key_id.nil? && !current_runner.instance_type?
current_runner.heartbeat if update_contacted_at
return unless ensure_runner_manager
@ -64,9 +58,6 @@ module API
end
def current_runner_manager
# NOTE: Avoid orphaned runners, since we're not allowed to created records with a nil sharding_key_id
raise UnknownRunnerOwnerError if !current_runner.instance_type? && current_runner.sharding_key_id.nil?
strong_memoize(:current_runner_manager) do
system_xid = params.fetch(:system_id, LEGACY_SYSTEM_XID)
current_runner&.ensure_manager(system_xid)

View File

@ -128,8 +128,6 @@ module API
status 200
present current_runner, with: Entities::Ci::RunnerRegistrationDetails
rescue ::API::Ci::Helpers::Runner::UnknownRunnerOwnerError
unprocessable_entity!('Runner is orphaned')
end
desc 'Reset runner authentication token with current token' do
@ -229,8 +227,6 @@ module API
Gitlab::Metrics.add_event(:build_invalid)
conflict!
end
rescue ::API::Ci::Helpers::Runner::UnknownRunnerOwnerError
unprocessable_entity!('Runner is orphaned')
end
desc 'Update a job' do

View File

@ -19,24 +19,20 @@ module API
expose :contacted_at
expose :maintenance_note
# rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options|
if options[:current_user].can_read_all_resources?
runner.projects
else
options[:current_user].authorized_projects.where(id: runner.runner_projects.pluck(:project_id))
options[:current_user].authorized_projects.id_in(runner.project_ids)
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
expose :groups, with: Entities::BasicGroupDetails do |runner, options|
if options[:current_user].can_read_all_resources?
runner.groups
else
options[:current_user].authorized_groups.where(id: runner.runner_namespaces.pluck(:namespace_id))
options[:current_user].authorized_groups.id_in(runner.namespace_ids)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def latest_runner_manager(runner)
strong_memoize_with(:latest_runner_manager, runner) do

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module Entities
class SshKeyWithUserId < Grape::Entity
expose :id, documentation: { type: 'integer', example: 1 }
expose :title, documentation: { type: 'string', example: 'Sample key 25' }
expose :created_at, documentation: { type: 'dateTime', example: '2015-09-03T07:24:44.627Z' }
expose :expires_at, documentation: { type: 'dateTime', example: '2020-09-03T07:24:44.627Z' }
expose :last_used_at, documentation: { type: 'dateTime', example: '2020-09-03T07:24:44.627Z' }
expose :usage_type, documentation: { type: 'string', example: 'auth' }
expose :user_id, documentation: { type: 'integer', example: 3 }
end
end
end

View File

@ -187,7 +187,7 @@ module Gitlab
::Ci::Runner.find_by_token(token.to_s) || raise(UnauthorizedError)
end
def validate_and_save_access_token!(scopes: [], save_auth_context: true)
def validate_and_save_access_token!(scopes: [], save_auth_context: true, reset_token: false)
# return early if we've already authenticated via a job token
return if @current_authenticated_job.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
@ -196,6 +196,12 @@ module Gitlab
return unless access_token
# Originally, we tried to use `reset` here to follow the rubocop rule introduced by
# gitlab-org/gitlab-foss#60218, but this caused a NoMethodError for OAuth tokens,
# leading to incident 18980 (see gitlab-com/gl-infra/production#18988).
# We're using reload instead and disabling the rubocop rule to prevent similar incidents.
access_token.reload if reset_token # rubocop:disable Cop/ActiveRecordAssociationReload
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
save_auth_failure_in_application_context(access_token, :insufficient_scope, scopes) if save_auth_context

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
namespace :ci do
namespace :job_tokens do
namespace :allowlist do
desc "CI | Job Tokens | Allowlist | Autopopulate allowlist from authorization log entries"
task autopopulate_and_enforce: :environment do
only_ids = ENV['ONLY_PROJECT_IDS']
exclude_ids = ENV['EXCLUDE_PROJECT_IDS']
preview = ENV['PREVIEW']
require_relative '../../../app/models/ci/job_token/allowlist_migration_task'
task = ::Ci::JobToken::AllowlistMigrationTask.new(only_ids: only_ids,
exclude_ids: exclude_ids,
preview: preview,
user: ::Users::Internal.admin_bot)
task.execute
end
end
end
end

View File

@ -800,6 +800,11 @@ msgstr ""
msgid "%{count} total weight"
msgstr ""
msgid "%{count} vulnerability set to %{severity} severity"
msgid_plural "%{count} vulnerabilities set to %{severity} severity"
msgstr[0] ""
msgstr[1] ""
msgid "%{days} days until tags are automatically removed"
msgstr ""
@ -16746,9 +16751,6 @@ msgstr ""
msgid "Could not upload your designs as one or more files uploaded are not supported."
msgstr ""
msgid "Couldn't add issue due to an internal error."
msgstr ""
msgid "Couldn't assign policy to project or group"
msgstr ""
@ -38121,6 +38123,9 @@ msgstr ""
msgid "No phone number data for matching"
msgstr ""
msgid "No placeholder users are awaiting reassignment."
msgstr ""
msgid "No plan"
msgstr ""
@ -63717,6 +63722,9 @@ msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not get user."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability severity."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
msgstr ""
@ -63735,6 +63743,9 @@ msgstr ""
msgid "VulnerabilityManagement|Vulnerability name or type. Ex: Cross-site scripting"
msgstr ""
msgid "VulnerabilityManagement|Vulnerability set to %{severity} severity"
msgstr ""
msgid "VulnerabilityManagement|Will not fix or a false-positive"
msgstr ""

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ApplicationCable::Channel, feature_category: :shared do
let(:user) { create(:user) }
let(:token) { create(:personal_access_token, user: user, scopes: [:api, :read_api]) }
let(:channel) { described_class.new(connection, {}) }
before do
stub_connection current_user: user
end
describe '#validate_token_scope' do
it 'validates the token scope' do
expect(channel).to receive(:validate_and_save_access_token!)
.with(scopes: [:api, :read_api], reset_token: true)
channel.validate_token_scope
end
context 'when an authentication error occurs' do
before do
allow(channel).to receive(:validate_and_save_access_token!)
.and_raise(Gitlab::Auth::AuthenticationError)
end
it 'handles the authentication error' do
expect(channel).to receive(:handle_authentication_error)
channel.validate_token_scope
end
context 'when client is subscribed' do
before do
allow(channel).to receive(:client_subscribed?).and_return(true)
end
it 'unsubscribes from the channel' do
expect(channel).to receive(:unsubscribe_from_channel)
channel.validate_token_scope
end
end
context 'when client is not subscribed' do
before do
allow(channel).to receive(:client_subscribed?).and_return(false)
end
it 'rejects the subscription' do
expect(channel).to receive(:reject)
channel.validate_token_scope
end
end
end
end
end

View File

@ -102,7 +102,7 @@ RSpec.describe UserSettings::PersonalAccessTokensController, feature_category: :
it "builds a PAT with name, description and scopes from params" do
name = 'My PAT'
scopes = 'api,read_user'
scopes = 'api,read_user,invalid'
description = 'My PAT description'
get :index, params: { name: name, scopes: scopes, description: description }

View File

@ -24,7 +24,8 @@ RSpec.describe 'Cluster agent registration', :js, feature_category: :deployment_
end
end
it 'allows the user to select an agent to install, and displays the resulting agent token' do
it 'allows the user to select an agent to install, and displays the resulting agent token',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/510927' do
find_by_testid('clusters-default-action-button').click
expect(page).to have_content('Create and register')

View File

@ -7,6 +7,7 @@ RSpec.describe 'Projects > Files > Project owner creates a license file', :js, f
let_it_be(:project) { create(:project, :repository, namespace: project_maintainer.namespace) }
before do
stub_feature_flags(blob_overflow_menu: false)
project.repository.delete_file(project_maintainer, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
sign_in(project_maintainer)

View File

@ -14,6 +14,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ
let(:user) { create(:user) }
before do
stub_feature_flags(blob_overflow_menu: false)
project.add_developer(user)
sign_in(user)
visit project_tree_path(project, 'master')

View File

@ -18,6 +18,7 @@ RSpec.describe 'Projects > Files > User creates files', :js, feature_category: :
let(:user) { create(:user) }
before do
stub_feature_flags(blob_overflow_menu: false)
project.add_maintainer(user)
sign_in(user)
end

View File

@ -8,6 +8,7 @@ RSpec.describe 'Projects > Files > User uploads files', feature_category: :sourc
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
before do
stub_feature_flags(blob_overflow_menu: false)
project.add_maintainer(user)
sign_in(user)
end

View File

@ -18,6 +18,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
context 'with developer user' do
before_all do
stub_feature_flags(blob_overflow_menu: false)
project.add_developer(user)
end

View File

@ -10,6 +10,7 @@ RSpec.describe 'Projects > Show > User uploads files', feature_category: :groups
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
before do
stub_feature_flags(blob_overflow_menu: false)
project.add_maintainer(user)
sign_in(user)
end

View File

@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlLink } from '@gitlab/ui';
import { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlLink, GlBreadcrumb } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/header_area/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
@ -12,6 +12,7 @@ import projectPathQuery from '~/repository/queries/project_path.query.graphql';
import createApolloProvider from 'helpers/mock_apollo_helper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { logError } from '~/lib/logger';
const defaultMockRoute = {
name: 'blobPath',
@ -20,6 +21,7 @@ const defaultMockRoute = {
const TEST_PROJECT_PATH = 'test-project/path';
Vue.use(VueApollo);
jest.mock('~/lib/logger');
describe('Repository breadcrumbs component', () => {
let wrapper;
@ -88,274 +90,378 @@ describe('Repository breadcrumbs component', () => {
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
const findRouterLinks = () => wrapper.findAllComponents(GlLink);
const findGLBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
beforeEach(() => {
permissionsQuerySpy = jest.fn().mockResolvedValue(createPermissionsQueryResponse());
});
it('queries for permissions', async () => {
factory({ currentPath: '/' });
describe('permissions queries', () => {
it.each`
featureFlag | description
${true} | ${'when blobOverflowMenu feature flag is enabled'}
${false} | ${'when blobOverflowMenu feature flag is disabled'}
`('queries for permissions $description', async ({ featureFlag }) => {
factory({
currentPath: '/',
glFeatures: {
blobOverflowMenu: featureFlag,
},
});
// We need to wait for the projectPath query to resolve
await waitForPromises();
// We need to wait for the projectPath query to resolve
await waitForPromises();
expect(permissionsQuerySpy).toHaveBeenCalledWith({
projectPath: TEST_PROJECT_PATH,
expect(permissionsQuerySpy).toHaveBeenCalledWith({
projectPath: TEST_PROJECT_PATH,
});
});
it.each`
featureFlag | description
${true} | ${'when blobOverflowMenu feature flag is enabled'}
${false} | ${'when blobOverflowMenu feature flag is disabled'}
`('queries for permissions $description', async ({ featureFlag }) => {
const mockError = new Error('timeout of 0ms exceeded');
permissionsQuerySpy = jest.fn().mockRejectedValue(mockError);
factory({
currentPath: '/',
glFeatures: {
blobOverflowMenu: featureFlag,
},
});
// We need to wait for the projectPath query to resolve
await waitForPromises();
expect(logError).toHaveBeenCalledWith(
'Failed to fetch user permissions. See exception details for more information.',
mockError,
);
});
});
it.each`
path | linkCount
${'/'} | ${1}
${'app'} | ${2}
${'app/assets'} | ${3}
${'app/assets/javascripts'} | ${4}
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory({ currentPath: path });
describe('when `blobOverflowMenu` feature flag is enabled', () => {
beforeEach(() => {
factory({
glFeatures: {
blobOverflowMenu: true,
},
});
});
expect(findRouterLinks().length).toEqual(linkCount);
});
it('renders the `gl-breadcrumb` component', () => {
expect(findGLBreadcrumb().exists()).toBe(true);
expect(findGLBreadcrumb().props()).toMatchObject({
items: [
{
path: '/',
text: '',
to: '/-/tree',
},
],
});
});
it.each`
routeName | path | linkTo
${'treePath'} | ${'app/assets/javascripts'} | ${'/-/tree/app/assets/javascripts'}
${'treePathDecoded'} | ${'app/assets/javascripts'} | ${'/-/tree/app/assets/javascripts'}
${'blobPath'} | ${'app/assets/index.js'} | ${'/-/blob/app/assets/index.js'}
${'blobPathDecoded'} | ${'app/assets/index.js'} | ${'/-/blob/app/assets/index.js'}
`(
'links to the correct router path when routeName is $routeName',
({ routeName, path, linkTo }) => {
it.each`
path | linkCount
${'/'} | ${1}
${'app'} | ${2}
${'app/assets'} | ${3}
${'app/assets/javascripts'} | ${4}
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory({
currentPath: path,
mockRoute: {
name: routeName,
glFeatures: {
blobOverflowMenu: true,
},
});
expect(findRouterLinks().at(3).attributes('to')).toEqual(linkTo);
},
);
it('escapes hash in directory path', () => {
factory({ currentPath: 'app/assets/javascripts#' });
expect(findRouterLinks().at(3).attributes('to')).toEqual('/-/tree/app/assets/javascripts%23');
});
it('renders last link as active', () => {
factory({ currentPath: 'app/assets' });
expect(findRouterLinks().at(2).attributes('aria-current')).toEqual('page');
});
it('does not render add to tree dropdown when permissions are false', async () => {
factory({
currentPath: '/',
extraProps: {
canCollaborate: false,
},
expect(findGLBreadcrumb().props('items').length).toEqual(linkCount);
});
await nextTick();
expect(findDropdown().exists()).toBe(false);
});
it.each`
routeName | isRendered
${'blobPath'} | ${false}
${'blobPathDecoded'} | ${false}
${'treePath'} | ${true}
${'treePathDecoded'} | ${true}
${'projectRoot'} | ${true}
`(
'does render add to tree dropdown $isRendered when route is $routeName',
({ routeName, isRendered }) => {
factory({
currentPath: 'app/assets/javascripts.js',
extraProps: {
canCollaborate: true,
canEditTree: true,
},
mockRoute: {
name: routeName,
},
});
expect(findDropdown().exists()).toBe(isRendered);
},
);
it.each`
currentPath | expectedPath | routeName
${'foo'} | ${'foo'} | ${'treePath'}
${'foo/bar'} | ${'foo/bar'} | ${'treePath'}
${'foo/bar/index.js'} | ${'foo/bar'} | ${'blobPath'}
`(
'sets data-current-path to $expectedPath when path is $currentPath and routeName is $routeName',
({ currentPath, expectedPath, routeName }) => {
factory({
currentPath,
mockRoute: {
name: routeName,
},
});
expect(wrapper.attributes('data-current-path')).toBe(expectedPath);
},
);
it('renders add to tree dropdown when permissions are true', async () => {
permissionsQuerySpy.mockResolvedValue(
createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }),
);
factory({
currentPath: '/',
extraProps: {
canCollaborate: true,
canEditTree: true,
},
});
await nextTick();
expect(findDropdown().exists()).toBe(true);
});
describe('copy-to-clipboard icon button', () => {
it.each`
description | flagValue | currentPath | expected
${'does not render button '} | ${false} | ${'/path/to/file'} | ${false}
${'does not render button '} | ${true} | ${''} | ${false}
${'renders button that copies current path'} | ${true} | ${'/path/to/file'} | ${true}
currentPath | expectedPath | routeName
${'foo'} | ${'foo'} | ${'treePath'}
${'foo/bar'} | ${'foo/bar'} | ${'treePath'}
${'foo/bar/index.js'} | ${'foo/bar'} | ${'blobPath'}
`(
'when flag is $flagValue and currentPath is "$currentPath", $description',
({ flagValue, currentPath, expected }) => {
'sets data-current-path to $expectedPath when path is $currentPath and routeName is $routeName',
({ currentPath, expectedPath, routeName }) => {
factory({
currentPath,
mockRoute: {
name: routeName,
},
glFeatures: {
blobOverflowMenu: flagValue,
blobOverflowMenu: true,
},
});
const btn = wrapper.findComponent(ClipboardButton);
expect(btn.exists()).toBe(expected);
if (expected) {
expect(btn.vm.text).toBe(currentPath);
}
expect(findGLBreadcrumb().attributes('data-current-path')).toBe(expectedPath);
},
);
describe('copy-to-clipboard icon button', () => {
it.each`
description | currentPath | expected
${'does not render button when path empty'} | ${''} | ${false}
${'renders button that copies current path'} | ${'/path/to/file'} | ${true}
`(
'when feature flag is enabled and currentPath is "$currentPath", $description',
({ currentPath, expected }) => {
factory({
currentPath,
glFeatures: {
blobOverflowMenu: true,
},
});
expect(findClipboardButton().exists()).toBe(expected);
if (expected) {
expect(findClipboardButton().vm.text).toBe(currentPath);
}
},
);
});
});
describe('renders the upload blob modal', () => {
beforeEach(() => {
describe('when `blobOverflowMenu` feature flag is disabled', () => {
it.each`
path | linkCount
${'/'} | ${1}
${'app'} | ${2}
${'app/assets'} | ${3}
${'app/assets/javascripts'} | ${4}
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory({ currentPath: path });
expect(findRouterLinks().length).toEqual(linkCount);
});
it.each`
routeName | path | linkTo
${'treePath'} | ${'app/assets/javascripts'} | ${'/-/tree/app/assets/javascripts'}
${'treePathDecoded'} | ${'app/assets/javascripts'} | ${'/-/tree/app/assets/javascripts'}
${'blobPath'} | ${'app/assets/index.js'} | ${'/-/blob/app/assets/index.js'}
${'blobPathDecoded'} | ${'app/assets/index.js'} | ${'/-/blob/app/assets/index.js'}
`(
'links to the correct router path when routeName is $routeName',
({ routeName, path, linkTo }) => {
factory({
currentPath: path,
mockRoute: {
name: routeName,
},
});
expect(findRouterLinks().at(3).attributes('to')).toEqual(linkTo);
},
);
it('escapes hash in directory path', () => {
factory({ currentPath: 'app/assets/javascripts#' });
expect(findRouterLinks().at(3).attributes('to')).toEqual('/-/tree/app/assets/javascripts%23');
});
it('renders last link as active', () => {
factory({ currentPath: 'app/assets' });
expect(findRouterLinks().at(2).attributes('aria-current')).toEqual('page');
});
it('does not render add to tree dropdown when permissions are false', async () => {
factory({
currentPath: '/',
extraProps: {
canEditTree: true,
canCollaborate: false,
},
});
await nextTick();
expect(findDropdown().exists()).toBe(false);
});
it('does not render the modal while loading', () => {
expect(findUploadBlobModal().exists()).toBe(false);
});
it.each`
routeName | isRendered
${'blobPath'} | ${false}
${'blobPathDecoded'} | ${false}
${'treePath'} | ${true}
${'treePathDecoded'} | ${true}
${'projectRoot'} | ${true}
`(
'does render add to tree dropdown $isRendered when route is $routeName',
({ routeName, isRendered }) => {
factory({
currentPath: 'app/assets/javascripts.js',
extraProps: {
canCollaborate: true,
canEditTree: true,
},
mockRoute: {
name: routeName,
},
});
expect(findDropdown().exists()).toBe(isRendered);
},
);
it('renders the modal once loaded', async () => {
await waitForPromises();
it.each`
currentPath | expectedPath | routeName
${'foo'} | ${'foo'} | ${'treePath'}
${'foo/bar'} | ${'foo/bar'} | ${'treePath'}
${'foo/bar/index.js'} | ${'foo/bar'} | ${'blobPath'}
`(
'sets data-current-path to $expectedPath when path is $currentPath and routeName is $routeName',
({ currentPath, expectedPath, routeName }) => {
factory({
currentPath,
mockRoute: {
name: routeName,
},
});
expect(findUploadBlobModal().exists()).toBe(true);
expect(findUploadBlobModal().props()).toStrictEqual({
canPushCode: false,
canPushToBranch: false,
commitMessage: 'Upload New File',
emptyRepo: false,
modalId: 'modal-upload-blob',
originalBranch: '',
path: '',
replacePath: null,
targetBranch: '',
});
});
});
expect(wrapper.attributes('data-current-path')).toBe(expectedPath);
},
);
describe('renders the new directory modal', () => {
beforeEach(() => {
factory({
currentPath: 'some_dir',
extraProps: {
canEditTree: true,
newDirPath: 'root/master',
},
});
});
it('does not render the modal while loading', () => {
expect(findNewDirectoryModal().exists()).toBe(false);
});
it('renders the modal once loaded', async () => {
await waitForPromises();
expect(findNewDirectoryModal().exists()).toBe(true);
expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir');
});
});
describe('"this repository" dropdown group', () => {
it('renders when user has pushCode permissions', async () => {
it('renders add to tree dropdown when permissions are true', async () => {
permissionsQuerySpy.mockResolvedValue(
createPermissionsQueryResponse({
pushCode: true,
}),
createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }),
);
factory({
currentPath: '/',
extraProps: {
canCollaborate: true,
canEditTree: true,
},
});
await waitForPromises();
await nextTick();
expect(findDropdownGroup().props('group').name).toBe('This repository');
expect(findDropdown().exists()).toBe(true);
});
it('does not render when user does not have pushCode permissions', async () => {
permissionsQuerySpy.mockResolvedValue(
createPermissionsQueryResponse({
pushCode: false,
}),
describe('copy-to-clipboard icon button', () => {
it.each`
description | flagValue | currentPath | expected
${'does not render button '} | ${false} | ${'/path/to/file'} | ${false}
${'does not render button '} | ${true} | ${''} | ${false}
${'renders button that copies current path'} | ${true} | ${'/path/to/file'} | ${true}
`(
'when flag is $flagValue and currentPath is "$currentPath", $description',
({ flagValue, currentPath, expected }) => {
factory({
currentPath,
glFeatures: {
blobOverflowMenu: flagValue,
},
});
expect(findClipboardButton().exists()).toBe(expected);
if (expected) {
expect(findClipboardButton().vm.text).toBe(currentPath);
}
},
);
factory({
currentPath: '/',
extraProps: {
canCollaborate: true,
},
});
await waitForPromises();
expect(findDropdownGroup().exists()).toBe(false);
});
});
describe('link rendering', () => {
it('passes `href` to GlLink when isBlobView is true', () => {
factory({
currentPath: '/',
extraProps: {
isBlobView: true,
},
});
expect(findRouterLinks().at(0).attributes('href')).toBe('/test-project/path/-/tree');
});
it('passes `to` to GlLink when isBlobView is false', () => {
factory({
currentPath: '/',
extraProps: {
isBlobView: false,
},
describe('renders the upload blob modal', () => {
beforeEach(() => {
factory({
currentPath: '/',
extraProps: {
canEditTree: true,
},
});
});
expect(findRouterLinks().at(0).attributes('to')).toBe('/-/tree');
it('does not render the modal while loading', () => {
expect(findUploadBlobModal().exists()).toBe(false);
});
it('renders the modal once loaded', async () => {
await waitForPromises();
expect(findUploadBlobModal().exists()).toBe(true);
expect(findUploadBlobModal().props()).toStrictEqual({
canPushCode: false,
canPushToBranch: false,
commitMessage: 'Upload New File',
emptyRepo: false,
modalId: 'modal-upload-blob',
originalBranch: '',
path: '',
replacePath: null,
targetBranch: '',
});
});
});
describe('renders the new directory modal', () => {
beforeEach(() => {
factory({
currentPath: 'some_dir',
extraProps: {
canEditTree: true,
newDirPath: 'root/master',
},
});
});
it('does not render the modal while loading', () => {
expect(findNewDirectoryModal().exists()).toBe(false);
});
it('renders the modal once loaded', async () => {
await waitForPromises();
expect(findNewDirectoryModal().exists()).toBe(true);
expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir');
});
});
describe('"this repository" dropdown group', () => {
it('renders when user has pushCode permissions', async () => {
permissionsQuerySpy.mockResolvedValue(
createPermissionsQueryResponse({
pushCode: true,
}),
);
factory({
currentPath: '/',
extraProps: {
canCollaborate: true,
},
});
await waitForPromises();
expect(findDropdownGroup().props('group').name).toBe('This repository');
});
it('does not render when user does not have pushCode permissions', async () => {
permissionsQuerySpy.mockResolvedValue(
createPermissionsQueryResponse({
pushCode: false,
}),
);
factory({
currentPath: '/',
extraProps: {
canCollaborate: true,
},
});
await waitForPromises();
expect(findDropdownGroup().exists()).toBe(false);
});
});
it('does not render copy to clipboard button', () => {
factory({
currentPath: '/path/to/file',
});
expect(findClipboardButton().exists()).toBe(false);
});
});
});

View File

@ -20,14 +20,7 @@ describe('GlobalSearch MergeRequestsFilters', () => {
hasMissingProjectContext: () => true,
};
const createComponent = (
initialState = {},
provide = {
glFeatures: {
searchMrFilterSourceBranch: true,
},
},
) => {
const createComponent = (initialState = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@ -42,7 +35,6 @@ describe('GlobalSearch MergeRequestsFilters', () => {
wrapper = shallowMount(MergeRequestsFilters, {
store,
provide,
});
};
@ -104,16 +96,6 @@ describe('GlobalSearch MergeRequestsFilters', () => {
});
});
describe('When feature flag search_mr_filter_source_branch is disabled', () => {
beforeEach(() => {
createComponent(null, { glFeatures: { searchMrFilterSourceBranch: false } });
});
it(`will not render SourceBranchFilter`, () => {
expect(findSourceBranchFilter().exists()).toBe(false);
});
});
describe('#hasMissingProjectContext getter', () => {
beforeEach(() => {
defaultGetters.hasMissingProjectContext = () => false;

View File

@ -142,6 +142,21 @@ describe('MrWidgetPipelineContainer', () => {
expect(wrapper.findComponent(MrWidgetPipeline).props().sourceBranchLink).toBe('Foo');
});
it('sanitizes the targetBranch output', () => {
createComponent({
props: {
isPostMerge: true,
mr: {
...mockStore,
targetBranch:
"x<i/class='js-unsanitized-code'/data-context-commits-path=/$PROJECT_PATH/-/raw/main/data.json>",
},
},
});
expect(wrapper.find('.js-unsanitized-code').exists()).toBe(false);
});
it('renders deployments', () => {
const expectedProps = mockStore.postMergeDeployments.map((dep) =>
expect.objectContaining({

View File

@ -402,7 +402,7 @@ export const mockStore = {
},
},
flags: {},
ref: {},
ref: { branch: '1' },
},
mergePipeline: {
id: 1,
@ -422,7 +422,7 @@ export const mockStore = {
},
},
flags: {},
ref: {},
ref: { branch: '1' },
},
pipelineEtag: '/etag',
pipelineIid: '12',

View File

@ -167,42 +167,6 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do
expect(current_runner_manager.sharding_key_id).to eq(runner.sharding_key_id)
end
end
context 'when project runner is missing sharding_key_id' do
let(:runner) { Ci::Runner.project_type.last }
let(:connection) { Ci::Runner.connection }
before do
connection.execute(<<~SQL)
ALTER TABLE ci_runners DISABLE TRIGGER ALL;
INSERT INTO ci_runners(created_at, runner_type, token, sharding_key_id) VALUES(NOW(), 3, 'foo', NULL);
ALTER TABLE ci_runners ENABLE TRIGGER ALL;
SQL
end
it 'fails to create a new runner manager', :aggregate_failures do
allow(helper).to receive(:params).and_return(token: runner.token, system_id: 'new_system_id')
expect(helper.current_runner).to eq(runner)
expect { current_runner_manager }.to raise_error described_class::UnknownRunnerOwnerError
end
# TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/516929 is closed.
context 'with reject_orphaned_runners FF disabled' do
before do
stub_feature_flags(reject_orphaned_runners: false)
end
it 'fails to create a new runner manager', :aggregate_failures do
allow(helper).to receive(:params).and_return(token: runner.token, system_id: 'new_system_id')
expect(helper.current_runner).to eq(runner)
expect { current_runner_manager }.to raise_error described_class::UnknownRunnerOwnerError
end
end
end
end
describe '#track_runner_authentication', :prometheus, feature_category: :runner do

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::SshKeyWithUserId, feature_category: :system_access do
describe '#as_json' do
subject { entity.as_json }
let(:key) { create(:key, user: create(:user)) }
let(:entity) { described_class.new(key) }
it 'includes basic fields', :aggregate_failures do
is_expected.to include(
id: key.id,
title: key.title,
created_at: key.created_at,
expires_at: key.expires_at,
usage_type: 'auth_and_signing',
last_used_at: key.last_used_at,
user_id: key.user.id
)
end
end
end

View File

@ -1095,6 +1095,88 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
end
end
describe '#validate_and_save_access_token!' do
context 'when token is a personal access token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
end
context 'when reset_token is true' do
it 'reloads the access token before validation' do
expect(personal_access_token).to receive(:reload)
validate_and_save_access_token!(reset_token: true)
end
it 'uses the reloaded token for validation' do
allow(personal_access_token).to receive(:reload) do
personal_access_token.expires_at = 1.day.ago
end
expect { validate_and_save_access_token!(reset_token: true) }.to raise_error(Gitlab::Auth::ExpiredError)
end
end
context 'when reset_token is false' do
it 'does not reload the access token before validation' do
expect(personal_access_token).not_to receive(:reload)
validate_and_save_access_token!(reset_token: false)
end
end
context 'when reset_token is not specified' do
it 'does not reload the access token before validation' do
expect(personal_access_token).not_to receive(:reload)
validate_and_save_access_token!
end
end
end
context 'when token is a OAuth access token' do
let!(:oauth_access_token) { create(:oauth_access_token, resource_owner: user) }
before do
allow_any_instance_of(described_class).to receive(:access_token).and_return(oauth_access_token)
end
context 'when reset_token is true' do
it 'reloads the access token before validation' do
expect(oauth_access_token).to receive(:reload)
validate_and_save_access_token!(reset_token: true)
end
it 'uses the reloaded token for validation' do
allow(oauth_access_token).to receive(:reload) do
allow(oauth_access_token).to receive(:expired?).and_return(true)
end
expect { validate_and_save_access_token!(reset_token: true) }.to raise_error(Gitlab::Auth::ExpiredError)
end
end
context 'when reset_token is false' do
it 'does not reload the access token before validation' do
expect(oauth_access_token).not_to receive(:reload)
validate_and_save_access_token!(reset_token: false)
end
end
context 'when reset_token is not specified' do
it 'does not reload the access token before validation' do
expect(oauth_access_token).not_to receive(:reload)
validate_and_save_access_token!
end
end
end
end
describe '#find_user_from_job_token' do
let(:token) { job.token }

View File

@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
RSpec.describe SyncFkReferencingPCiPipelines, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:all_tmp_fk_names) do
let!(:all_tmp_fk_names) do
(described_class::FOREIGN_KEYS + described_class::P_FOREIGN_KEYS).pluck(:name)
end
@ -27,6 +27,27 @@ RSpec.describe SyncFkReferencingPCiPipelines, migration: :gitlab_ci, feature_cat
end
end
it 'is idempotent' do
original_foreign_keys = described_class::FOREIGN_KEYS
original_p_foreign_keys = described_class::P_FOREIGN_KEYS
stub_const("#{described_class}::FOREIGN_KEYS", original_foreign_keys + original_foreign_keys)
stub_const("#{described_class}::P_FOREIGN_KEYS", original_p_foreign_keys + original_p_foreign_keys)
reversible_migration do |migration|
migration.before -> {
expect(fk_count_for(:p_ci_pipelines, all_tmp_fk_names, is_valid: false)).to eq(16)
expect(fk_count_for(:p_ci_pipelines, all_fk_names)).to eq(0)
expect(fk_count_for(:ci_pipelines, all_fk_names)).to eq(16)
}
migration.after -> {
expect(fk_count_for(:p_ci_pipelines, all_tmp_fk_names, is_valid: false)).to eq(0)
expect(fk_count_for(:p_ci_pipelines, all_fk_names)).to eq(16)
expect(fk_count_for(:ci_pipelines, all_fk_names)).to eq(0)
}
end
end
private
def fk_count_for(referenced_table, names, is_valid: true)

View File

@ -0,0 +1,166 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::JobToken::AllowlistMigrationTask, :silence_stdout, feature_category: :secrets_management do
let(:only_ids) { nil }
let(:exclude_ids) { nil }
let(:preview) { nil }
let(:output_stream) { StringIO.new }
let(:path) { Rails.root.join('tmp/tests/doc/ci/jobs') }
let(:user) { ::Users::Internal.admin_bot }
let(:task) do
described_class.new(only_ids: only_ids, exclude_ids: exclude_ids, preview: preview, user: user,
output_stream: output_stream)
end
let_it_be(:origin_project) { create(:project) }
let_it_be(:accessed_project1) { create(:project) }
let_it_be(:accessed_project2) { create(:project) }
let_it_be(:accessed_project3) { create(:project) }
let(:accessed_projects) do
[
accessed_project1.reload,
accessed_project2.reload,
accessed_project3.reload
]
end
before do
accessed_projects.each do |accessed_project|
create(:ci_job_token_authorization, origin_project: origin_project, accessed_project: accessed_project,
last_authorized_at: 1.day.ago)
accessed_projects.map { |p| p.ci_cd_settings.update!(inbound_job_token_scope_enabled: false) }
end
end
describe '#execute' do
context "when preview mode is enabled" do
let(:preview) { "1" }
it 'does not call unsafe_execute!' do
expect_any_instance_of(::Ci::JobToken::AutopopulateAllowlistService).not_to receive(:unsafe_execute!) # rubocop:disable RSpec/AnyInstanceOf -- not the next instance
task.execute
end
it 'logs the expected messages' do
messages = []
messages << task.send(:preview_banner)
messages << "\n\nMigrating project(s) in preview mode...\n"
accessed_projects.each do |accessed_project|
messages << "\nWould have migrated project id: #{accessed_project.id}."
end
messages << "\n\nMigration complete in preview mode.\n\n"
task.execute
expect(output_stream.string).to eq(messages.join)
end
end
context "when preview mode is disabled" do
it 'calls unsafe_execute!' do
service = instance_double(::Ci::JobToken::AutopopulateAllowlistService)
allow(::Ci::JobToken::AutopopulateAllowlistService).to receive(:new).and_return(service)
expect(service).to receive(:unsafe_execute!).exactly(3).times
task.execute
end
it 'logs the expected messages' do
messages = []
messages << "\nMigrating project(s)...\n"
accessed_projects.each do |accessed_project|
messages << "\nMigrated project id: #{accessed_project.id}."
end
messages << "\n\nMigration complete.\n\n"
task.execute
expect(output_stream.string).to eq(messages.join)
end
context "when an exception is raised" do
let(:project) { create(:project) }
let(:only_ids) { project.id.to_s }
it 'logs the error' do
exception = Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError
message = "Error migrating project id: #{project.id}, error: #{exception}\n"
expect_next_instance_of(::Ci::JobToken::AutopopulateAllowlistService) do |instance|
expect(instance).to receive(:unsafe_execute!).and_raise(exception)
end
task.execute
expect(output_stream.string).to include(message)
end
end
end
context "when exclude_ids is supplied" do
let(:exclude_ids) { "#{accessed_project1.id}, #{accessed_project2.id}" }
it 'migrates all projects expect those excluded by exclude_ids' do
expect(task).not_to receive(:migrate_project).with(accessed_project1)
expect(task).not_to receive(:migrate_project).with(accessed_project2)
expect(task).to receive(:migrate_project).with(accessed_project3)
task.execute
end
context 'when too many exclude_ids are supplied' do
let(:exclude_ids) { (1..1001).to_a.join(",") }
it 'displays an error' do
task.execute
expect(output_stream.string).to include(
"ONLY_PROJECT_IDS and EXCLUDE_PROJECT_IDS must contain less than 1000 items, try again"
)
end
end
end
context "when only_ids is supplied" do
let(:only_ids) { "#{accessed_project1.id}, #{accessed_project2.id}" }
it 'only migrates projects listed in only_ids' do
Project.find([accessed_project1.id, accessed_project2.id]).each do |project|
expect(task).to receive(:migrate_project).with(project)
end
task.execute
end
context 'when too many only_ids are supplied' do
let(:only_ids) { (1..1001).to_a.join(",") }
it 'displays an error' do
task.execute
expect(output_stream.string).to include(
"ONLY_PROJECT_IDS and EXCLUDE_PROJECT_IDS must contain less than 1000 items, try again"
)
end
end
end
context "when both only_ids and exclude_ids are supplied" do
let(:exclude_ids) { accessed_project1.id.to_s }
let(:only_ids) { accessed_project2.id.to_s }
it 'displays an error' do
task.execute
expect(output_stream.string).to include(
"ONLY_PROJECT_IDS and EXCLUDE_PROJECT_IDS cannot both be set, try again."
)
end
end
end
end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets_management do
let_it_be(:accessed_project) { create(:project) }
let(:excluded_namespace_paths) { [] }
let(:compactor) { described_class.new(accessed_project.id) }
let(:compactor) { described_class.new(accessed_project) }
# [1, 21], ns1, p1
# [1, 2, 3], ns1, ns2, p2
@ -61,7 +61,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
it 'raises when the limit cannot be achieved' do
expect do
compactor.compact(1)
end.to raise_error(Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError)
end.to raise_error(described_class::Error, "CompactionLimitCannotBeAchievedError")
end
it 'raises when an unexpected compaction entry is found' do
@ -70,7 +70,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
original_response << [1, 2, 3]
end
expect { compactor.compact(5) }.to raise_error(Gitlab::Utils::TraversalIdCompactor::UnexpectedCompactionEntry)
expect { compactor.compact(5) }.to raise_error(described_class::Error, "UnexpectedCompactionEntry")
end
it 'raises when a redundant compaction entry is found' do
@ -80,7 +80,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
original_response << (last_item.size > 1 ? last_item[0...-1] : last_item)
end
expect { compactor.compact(5) }.to raise_error(Gitlab::Utils::TraversalIdCompactor::RedundantCompactionEntry)
expect { compactor.compact(5) }.to raise_error(described_class::Error, "RedundantCompactionEntry")
end
context 'with three top-level namespaces' do
@ -104,7 +104,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
it 'raises when the limit cannot be achieved' do
expect do
compactor.compact(2)
end.to raise_error(Gitlab::Utils::TraversalIdCompactor::CompactionLimitCannotBeAchievedError)
end.to raise_error(described_class::Error, "CompactionLimitCannotBeAchievedError")
end
it 'does not raise when the limit cannot be achieved' do
@ -128,13 +128,13 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
end
end
describe 'when a multiple groups exist' do
describe 'when multiple groups exist' do
before do
create(:ci_job_token_group_scope_link, source_project: accessed_project, target_group: ns6)
create(:ci_job_token_group_scope_link, source_project: accessed_project, target_group: ns2)
end
it 'removes it from the compaction process' do
it 'removes them from the compaction process' do
compactor.compact(4)
expect(compactor.allowlist_groups).to match_array([ns4])
@ -159,7 +159,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
end
end
describe 'when a multiple projects exist' do
describe 'when multiple projects exist' do
before do
create(:ci_job_token_project_scope_link, source_project: accessed_project, direction: :inbound,
target_project: pns8.project)
@ -167,7 +167,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
target_project: pns2.project)
end
it 'removes it from the compaction process' do
it 'removes them from the compaction process' do
compactor.compact(6)
expect(compactor.allowlist_groups).to match_array([ns2, ns4])
@ -183,7 +183,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
target_project: pns8.project)
end
it 'removes it from the compaction process' do
it 'removes them from the compaction process' do
compactor.compact(4)
expect(compactor.allowlist_groups).to match_array([ns4])
@ -196,7 +196,7 @@ RSpec.describe Ci::JobToken::AuthorizationsCompactor, feature_category: :secrets
accessed_project_control = create(:project)
create(:ci_job_token_authorization, origin_project: pns1.project, accessed_project: accessed_project_control,
last_authorized_at: 1.day.ago)
compactor_control = described_class.new(accessed_project_control.id)
compactor_control = described_class.new(accessed_project_control)
control = ActiveRecord::QueryRecorder.new do
compactor_control.origin_project_traversal_ids
end

View File

@ -545,8 +545,7 @@ RSpec.describe Ci::Runner, type: :model, factory_default: :keep, feature_categor
expect(assign_to).to be_truthy
expect(runner).to be_project_type
expect(runner.runner_projects.pluck(:project_id))
.to contain_exactly(project.id, owner_project.id, fallback_owner_project.id)
expect(runner.project_ids).to contain_exactly(project.id, owner_project.id, fallback_owner_project.id)
end
it 'does not change sharding_key_id or owner' do

View File

@ -10,7 +10,38 @@ RSpec.describe Ci::RunnerTagging, feature_category: :runner do
describe 'validations' do
it { is_expected.to validate_presence_of(:runner_type) }
it { is_expected.to validate_presence_of(:sharding_key_id) }
describe 'sharding_key_id' do
subject(:runner_tagging) { runner.taggings.first }
context 'when runner_type is instance_type' do
let(:runner) { create(:ci_runner, :instance, tag_list: ['postgres']) }
it { is_expected.to be_valid }
context 'and sharding_key_id is not nil' do
before do
runner_tagging.sharding_key_id = group.id
end
it { is_expected.to be_invalid }
end
end
context 'when runner_type is group_type' do
let(:runner) { create(:ci_runner, :group, groups: [group], tag_list: ['postgres']) }
it { is_expected.to be_valid }
context 'and sharding_key_id is nil' do
before do
runner_tagging.sharding_key_id = nil
end
it { is_expected.to be_invalid }
end
end
end
end
describe 'partitioning' do

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Integrations::Instance::Pivotaltracker, feature_category: :integrations do
it_behaves_like Integrations::Base::Pivotaltracker
end

View File

@ -3,104 +3,5 @@
require 'spec_helper'
RSpec.describe Integrations::Pivotaltracker, feature_category: :integrations do
include StubRequests
describe 'Validations' do
context 'when integration is active' do
before do
subject.active = true
end
it { is_expected.to validate_presence_of(:token) }
end
context 'when integration is inactive' do
before do
subject.active = false
end
it { is_expected.not_to validate_presence_of(:token) }
end
end
describe 'Execute' do
let(:integration) do
described_class.new.tap do |integration|
integration.token = 'secret_api_token'
end
end
let(:url) { described_class::API_ENDPOINT }
def push_data(branch: 'master')
{
object_kind: 'push',
ref: "refs/heads/#{branch}",
commits: [
{
id: '21c12ea',
author: {
name: 'Some User'
},
url: 'https://example.com/commit',
message: 'commit message'
}
]
}
end
before do
stub_full_request(url, method: :post)
end
it 'posts correct message' do
integration.execute(push_data)
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
body: {
'source_commit' => {
'commit_id' => '21c12ea',
'author' => 'Some User',
'url' => 'https://example.com/commit',
'message' => 'commit message'
}
},
headers: {
'Content-Type' => 'application/json',
'X-TrackerToken' => 'secret_api_token'
}
).once
end
context 'when allowed branches is specified' do
let(:integration) do
super().tap do |integration|
integration.restrict_to_branch = 'master,v10'
end
end
it 'posts message if branch is in the list' do
integration.execute(push_data(branch: 'master'))
integration.execute(push_data(branch: 'v10'))
expect(WebMock).to have_requested(:post, stubbed_hostname(url)).twice
end
it 'does not post message if branch is not in the list' do
integration.execute(push_data(branch: 'mas'))
integration.execute(push_data(branch: 'v11'))
expect(WebMock).not_to have_requested(:post, stubbed_hostname(url))
end
end
end
describe '#avatar_url' do
it 'returns the avatar image path' do
expect(subject.avatar_url).to eq(
ActionController::Base.helpers.image_path(
'illustrations/third-party-logos/integrations-logos/pivotal-tracker.svg'
)
)
end
end
it_behaves_like Integrations::Base::Pivotaltracker
end

View File

@ -937,4 +937,17 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
end
end
context 'with incident issue type' do
let_it_be(:project) { create(:project, group: group, guests: guest, planners: planner, reporters: reporter, owners: owner) }
let_it_be(:incident) { create(:issue, :incident, project: project) }
it 'allows accessing an incident' do
expect(permissions(guest, incident)).to be_disallowed(:update_issue, :admin_issue)
expect(permissions(planner, incident)).to be_disallowed(:update_issue, :admin_issue, :destroy_issue)
expect(permissions(reporter, incident)).to be_allowed(:update_issue, :admin_issue)
expect(permissions(reporter, incident)).to be_disallowed(:destroy_issue)
expect(permissions(owner, incident)).to be_allowed(:update_issue, :admin_issue, :destroy_issue)
end
end
end

View File

@ -38,6 +38,8 @@ RSpec.describe WorkItemPolicy, :aggregate_failures, feature_category: :team_plan
let(:authored_project_confidential_work_item) { create(:work_item, confidential: true, project: private_project, author: guest_author) }
let(:not_persisted_project_work_item) { build(:work_item, project: private_project) }
let_it_be(:incident_work_item) { create(:work_item, :incident, project: private_project) }
it_behaves_like 'checks abilities for project level work items'
it 'checks non-member abilities' do
@ -70,6 +72,8 @@ RSpec.describe WorkItemPolicy, :aggregate_failures, feature_category: :team_plan
let(:authored_project_confidential_work_item) { create(:work_item, confidential: true, project: private_project, author: guest_author) }
let(:not_persisted_project_work_item) { build(:work_item, project: public_project) }
let_it_be(:incident_work_item) { create(:work_item, :incident, project: public_project) }
it_behaves_like 'checks abilities for project level work items'
it 'checks non-member abilities' do

View File

@ -203,19 +203,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq({ message: '403 Forbidden - Runner is orphaned' }.to_json)
end
# TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/516929 is closed.
context 'with reject_orphaned_runners FF disabled' do
before do
stub_feature_flags(reject_orphaned_runners: false)
end
it 'returns unprocessable entity status code', :aggregate_failures do
expect { request }.not_to change { Ci::RunnerManager.count }.from(0)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(response.body).to eq({ message: 'Runner is orphaned' }.to_json)
end
end
end
private

View File

@ -123,21 +123,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq({ message: '403 Forbidden - Runner is orphaned' }.to_json)
end
# TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/516929 is closed.
context 'with reject_orphaned_runners FF disabled' do
before do
stub_feature_flags(reject_orphaned_runners: false)
end
it 'does not update contacted_at and returns error', :aggregate_failures do
expect { verify }.not_to change { partitioned_runner_exists?(non_partitioned_runner) }.from(false)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(response.body).to eq({ message: 'Runner is orphaned' }.to_json)
expect(non_partitioned_runner.contacted_at).to be_nil
end
end
end
private

Some files were not shown because too many files have changed in this diff Show More