Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
45e2205d7c
commit
357b66a2be
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.-->
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
44
CHANGELOG.md
44
CHANGELOG.md
|
|
@ -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)
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class ProjectCiCdSetting < ApplicationRecord
|
||||
include ChronicDurationAttribute
|
||||
include EachBatch
|
||||
|
||||
belongs_to :project, inverse_of: :ci_cd_settings
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue