diff options
67 files changed, 1772 insertions, 478 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index dd47acd0b7d..fcb853a7bd2 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1467,6 +1467,12 @@ changes: ["vendor/gems/ipynbdiff/**/*"] - <<: *if-merge-request-labels-run-all-rspec +.vendor:rules:omniauth-azure-oauth2: + rules: + - <<: *if-merge-request + changes: ["vendor/gems/omniauth-azure-oauth2/**/*"] + - <<: *if-merge-request-labels-run-all-rspec + .vendor:rules:omniauth-cas3: rules: - <<: *if-merge-request diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml index 275faa389e2..4408a6e4624 100644 --- a/.gitlab/ci/vendored-gems.gitlab-ci.yml +++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml @@ -14,6 +14,14 @@ vendor ipynbdiff: include: vendor/gems/ipynbdiff/.gitlab-ci.yml strategy: depend +vendor omniauth-azure-oauth2: + extends: + - .vendor:rules:omniauth-azure-oauth2 + needs: [] + trigger: + include: vendor/gems/omniauth-azure-oauth2/.gitlab-ci.yml + strategy: depend + vendor omniauth-cas3: extends: - .vendor:rules:omniauth-cas3 diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index 0f440235339..4a98bcea706 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -2,27 +2,6 @@ # Cop supports --auto-correct. Layout/HashAlignment: Exclude: - - 'ee/app/graphql/types/vulnerable_dependency_type.rb' - - 'ee/app/graphql/types/vulnerable_kubernetes_resource_type.rb' - - 'ee/app/graphql/types/vulnerable_projects_by_grade_type.rb' - - 'ee/app/graphql/types/work_items/widgets/verification_status_type.rb' - - 'ee/app/graphql/types/work_items/widgets/weight_type.rb' - - 'ee/app/helpers/ee/geo_helper.rb' - - 'ee/app/serializers/ee/evidences/release_entity.rb' - - 'ee/app/services/ci/external_pull_requests/process_github_event_service.rb' - - 'ee/app/services/ci_cd/setup_project.rb' - - 'ee/app/services/ee/issues/base_service.rb' - - 'ee/app/services/elastic/cluster_reindexing_service.rb' - - 'ee/app/services/elastic/process_bookkeeping_service.rb' - - 'ee/app/services/epics/epic_links/list_service.rb' - - 'ee/app/services/epics/issue_promote_service.rb' - - 'ee/app/services/groups/memberships/export_service.rb' - - 'ee/app/services/projects/setup_ci_cd.rb' - - 'ee/app/services/security/security_orchestration_policies/on_demand_scan_pipeline_configuration_service.rb' - - 'ee/config/routes/project.rb' - - 'ee/config/routes/uploads.rb' - - 'ee/elastic/migrate/20220118150500_delete_orphaned_commits.rb' - - 'ee/elastic/migrate/20220119120500_populate_commit_permissions_in_main_index.rb' - 'ee/lib/api/iterations.rb' - 'ee/lib/api/merge_trains.rb' - 'ee/lib/api/related_epic_links.rb' @@ -39,7 +39,7 @@ gem 'ruby-saml', '~> 1.13.0' gem 'omniauth', '~> 1.8' gem 'omniauth-auth0', '~> 2.0.0' gem 'omniauth-azure-activedirectory-v2', '~> 1.0' -gem 'omniauth-azure-oauth2', '~> 0.0.9' # Deprecated v1 version +gem 'omniauth-azure-oauth2', '~> 0.0.9' # See vendor/gems/omniauth-azure-oauth2/README.md gem 'omniauth-cas3', '~> 1.1.4', path: 'vendor/gems/omniauth-cas3' # See vendor/gems/omniauth-cas3/README.md gem 'omniauth-dingtalk-oauth2', '~> 1.0' gem 'omniauth-alicloud', '~> 1.0.1' diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 9f5dbe70331..e9809ac661b 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -79,9 +79,6 @@ export default { size: numberToHumanSize(this.size), }); }, - showJobLogSearch() { - return this.glFeatures.jobLogSearch; - }, showJumpToFailures() { return this.glFeatures.jobLogJumpToFailures; }, @@ -198,24 +195,22 @@ export default { <!-- eo truncate information --> <div class="controllers"> - <template v-if="showJobLogSearch"> - <gl-search-box-by-click - v-model="searchTerm" - class="gl-mr-3" - :placeholder="$options.i18n.searchPlaceholder" - data-testid="job-log-search-box" - @clear="$emit('searchResults', [])" - @submit="searchJobLog" - /> + <gl-search-box-by-click + v-model="searchTerm" + class="gl-mr-3" + :placeholder="$options.i18n.searchPlaceholder" + data-testid="job-log-search-box" + @clear="$emit('searchResults', [])" + @submit="searchJobLog" + /> - <help-popover class="gl-mr-3"> - <template #title>{{ $options.i18n.searchPopoverTitle }}</template> + <help-popover class="gl-mr-3"> + <template #title>{{ $options.i18n.searchPopoverTitle }}</template> - <p class="gl-mb-0"> - {{ $options.i18n.searchPopoverDescription }} - </p> - </help-popover> - </template> + <p class="gl-mb-0"> + {{ $options.i18n.searchPopoverDescription }} + </p> + </help-popover> <!-- links --> <gl-button diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 9f4e12a3010..48be8af3ff6 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -263,10 +263,12 @@ export function insertMarkdownText({ if (tag === LINK_TAG_PATTERN) { if (URL) { try { - new URL(selected); // eslint-disable-line no-new - // valid url - tag = '[text]({text})'; - select = 'text'; + const url = new URL(selected); + + if (url.origin !== 'null' || url.origin === null) { + tag = '[text]({text})'; + select = 'text'; + } } catch (e) { // ignore - no valid url } diff --git a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue index a4ef0befb91..83fcab4b343 100644 --- a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue +++ b/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue @@ -11,6 +11,8 @@ import { GlSprintf, } from '@gitlab/ui'; import { s__, __ } from '~/locale'; +import Tracking from '~/tracking'; +import { pipelineEditorTrackingOptions } from '../../constants'; import ValidatePipelinePopover from '../popovers/validate_pipeline_popover.vue'; import CiLintResults from '../lint/ci_lint_results.vue'; import getBlobContent from '../../graphql/queries/blob_content.query.graphql'; @@ -70,6 +72,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [Tracking.mixin()], inject: ['ciConfigPath', 'ciLintPath', 'projectFullPath', 'validateTabIllustrationPath'], props: { ciFileContent: { @@ -110,6 +113,9 @@ export default { }; }, computed: { + canResimulatePipeline() { + return this.hasSimulationResults && this.hasCiContentChanged; + }, isInitialCiContentLoading() { return this.$apollo.queries.initialBlobContent.loading; }, @@ -128,6 +134,10 @@ export default { variant: this.isValid ? 'success' : 'danger', }; }, + trackingAction() { + const { actions } = pipelineEditorTrackingOptions; + return this.canResimulatePipeline ? actions.resimulatePipeline : actions.simulatePipeline; + }, }, watch: { ciFileContent(value) { @@ -139,7 +149,12 @@ export default { cancelSimulation() { this.state = VALIDATE_TAB_INIT; }, + trackSimulation() { + const { label } = pipelineEditorTrackingOptions; + this.track(this.trackingAction, { label }); + }, async validateYaml() { + this.trackSimulation(); this.state = VALIDATE_TAB_LOADING; try { @@ -198,7 +213,7 @@ export default { :aria-label="$options.i18n.help" /> </div> - <div v-if="hasSimulationResults && hasCiContentChanged"> + <div v-if="canResimulatePipeline"> <span class="gl-text-gray-400" data-testid="content-status"> {{ $options.i18n.contentChange }} </span> diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js index 4b07045e638..dd25c4d433b 100644 --- a/app/assets/javascripts/pipeline_editor/constants.js +++ b/app/assets/javascripts/pipeline_editor/constants.js @@ -75,6 +75,8 @@ export const pipelineEditorTrackingOptions = { [CI_YAML_LINK]: 'visit_help_drawer_link_yaml', }, openHelpDrawer: 'open_help_drawer', + resimulatePipeline: 'resimulate_pipeline', + simulatePipeline: 'simulate_pipeline', }, }; diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb index 82fff287c4a..f3283c88740 100644 --- a/app/controllers/projects/alerting/notifications_controller.rb +++ b/app/controllers/projects/alerting/notifications_controller.rb @@ -13,8 +13,6 @@ module Projects prepend_before_action :repository, :project_without_auth feature_category :incident_management - # Goal is to increase the urgency to medium. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/361310. urgency :low, [:create] def create diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 8570bbf1dbd..7878ace5015 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -18,7 +18,6 @@ class Projects::JobsController < Projects::ApplicationController before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize before_action :verify_proxy_request!, only: :proxy_websocket_authorize - before_action :push_job_log_search, only: [:show] before_action :push_job_log_jump_to_failures, only: [:show] before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase] @@ -250,10 +249,6 @@ class Projects::JobsController < Projects::ApplicationController ::Gitlab::Workhorse.channel_websocket(service) end - def push_job_log_search - push_frontend_feature_flag(:job_log_search, @project) - end - def push_job_log_jump_to_failures push_frontend_feature_flag(:job_log_jump_to_failures, @project) end diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 647464b31f8..f7a02c521f5 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -11,14 +11,14 @@ .labels-container.gl-mt-5 - if can_admin_label && search.blank? %p.text-muted - = _('Labels can be applied to issues and merge requests.') - %br - = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') + = _('Labels can be applied to issues and merge requests. Star a label to make it a priority label.') -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels.gl-mb-7{ class: [('hide' if hide), ('is-not-draggable' unless can_admin_label)] } %h4.gl-mt-3= _('Prioritized Labels') + %p.text-muted + = _('Drag to reorder prioritized labels and change their relative priority.') .manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } } #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" } = render 'shared/empty_states/priority_labels' diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 252f9c26f06..c351ea29c7c 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -3,11 +3,11 @@ - show_label_issues_link = subject_or_group_defined && show_label_issuables_link?(label, :issues) - show_label_merge_requests_link = subject_or_group_defined && show_label_issuables_link?(label, :merge_requests) -.label-name.gl-flex-shrink-0.gl-mt-2.gl-mr-3 +.label-name.gl-flex-shrink-0.gl-mt-2.gl-mr-5 = render_label(label, tooltip: false) .label-description.gl-overflow-hidden.gl-w-full .gl-display-flex.gl-align-items-stretch.gl-flex-wrap.gl-mt-2 - .gl-flex-basis-half.gl-flex-grow-1.gl-overflow-hidden.gl-mr-2 + .gl-flex-basis-half.gl-flex-grow-1.gl-overflow-hidden.gl-mr-5 - if label.description.present? = markdown_field(label, :description) - elsif show_labels_full_path?(@project, @group) diff --git a/config/feature_flags/development/job_log_search.yml b/config/feature_flags/development/job_log_search.yml deleted file mode 100644 index b6f1cec26f6..00000000000 --- a/config/feature_flags/development/job_log_search.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: job_log_search -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91293 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366455 -milestone: '15.2' -type: development -group: group::pipeline execution -default_enabled: false diff --git a/db/post_migrate/20220616171355_update_vulnerabilities_project_id_id_active_cis_index.rb b/db/post_migrate/20220616171355_update_vulnerabilities_project_id_id_active_cis_index.rb new file mode 100644 index 00000000000..047ae0d1132 --- /dev/null +++ b/db/post_migrate/20220616171355_update_vulnerabilities_project_id_id_active_cis_index.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class UpdateVulnerabilitiesProjectIdIdActiveCisIndex < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + NEW_INDEX_NAME = 'idx_vulnerabilities_on_project_id_and_id_active_cis_dft_branch' + OLD_INDEX_NAME = 'index_vulnerabilities_on_project_id_and_id_active_cis' + OLD_INDEX_FILTER_CONDITION = 'report_type = 7 AND state = ANY(ARRAY[1, 4])' + NEW_INDEX_FILTER_CONDITION = 'report_type = 7 AND state = ANY(ARRAY[1, 4]) AND present_on_default_branch IS TRUE' + + def up + add_concurrent_index :vulnerabilities, [:project_id, :id], + where: NEW_INDEX_FILTER_CONDITION, + name: NEW_INDEX_NAME + + remove_concurrent_index_by_name(:vulnerabilities, OLD_INDEX_NAME) + end + + def down + add_concurrent_index :vulnerabilities, [:project_id, :id], where: OLD_INDEX_FILTER_CONDITION, name: OLD_INDEX_NAME + + remove_concurrent_index_by_name(:vulnerabilities, NEW_INDEX_NAME) + end +end diff --git a/db/schema_migrations/20220616171355 b/db/schema_migrations/20220616171355 new file mode 100644 index 00000000000..cd212025f70 --- /dev/null +++ b/db/schema_migrations/20220616171355 @@ -0,0 +1 @@ +63ec85b4f8b7eb15c232c4a25c1e63027c38c23caf81a89c4d05227a6be00e4b
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b055d831ce6..aff91f735e7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -27472,6 +27472,8 @@ CREATE UNIQUE INDEX idx_vuln_signatures_on_occurrences_id_and_signature_sha ON v CREATE UNIQUE INDEX idx_vuln_signatures_uniqueness_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, algorithm_type, signature_sha); +CREATE INDEX idx_vulnerabilities_on_project_id_and_id_active_cis_dft_branch ON vulnerabilities USING btree (project_id, id) WHERE ((report_type = 7) AND (state = ANY (ARRAY[1, 4])) AND (present_on_default_branch IS TRUE)); + CREATE INDEX idx_vulnerabilities_partial_devops_adoption_and_default_branch ON vulnerabilities USING btree (project_id, created_at, present_on_default_branch) WHERE (state <> 1); CREATE UNIQUE INDEX idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue ON vulnerability_external_issue_links USING btree (vulnerability_id, external_type, external_project_key, external_issue_key); @@ -30364,8 +30366,6 @@ CREATE INDEX index_vulnerabilities_on_last_edited_by_id ON vulnerabilities USING CREATE INDEX index_vulnerabilities_on_milestone_id ON vulnerabilities USING btree (milestone_id); -CREATE INDEX index_vulnerabilities_on_project_id_and_id_active_cis ON vulnerabilities USING btree (project_id, id) WHERE ((report_type = 7) AND (state = ANY (ARRAY[1, 4]))); - CREATE INDEX index_vulnerabilities_on_project_id_and_state_and_severity ON vulnerabilities USING btree (project_id, state, severity); CREATE INDEX index_vulnerabilities_on_resolved_by_id ON vulnerabilities USING btree (resolved_by_id); diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 0aa766aa0d4..92504c226fb 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -152,6 +152,7 @@ From there, you can see the following actions: - Added, removed, or updated protected branches - Release was added to a project - Release was updated +- Release was deleted ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94793/) in GitLab 13.5) - Release milestone associations changed - Permission to approve merge requests by committers was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9) - Permission to approve merge requests by committers was updated. diff --git a/doc/api/packages.md b/doc/api/packages.md index 9d9c21546cd..3cd93bd09c1 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -6,6 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Packages API **(FREE)** +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349418) support for [GitLab CI/CD job token](../ci/jobs/ci_job_token.md) authentication for the project-level API in GitLab 15.3. + This is the API documentation of [GitLab Packages](../administration/packages/index.md). ## List packages diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index 9567bc9cd65..93f22da648a 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -13,6 +13,7 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints - Packages: - [Package Registry](../../user/packages/package_registry/index.md#use-gitlab-cicd-to-build-packages). + - [Packages API](../../api/packages.md) (project-level). - [Container Registry](../../user/packages/container_registry/index.md#build-and-push-by-using-gitlab-cicd) (the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`). - [Container Registry API](../../api/container_registry.md) diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md index e2a1751f279..0d5357e63ad 100644 --- a/doc/ci/jobs/job_control.md +++ b/doc/ci/jobs/job_control.md @@ -567,6 +567,9 @@ In blocking manual jobs: enabled can't be merged with a blocked pipeline. - The pipeline shows a status of **blocked**. +When using manual jobs in triggered pipelines with [`strategy: depend`](../yaml/index.md#triggerstrategy), +the type of manual job can affect the trigger job's status while the pipeline runs. + ### Run a manual job To run a manual job, you must have permission to merge to the assigned branch: diff --git a/doc/ci/pipelines/multi_project_pipelines.md b/doc/ci/pipelines/multi_project_pipelines.md index 81c0639817f..a71af78f410 100644 --- a/doc/ci/pipelines/multi_project_pipelines.md +++ b/doc/ci/pipelines/multi_project_pipelines.md @@ -341,8 +341,8 @@ If you use [`only/except`](../yaml/index.md#only--except) to control job behavio > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in GitLab Premium 12.3. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8. -You can mirror the pipeline status from the triggered pipeline to the source -trigger job by using `strategy: depend`. For example: +You can mirror the pipeline status from the triggered pipeline to the source trigger job +by using [`strategy: depend`](../yaml/index.md#triggerstrategy). For example: ```yaml trigger_job: diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 466cfc5e6ba..0b434e7013c 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3966,6 +3966,17 @@ trigger_job: In this example, jobs from subsequent stages wait for the triggered pipeline to successfully complete before starting. +**Additional details**: + +- [Optional manual jobs](../jobs/job_control.md#types-of-manual-jobs) in the downstream pipeline + do not affect the status of the downstream pipeline or the upstream trigger job. + The downstream pipeline can complete successfully without running any optional manual jobs. +- [Blocking manual jobs](../jobs/job_control.md#types-of-manual-jobs) in the downstream pipeline + must run before the trigger job is marked as successful or failed. The trigger job + shows **pending** (**{status_pending}**) if the downstream pipeline status is + **waiting for manual action** (**{status_manual}**) due to manual jobs. By default, + jobs in later stages do not start until the trigger job completes. + #### `trigger:forward` > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213729) in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `ci_trigger_forward_variables`. Disabled by default. diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md index 361a14be75c..118d375da01 100644 --- a/doc/user/admin_area/settings/visibility_and_access_controls.md +++ b/doc/user/admin_area/settings/visibility_and_access_controls.md @@ -53,6 +53,7 @@ By default both administrators and anyone with the **Owner** role can delete a p > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2 for groups created after August 12, 2021. > - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/352960) from default delayed project deletion in GitLab 15.1. > - [Enabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89466) in GitLab 15.1. +> - [Disabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95495) in GitLab 15.3. Instance-level protection against accidental deletion of groups and projects. diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 5a3a7e13205..b973a0f56d1 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -447,9 +447,10 @@ in GitLab 12.6, and then to [immediate deletion](https://gitlab.com/gitlab-org/g ### Delayed project deletion **(PREMIUM)** -> [Enabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89466) in GitLab 15.1. +> - [Enabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89466) in GitLab 15.1. +> - [Disabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95495) in GitLab 15.3. -Projects can be deleted after a delay period. Multiple settings can affect whether +Projects in a group (not a personal namespace) can be deleted after a delay period. Multiple settings can affect whether delayed project deletion is enabled for a particular project: - Self-managed instance [settings](../../admin_area/settings/visibility_and_access_controls.md#delayed-project-deletion). diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index 21b2b17d234..800966408fc 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -36,6 +36,7 @@ module API optional :status, type: String, values: Packages::Package.statuses.keys, desc: 'Return packages with specified status' end + route_setting :authentication, job_token_allowed: true get ':id/packages' do packages = ::Packages::PackagesFinder.new( user_project, @@ -52,6 +53,7 @@ module API params do requires :package_id, type: Integer, desc: 'The ID of a package' end + route_setting :authentication, job_token_allowed: true get ':id/packages/:package_id' do package = ::Packages::PackageFinder .new(user_project, params[:package_id]).execute @@ -65,6 +67,7 @@ module API params do requires :package_id, type: Integer, desc: 'The ID of a package' end + route_setting :authentication, job_token_allowed: true delete ':id/packages/:package_id' do authorize_destroy_package!(user_project) diff --git a/lib/api/releases.rb b/lib/api/releases.rb index d7e497bddf3..10e879ec70b 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -186,6 +186,8 @@ module API .execute if result[:status] == :success + log_release_deleted_audit_event + present result[:release], with: Entities::Release, current_user: current_user else render_api_error!(result[:message], result[:http_status]) @@ -238,6 +240,10 @@ module API # extended in EE end + def log_release_deleted_audit_event + # extended in EE + end + def log_release_milestones_updated_audit_event # extended in EE end diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb index 78794f524f4..975da8662e1 100644 --- a/lib/gitlab/ci/config/entry/processable.rb +++ b/lib/gitlab/ci/config/entry/processable.rb @@ -120,7 +120,7 @@ module Gitlab stage: stage_value, extends: extends, rules: rules_value, - job_variables: variables_value.to_h, + job_variables: variables_entry.value_with_data, root_variables_inheritance: root_variables_inheritance, only: only_value, except: except_value, diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index efb469ee32a..3130aec0446 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -18,7 +18,9 @@ module Gitlab end def value - @config.to_h { |key, value| [key.to_s, expand_value(value)[:value]] } + @config.to_h do |key, data| + [key.to_s, expand_data(data)[:value]] + end end def self.default(**) @@ -26,7 +28,9 @@ module Gitlab end def value_with_data - @config.to_h { |key, value| [key.to_s, expand_value(value)] } + @config.to_h do |key, data| + [key.to_s, expand_data(data)] + end end def use_value_data? @@ -35,11 +39,11 @@ module Gitlab private - def expand_value(value) - if value.is_a?(Hash) - { value: value[:value].to_s, description: value[:description] } + def expand_data(data) + if data.is_a?(Hash) + { value: data[:value].to_s, description: data[:description] }.compact else - { value: value.to_s, description: nil } + { value: data.to_s } end end end diff --git a/lib/gitlab/ci/variables/helpers.rb b/lib/gitlab/ci/variables/helpers.rb index 7cc727bb3ea..300b2708e6d 100644 --- a/lib/gitlab/ci/variables/helpers.rb +++ b/lib/gitlab/ci/variables/helpers.rb @@ -6,24 +6,22 @@ module Gitlab module Helpers class << self def merge_variables(current_vars, new_vars) - current_vars = transform_from_yaml_variables(current_vars) - new_vars = transform_from_yaml_variables(new_vars) + return current_vars if new_vars.blank? - transform_to_yaml_variables( - current_vars.merge(new_vars) - ) - end + current_vars = transform_to_array(current_vars) if current_vars.is_a?(Hash) + new_vars = transform_to_array(new_vars) if new_vars.is_a?(Hash) - def transform_to_yaml_variables(vars) - vars.to_h.map do |key, value| - { key: key.to_s, value: value, public: true } - end + (new_vars + current_vars).uniq { |var| var[:key] } end - def transform_from_yaml_variables(vars) - return vars.stringify_keys.transform_values(&:to_s) if vars.is_a?(Hash) - - vars.to_a.to_h { |var| [var[:key].to_s, var[:value]] } + def transform_to_array(vars) + vars.to_h.map do |key, data| + if data.is_a?(Hash) + { key: key.to_s, **data.except(:key) } + else + { key: key.to_s, value: data } + end + end end def inherit_yaml_variables(from:, to:, inheritance:) @@ -35,7 +33,7 @@ module Gitlab def apply_inheritance(variables, inheritance) case inheritance when true then variables - when false then {} + when false then [] when Array then variables.select { |var| inheritance.include?(var[:key]) } end end diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb index 4bd1ac3b67f..f203f88442d 100644 --- a/lib/gitlab/ci/yaml_processor/result.rb +++ b/lib/gitlab/ci/yaml_processor/result.rb @@ -43,7 +43,7 @@ module Gitlab end def root_variables - @root_variables ||= transform_to_yaml_variables(variables) + @root_variables ||= transform_to_array(variables) end def jobs @@ -70,7 +70,7 @@ module Gitlab environment: job[:environment_name], coverage_regex: job[:coverage], # yaml_variables is calculated with using job_variables in Seed::Build - job_variables: transform_to_yaml_variables(job[:job_variables]), + job_variables: transform_to_array(job[:job_variables]), root_variables_inheritance: job[:root_variables_inheritance], needs_attributes: job.dig(:needs, :job), interruptible: job[:interruptible], @@ -114,7 +114,7 @@ module Gitlab Gitlab::Ci::Variables::Helpers.inherit_yaml_variables( from: root_variables, - to: transform_to_yaml_variables(job[:job_variables]), + to: job[:job_variables], inheritance: job.fetch(:root_variables_inheritance, true) ) end @@ -137,8 +137,8 @@ module Gitlab job[:release] end - def transform_to_yaml_variables(variables) - ::Gitlab::Ci::Variables::Helpers.transform_to_yaml_variables(variables) + def transform_to_array(variables) + ::Gitlab::Ci::Variables::Helpers.transform_to_array(variables) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8e7d31fd88e..b86d02d5627 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13929,6 +13929,9 @@ msgstr "" msgid "Draft: %{filename}" msgstr "" +msgid "Drag to reorder prioritized labels and change their relative priority." +msgstr "" + msgid "Drag your designs here or %{linkStart}click to upload%{linkEnd}." msgstr "" @@ -22949,7 +22952,7 @@ msgstr "" msgid "Labels can be applied to issues and merge requests to categorize them." msgstr "" -msgid "Labels can be applied to issues and merge requests." +msgid "Labels can be applied to issues and merge requests. Star a label to make it a priority label." msgstr "" msgid "Labels with no issues in this iteration:" @@ -37306,9 +37309,6 @@ msgstr "" msgid "Standard" msgstr "" -msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging." -msgstr "" - msgid "Star labels to start sorting by priority" msgstr "" diff --git a/package.json b/package.json index a601c5804fc..ecd30dd2560 100644 --- a/package.json +++ b/package.json @@ -205,7 +205,6 @@ "@types/jest": "^27.5.1", "@vue/test-utils": "1.3.0", "@vue/vue2-jest": "^27.0.0", - "acorn": "^6.3.0", "ajv": "^8.10.0", "ajv-formats": "^2.1.1", "axios-mock-adapter": "^1.15.0", diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index c8b22bb3125..0ae4ef18649 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -1,111 +1,164 @@ # frozen_string_literal: true -require "spec_helper" +require 'spec_helper' -RSpec.describe "User creates a merge request", :js do +RSpec.describe 'User creates a merge request', :js do include ProjectForksHelper - let_it_be(:project) { create(:project, :repository) } - let_it_be(:user) { create(:user) } - - let(:title) { "Some feature" } - - before do - project.add_maintainer(user) - sign_in(user) - end - - it "creates a merge request" do - visit(project_new_merge_request_path(project)) + shared_examples 'creates a merge request' do + specify do + visit(project_new_merge_request_path(project)) - find(".js-source-branch").click - click_link("fix") + compare_source_and_target('fix', 'feature') - find(".js-target-branch").click - click_link("feature") + page.within('.merge-request-form') do + expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.' + end - click_button("Compare branches") + fill_in('Title', with: title) + click_button('Create merge request') - page.within('.merge-request-form') do - expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.' + page.within('.merge-request') do + expect(page).to have_content(title) + end end + end - fill_in("Title", with: title) - click_button("Create merge request") + shared_examples 'renders not found' do + specify do + visit project_new_merge_request_path(project) - page.within(".merge-request") do - expect(page).to have_content(title) + expect(page).to have_title('Not Found') + expect(page).to have_content('Page Not Found') end end - context "XSS branch name exists" do + context 'when user is a direct project member' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:title) { 'Some feature' } + before do - project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", "master") + project.add_maintainer(user) + sign_in(user) end - it "doesn't execute the dodgy branch name" do - visit(project_new_merge_request_path(project)) + it_behaves_like 'creates a merge request' - find(".js-source-branch").click - click_link("<img/src='x'/onerror=alert('oops')>") + context 'with XSS branch name' do + before do + project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", 'master') + end - find(".js-target-branch").click - click_link("feature") + it 'does not execute the suspicious branch name' do + visit(project_new_merge_request_path(project)) - click_button("Compare branches") + compare_source_and_target("<img/src='x'/onerror=alert('oops')>", 'feature') - expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError) + expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError) + end end - end - context "to a forked project" do - let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) } + context 'to a forked project' do + let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) } + + it 'creates a merge request', :sidekiq_might_not_need_inline do + visit(project_new_merge_request_path(forked_project)) + + expect(page).to have_content('Source branch').and have_content('Target branch') + expect(find('#merge_request_target_project_id', visible: false).value).to eq(project.id.to_s) - it "creates a merge request", :sidekiq_might_not_need_inline do - visit(project_new_merge_request_path(forked_project)) + click_button('Compare branches and continue') - expect(page).to have_content("Source branch").and have_content("Target branch") - expect(find("#merge_request_target_project_id", visible: false).value).to eq(project.id.to_s) + expect(page).to have_content('You must select source and target branch') - click_button("Compare branches and continue") + first('.js-source-project').click + first('.dropdown-source-project a', text: forked_project.full_path) - expect(page).to have_content("You must select source and target branch") + first('.js-target-project').click + first('.dropdown-target-project a', text: project.full_path) - first(".js-source-project").click - first(".dropdown-source-project a", text: forked_project.full_path) + first('.js-source-branch').click - first(".js-target-project").click - first(".dropdown-target-project a", text: project.full_path) + wait_for_requests - first(".js-source-branch").click + source_branch = 'fix' - wait_for_requests + first('.js-source-branch-dropdown .dropdown-content a', text: source_branch).click - source_branch = "fix" + click_button('Compare branches and continue') - first(".js-source-branch-dropdown .dropdown-content a", text: source_branch).click + expect(page).to have_text _('New merge request') - click_button("Compare branches and continue") + page.within('form#new_merge_request') do + fill_in('Title', with: title) + end - expect(page).to have_text _('New merge request') + expect(find('.js-assignee-search')['data-project-id']).to eq(project.id.to_s) + find('.js-assignee-search').click - page.within("form#new_merge_request") do - fill_in("Title", with: title) + page.within('.dropdown-menu-user') do + expect(page).to have_content('Unassigned') + .and have_content(user.name) + .and have_content(project.users.first.name) + end + find('.js-assignee-search').click + + click_button('Create merge request') + + expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master") + end + end + end + + context 'when user is an inherited member from the group' do + let_it_be(:group) { create(:group, :public) } + + let(:user) { create(:user) } + + context 'when project is public and merge requests are private' do + let_it_be(:project) do + create(:project, + :public, + :repository, + group: group, + merge_requests_access_level: ProjectFeature::DISABLED) end - expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s) - find('.js-assignee-search').click + context 'and user is a guest' do + before do + group.add_guest(user) + sign_in(user) + end - page.within(".dropdown-menu-user") do - expect(page).to have_content("Unassigned") - .and have_content(user.name) - .and have_content(project.users.first.name) + it_behaves_like 'renders not found' end - find('.js-assignee-search').click + end + + context 'when project is private' do + let_it_be(:project) { create(:project, :private, :repository, group: group) } - click_button("Create merge request") + context 'and user is a guest' do + before do + group.add_guest(user) + sign_in(user) + end - expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master") + it_behaves_like 'renders not found' + end end end + + private + + def compare_source_and_target(source_branch, target_branch) + find('.js-source-branch').click + click_link(source_branch) + + find('.js-target-branch').click + click_link(target_branch) + + click_button('Compare branches') + end end diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js index 4075abd24d5..aa85253a177 100644 --- a/spec/frontend/jobs/components/job_log_controllers_spec.js +++ b/spec/frontend/jobs/components/job_log_controllers_spec.js @@ -34,7 +34,7 @@ describe('Job log controllers', () => { jobLog: mockJobLog, }; - const createWrapper = (props, { jobLogSearch = false, jobLogJumpToFailures = false } = {}) => { + const createWrapper = (props, { jobLogJumpToFailures = false } = {}) => { wrapper = mount(JobLogControllers, { propsData: { ...defaultProps, @@ -42,7 +42,6 @@ describe('Job log controllers', () => { }, provide: { glFeatures: { - jobLogSearch, jobLogJumpToFailures, }, }, @@ -290,38 +289,27 @@ describe('Job log controllers', () => { }); describe('Job log search', () => { - describe('with feature flag off', () => { - it('does not display job log search', () => { - createWrapper(); - - expect(findJobLogSearch().exists()).toBe(false); - expect(findSearchHelp().exists()).toBe(false); - }); + beforeEach(() => { + createWrapper(); }); - describe('with feature flag on', () => { - beforeEach(() => { - createWrapper({}, { jobLogSearch: true }); - }); - - it('displays job log search', () => { - expect(findJobLogSearch().exists()).toBe(true); - expect(findSearchHelp().exists()).toBe(true); - }); + it('displays job log search', () => { + expect(findJobLogSearch().exists()).toBe(true); + expect(findSearchHelp().exists()).toBe(true); + }); - it('emits search results', () => { - const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]]; + it('emits search results', () => { + const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]]; - findJobLogSearch().vm.$emit('submit'); + findJobLogSearch().vm.$emit('submit'); - expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults); - }); + expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults); + }); - it('clears search results', () => { - findJobLogSearch().vm.$emit('clear'); + it('clears search results', () => { + findJobLogSearch().vm.$emit('clear'); - expect(wrapper.emitted('searchResults')).toEqual([[[]]]); - }); + expect(wrapper.emitted('searchResults')).toEqual([[[]]]); }); }); }); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 733d89fe08c..8d179baa505 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -586,6 +586,33 @@ describe('init markdown', () => { ); }); + it('only converts valid URLs', () => { + const notValidUrl = 'group::label'; + const expectedUrlValue = 'url'; + const expectedText = `other [${notValidUrl}](${expectedUrlValue}) text`; + const initialValue = `other ${notValidUrl} text`; + + textArea.value = initialValue; + selectedIndex = initialValue.indexOf(notValidUrl); + textArea.setSelectionRange(selectedIndex, selectedIndex + notValidUrl.length); + + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected: notValidUrl, + wrap: false, + select, + }); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedUrlValue, 1)); + expect(textArea.selectionEnd).toEqual( + expectedText.indexOf(expectedUrlValue, 1) + expectedUrlValue.length, + ); + }); + it('adds block tags on line above and below selection', () => { selected = 'this text\nis multiple\nlines'; text = `before \n${selected}\nafter `; diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js index b8fbc2b2460..09d4f9736ad 100644 --- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js +++ b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js @@ -2,6 +2,7 @@ import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/u import { nextTick } from 'vue'; import { createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue'; @@ -9,6 +10,7 @@ import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_valid import ValidatePipelinePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue'; import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql'; import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql'; +import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants'; import { mockBlobContentQueryResponse, mockCiLintPath, @@ -24,6 +26,7 @@ describe('Pipeline Editor Validate Tab', () => { let wrapper; let mockApollo; let mockBlobContentData; + let trackingSpy; const createComponent = ({ props, @@ -140,9 +143,24 @@ describe('Pipeline Editor Validate Tab', () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); await createComponentWithApollo(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); }); + afterEach(() => { + unmockTracking(); + }); + + it('tracks the simulation event', () => { + const { + label, + actions: { simulatePipeline }, + } = pipelineEditorTrackingOptions; + findCta().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, simulatePipeline, { label }); + }); + it('renders loading state while simulation is ongoing', async () => { findCta().vm.$emit('click'); await nextTick(); @@ -224,10 +242,27 @@ describe('Pipeline Editor Validate Tab', () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); await createComponentWithApollo(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); await findCta().vm.$emit('click'); }); + afterEach(() => { + unmockTracking(); + }); + + it('tracks the second simulation event', async () => { + const { + label, + actions: { resimulatePipeline }, + } = pipelineEditorTrackingOptions; + + await wrapper.setProps({ ciFileContent: 'new yaml content' }); + findResultsCta().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, resimulatePipeline, { label }); + }); + it('renders content change status', async () => { await wrapper.setProps({ ciFileContent: 'new yaml content' }); diff --git a/spec/graphql/mutations/merge_requests/create_spec.rb b/spec/graphql/mutations/merge_requests/create_spec.rb index e1edb60e4ff..6e593a5f4be 100644 --- a/spec/graphql/mutations/merge_requests/create_spec.rb +++ b/spec/graphql/mutations/merge_requests/create_spec.rb @@ -7,8 +7,7 @@ RSpec.describe Mutations::MergeRequests::Create do subject(:mutation) { described_class.new(object: nil, context: context, field: nil) } - let_it_be(:project) { create(:project, :public, :repository) } - let_it_be(:user) { create(:user) } + let(:user) { create(:user) } let(:context) do GraphQL::Query::Context.new( @@ -38,62 +37,106 @@ RSpec.describe Mutations::MergeRequests::Create do let(:mutated_merge_request) { subject[:merge_request] } - it 'raises an error if the resource is not accessible to the user' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + shared_examples 'resource not available' do + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end end - context 'when user does not have enough permissions to create a merge request' do - before do - project.add_guest(user) - end + context 'when user is not a project member' do + let_it_be(:project) { create(:project, :public, :repository) } - it 'raises an error if the resource is not accessible to the user' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end + it_behaves_like 'resource not available' end - context 'when the user can create a merge request' do - before_all do - project.add_developer(user) - end + context 'when user is a direct project member' do + let_it_be(:project) { create(:project, :public, :repository) } - it 'creates a new merge request' do - expect { mutated_merge_request }.to change(MergeRequest, :count).by(1) - end + context 'and user is a guest' do + before do + project.add_guest(user) + end - it 'returns a new merge request' do - expect(mutated_merge_request.title).to eq(title) - expect(subject[:errors]).to be_empty + it_behaves_like 'resource not available' end - context 'when optional description field is set' do - let(:description) { 'content' } + context 'and user is a developer' do + before do + project.add_developer(user) + end + + it 'creates a new merge request' do + expect { mutated_merge_request }.to change(MergeRequest, :count).by(1) + end - it 'returns a new merge request with a description' do - expect(mutated_merge_request.description).to eq(description) + it 'returns a new merge request' do + expect(mutated_merge_request.title).to eq(title) expect(subject[:errors]).to be_empty end - end - context 'when optional labels field is set' do - let(:labels) { %w[label-1 label-2] } + context 'when optional description field is set' do + let(:description) { 'content' } - it 'returns a new merge request with labels' do - expect(mutated_merge_request.labels.map(&:title)).to eq(labels) - expect(subject[:errors]).to be_empty + it 'returns a new merge request with a description' do + expect(mutated_merge_request.description).to eq(description) + expect(subject[:errors]).to be_empty + end + end + + context 'when optional labels field is set' do + let(:labels) { %w[label-1 label-2] } + + it 'returns a new merge request with labels' do + expect(mutated_merge_request.labels.map(&:title)).to eq(labels) + expect(subject[:errors]).to be_empty + end + end + + context 'when service cannot create a merge request' do + let(:title) { nil } + + it 'does not create a new merge request' do + expect { mutated_merge_request }.not_to change(MergeRequest, :count) + end + + it 'returns errors' do + expect(mutated_merge_request).to be_nil + expect(subject[:errors]).to match_array(['Title can\'t be blank']) + end end end + end - context 'when service cannot create a merge request' do - let(:title) { nil } + context 'when user is an inherited member from the group' do + let_it_be(:group) { create(:group, :public) } - it 'does not create a new merge request' do - expect { mutated_merge_request }.not_to change(MergeRequest, :count) + context 'when project is public with private merge requests' do + let_it_be(:project) do + create(:project, + :public, + :repository, + group: group, + merge_requests_access_level: ProjectFeature::DISABLED) end - it 'returns errors' do - expect(mutated_merge_request).to be_nil - expect(subject[:errors]).to eq(['Title can\'t be blank']) + context 'and user is a guest' do + before do + group.add_guest(user) + end + + it_behaves_like 'resource not available' + end + end + + context 'when project is private' do + let_it_be(:project) { create(:project, :private, :repository, group: group) } + + context 'and user is a guest' do + before do + group.add_guest(user) + end + + it_behaves_like 'resource not available' end end end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 55ad119ea21..1f8543227c9 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -155,7 +155,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }], only: { refs: %w(branches tags) }, - job_variables: { 'VAR' => 'job' }, + job_variables: { 'VAR' => { value: 'job' } }, root_variables_inheritance: true, after_script: [], ignore: false, @@ -215,7 +215,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], - job_variables: { 'VAR' => 'job' }, + job_variables: { 'VAR' => { value: 'job' } }, root_variables_inheritance: true, ignore: false, after_script: ['make clean'], diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 890ba51157a..75f6a773c2d 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -97,15 +97,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - job_variables: [{ key: 'VAR1', value: 'var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }], + job_variables: [{ key: 'VAR1', value: 'var 1' }, + { key: 'VAR2', value: 'var 2' }], rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } end it do - is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }]) + is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR2', value: 'var 2' }]) end end @@ -114,13 +114,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do { name: 'rspec', ref: 'master', - job_variables: [{ key: 'VARIABLE', value: 'value', public: true }], + job_variables: [{ key: 'VARIABLE', value: 'value' }], tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE'] } end it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) } - it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value', public: true }]) } + it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) } end context 'with cache:key' do @@ -257,19 +257,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - yaml_variables: [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }], - job_variables: [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }], + yaml_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], + job_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], root_variables_inheritance: root_variables_inheritance } end context 'when the pipeline has variables' do let(:root_variables) do - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var pipeline 2', public: true }, - { key: 'VAR3', value: 'var pipeline 3', public: true }, - { key: 'VAR4', value: 'new var pipeline 4', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var pipeline 2' }, + { key: 'VAR3', value: 'var pipeline 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] end context 'when root_variables_inheritance is true' do @@ -277,10 +277,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns calculated yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }, - { key: 'VAR4', value: 'new var pipeline 4', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] ) end end @@ -290,8 +290,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns job variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }] + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] ) end end @@ -301,9 +301,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns calculated yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] ) end end @@ -314,8 +314,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns seed yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }]) + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }]) end end end @@ -324,8 +324,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }], - job_variables: [{ key: 'VAR1', value: 'var 1', public: true }], + yaml_variables: [{ key: 'VAR1', value: 'var 1' }], + job_variables: [{ key: 'VAR1', value: 'var 1' }], root_variables_inheritance: root_variables_inheritance, rules: rules } end @@ -338,14 +338,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true }, - { key: 'VAR2', value: 'new var 2', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'new var 2' }) end end context 'when the rules use root variables' do let(:root_variables) do - [{ key: 'VAR2', value: 'var pipeline 2', public: true }] + [{ key: 'VAR2', value: 'var pipeline 2' }] end let(:rules) do @@ -353,15 +353,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true }, - { key: 'VAR2', value: 'overridden var 2', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'overridden var 2' }) end context 'when the root_variables_inheritance is false' do let(:root_variables_inheritance) { false } it 'does not recalculate the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' }) end end end diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb index fc1055751bd..2a1cdaeb3a7 100644 --- a/spec/lib/gitlab/ci/variables/helpers_spec.rb +++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb @@ -15,21 +15,27 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do end let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end subject { described_class.merge_variables(current_variables, new_variables) } - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } context 'when new variables is a hash' do let(:new_variables) do { 'key2' => 'value22', 'key3' => 'value3' } end - it { is_expected.to eq(result) } + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] + end + + it { is_expected.to match_array(result) } end context 'when new variables is a hash with symbol keys' do @@ -37,79 +43,72 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do { key2: 'value22', key3: 'value3' } end - it { is_expected.to eq(result) } + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] + end + + it { is_expected.to match_array(result) } end context 'when new variables is nil' do let(:new_variables) {} let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end end - describe '.transform_to_yaml_variables' do - let(:variables) do - { 'key1' => 'value1', 'key2' => 'value2' } - end + describe '.transform_to_array' do + subject { described_class.transform_to_array(variables) } - let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] - end - - subject { described_class.transform_to_yaml_variables(variables) } - - it { is_expected.to eq(result) } - - context 'when variables is nil' do - let(:variables) {} - - it { is_expected.to eq([]) } - end - end + context 'when values are strings' do + let(:variables) do + { 'key1' => 'value1', 'key2' => 'value2' } + end - describe '.transform_from_yaml_variables' do - let(:variables) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] - end + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end - let(:result) do - { 'key1' => 'value1', 'key2' => 'value2' } + it { is_expected.to match_array(result) } end - subject { described_class.transform_from_yaml_variables(variables) } - - it { is_expected.to eq(result) } - context 'when variables is nil' do let(:variables) {} - it { is_expected.to eq({}) } + it { is_expected.to match_array([]) } end - context 'when variables is a hash' do + context 'when values are hashes' do let(:variables) do - { key1: 'value1', 'key2' => 'value2' } + { 'key1' => { value: 'value1', description: 'var 1' }, 'key2' => { value: 'value2' } } end - it { is_expected.to eq(result) } - end - - context 'when variables contain integers and symbols' do - let(:variables) do - { key1: 1, key2: :value2 } + let(:result) do + [{ key: 'key1', value: 'value1', description: 'var 1' }, + { key: 'key2', value: 'value2' }] end - let(:result1) do - { 'key1' => '1', 'key2' => 'value2' } - end + it { is_expected.to match_array(result) } + + context 'when a value data has `key` as a key' do + let(:variables) do + { 'key1' => { value: 'value1', key: 'new_key1' }, 'key2' => { value: 'value2' } } + end - it { is_expected.to eq(result1) } + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end + + it { is_expected.to match_array(result) } + end end end @@ -127,35 +126,35 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do let(:inheritance) { true } let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end subject { described_class.inherit_yaml_variables(from: from, to: to, inheritance: inheritance) } - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } context 'when inheritance is false' do let(:inheritance) { false } let(:result) do - [{ key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end context 'when inheritance is array' do let(:inheritance) { ['key2'] } let(:result) do - [{ key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end end end diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index 8416501e949..f7a0905d9da 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -72,8 +72,8 @@ module Gitlab it 'returns calculated variables with root and job variables' do is_expected.to match_array([ - { key: 'VAR1', value: 'value 11', public: true }, - { key: 'VAR2', value: 'value 2', public: true } + { key: 'VAR1', value: 'value 11' }, + { key: 'VAR2', value: 'value 2' } ]) end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 35af9ae6201..3477fe837b4 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -448,7 +448,7 @@ module Gitlab it 'parses the root:variables as #root_variables' do expect(subject.root_variables) - .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) + .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' }) end end @@ -490,7 +490,7 @@ module Gitlab it 'parses the root:variables as #root_variables' do expect(subject.root_variables) - .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) + .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' }) end end @@ -1098,8 +1098,8 @@ module Gitlab it 'returns job variables' do expect(job_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } + { key: 'VAR1', value: 'value1' }, + { key: 'VAR2', value: 'value2' } ) expect(root_variables_inheritance).to eq(true) end @@ -1203,21 +1203,21 @@ module Gitlab expect(config_processor.builds[0]).to include( name: 'test1', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'test1 var 1', public: true }, - { key: 'VAR2', value: 'test2 var 2', public: true }] + job_variables: [{ key: 'VAR1', value: 'test1 var 1' }, + { key: 'VAR2', value: 'test2 var 2' }] ) expect(config_processor.builds[1]).to include( name: 'test2', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }, - { key: 'VAR2', value: 'test2 var 2', public: true }] + job_variables: [{ key: 'VAR1', value: 'base var 1' }, + { key: 'VAR2', value: 'test2 var 2' }] ) expect(config_processor.builds[2]).to include( name: 'test3', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }] + job_variables: [{ key: 'VAR1', value: 'base var 1' }] ) expect(config_processor.builds[3]).to include( diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb index dd42e1b9313..7e1af132b1d 100644 --- a/spec/policies/merge_request_policy_spec.rb +++ b/spec/policies/merge_request_policy_spec.rb @@ -7,24 +7,18 @@ RSpec.describe MergeRequestPolicy do let_it_be(:guest) { create(:user) } let_it_be(:author) { create(:user) } + let_it_be(:reporter) { create(:user) } let_it_be(:developer) { create(:user) } let_it_be(:non_team_member) { create(:user) } - let(:project) { create(:project, :public) } - def permissions(user, merge_request) described_class.new(user, merge_request) end - before do - project.add_guest(guest) - project.add_guest(author) - project.add_developer(developer) - end - mr_perms = %i[create_merge_request_in create_merge_request_from read_merge_request + update_merge_request create_todo approve_merge_request create_note @@ -40,7 +34,28 @@ RSpec.describe MergeRequestPolicy do end end - shared_examples_for 'a user with access' do + shared_examples_for 'a user with reporter access' do + using RSpec::Parameterized::TableSyntax + + where(:policy, :is_allowed) do + :create_merge_request_in | true + :read_merge_request | true + :create_todo | true + :create_note | true + :update_subscription | true + :create_merge_request_from | false + :approve_merge_request | false + :update_merge_request | false + end + + with_them do + specify do + is_allowed ? (is_expected.to be_allowed(policy)) : (is_expected.to be_disallowed(policy)) + end + end + end + + shared_examples_for 'a user with full access' do let(:perms) { permissions(subject, merge_request) } mr_perms.each do |thing| @@ -50,199 +65,304 @@ RSpec.describe MergeRequestPolicy do end end - context 'when merge request is public' do - let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } - let(:user) { author } - - context 'and user is anonymous' do - subject { permissions(nil, merge_request) } + context 'when user is a direct project member' do + let(:project) { create(:project, :public) } - it do - is_expected.to be_disallowed(:create_todo, :update_subscription) - end + before do + project.add_guest(guest) + project.add_guest(author) + project.add_developer(developer) end - context 'and user is author' do - subject { permissions(user, merge_request) } + context 'when merge request is public' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + let(:user) { author } - context 'and the user is a guest' do - let(:user) { guest } + context 'and user is author' do + subject { permissions(user, merge_request) } - it do - is_expected.to be_allowed(:update_merge_request) - end + context 'and the user is a guest' do + let(:user) { guest } - it do - is_expected.to be_allowed(:reopen_merge_request) - end + it do + is_expected.to be_allowed(:update_merge_request) + end - it do - is_expected.to be_allowed(:approve_merge_request) + it do + is_expected.to be_allowed(:reopen_merge_request) + end + + it do + is_expected.to be_allowed(:approve_merge_request) + end end end + end - context 'and the user is a group member' do - let(:project) { create(:project, :public, group: group) } - let(:group) { create(:group) } - let(:user) { non_team_member } - - before do - group.add_guest(non_team_member) - end + context 'when merge requests have been disabled' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) } - it do - is_expected.to be_allowed(:approve_merge_request) - end + before do + project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) end - context 'and the user is a member of a shared group' do - let(:user) { non_team_member } + describe 'the author' do + subject { author } - before do - group = create(:group) - project.project_group_links.create!( - group: group, - group_access: Gitlab::Access::DEVELOPER) + it_behaves_like 'a denied user' + end - group.add_guest(non_team_member) - end + describe 'a guest' do + subject { guest } - it do - is_expected.to be_allowed(:approve_merge_request) - end + it_behaves_like 'a denied user' end - context 'and the user is not a project member' do - let(:user) { non_team_member } + describe 'a developer' do + subject { developer } - it do - is_expected.not_to be_allowed(:approve_merge_request) - end + it_behaves_like 'a denied user' end end - end - context 'when merge requests have been disabled' do - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) } + context 'when merge requests are private' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) } - before do - project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) - end + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + end - describe 'the author' do - subject { author } + describe 'the author' do + subject { author } - it_behaves_like 'a denied user' + it_behaves_like 'a denied user' + end + + describe 'a developer' do + subject { developer } + + it_behaves_like 'a user with full access' + end end - describe 'a guest' do - subject { guest } + context 'when merge request is unlocked' do + let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) } - it_behaves_like 'a denied user' + it 'allows author to reopen merge request' do + expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request) + end + + it 'allows developer to reopen merge request' do + expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request) + end + + it 'prevents guest from reopening merge request' do + expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request) + end end - describe 'a developer' do - subject { developer } + context 'when merge request is locked' do + let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) } - it_behaves_like 'a denied user' + it 'prevents author from reopening merge request' do + expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request) + end + + it 'prevents developer from reopening merge request' do + expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request) + end + + it 'prevents guests from reopening merge request' do + expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request) + end + + context 'when the user is project member, with at least guest access' do + let(:user) { guest } + + it 'can create a note' do + expect(permissions(user, merge_request_locked)).to be_allowed(:create_note) + end + end end - describe 'any other user' do - subject { non_team_member } + context 'with external authorization enabled' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:policies) { described_class.new(user, merge_request) } - it_behaves_like 'a denied user' + before do + enable_external_authorization_service_check + end + + it 'can read the issue iid without accessing the external service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(policies).to be_allowed(:read_merge_request_iid) + end end end - context 'when merge requests are private' do - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) } + context 'when user is an inherited member from the parent group' do + let_it_be(:group) { create(:group, :public) } - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) } + + before_all do + group.add_guest(guest) + group.add_guest(author) + group.add_reporter(reporter) + group.add_developer(developer) end - describe 'a non-team-member' do - subject { non_team_member } + context 'when project is public' do + let(:project) { create(:project, :public, group: group) } - it_behaves_like 'a denied user' - end + describe 'the merge request author' do + subject { permissions(author, merge_request) } - describe 'the author' do - subject { author } + specify do + is_expected.to be_allowed(:approve_merge_request) + end + end - it_behaves_like 'a denied user' + context 'and merge requests are private' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + end + + describe 'a guest' do + subject { guest } + + it_behaves_like 'a denied user' + end + + describe 'a reporter' do + subject { permissions(reporter, merge_request) } + + it_behaves_like 'a user with reporter access' + end + + describe 'a developer' do + subject { developer } + + it_behaves_like 'a user with full access' + end + end end - describe 'a developer' do - subject { developer } + context 'when project is private' do + let(:project) { create(:project, :private, group: group) } + + describe 'a guest' do + subject { guest } - it_behaves_like 'a user with access' + it_behaves_like 'a denied user' + end + + describe 'a reporter' do + subject { permissions(reporter, merge_request) } + + it_behaves_like 'a user with reporter access' + end + + describe 'a developer' do + subject { developer } + + it_behaves_like 'a user with full access' + end end end - context 'when merge request is unlocked' do - let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) } + context 'when user is an inherited member from a shared group' do + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + let(:user) { author } - it 'allows author to reopen merge request' do - expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request) + before do + project.add_guest(author) end - it 'allows developer to reopen merge request' do - expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request) - end + context 'and group is given developer access' do + let(:user) { non_team_member } + + subject { permissions(user, merge_request) } + + before do + group = create(:group) + project.project_group_links.create!( + group: group, + group_access: Gitlab::Access::DEVELOPER) + + group.add_guest(non_team_member) + end - it 'prevents guest from reopening merge request' do - expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request) + specify do + is_expected.to be_allowed(:approve_merge_request) + end end end - context 'when merge request is locked' do - let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) } + context 'when user is not a project member' do + let(:project) { create(:project, :public) } - it 'prevents author from reopening merge request' do - expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request) - end + context 'when merge request is public' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - it 'prevents developer from reopening merge request' do - expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request) + subject { permissions(non_team_member, merge_request) } + + specify do + is_expected.not_to be_allowed(:approve_merge_request) + end end - it 'prevents guests from reopening merge request' do - expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request) + context 'when merge requests are disabled' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + before do + project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + end + + subject { non_team_member } + + it_behaves_like 'a denied user' end - context 'when the user is not a project member' do - let(:user) { create(:user) } + context 'when merge requests are private' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - it 'cannot create a note' do - expect(permissions(user, merge_request_locked)).to be_disallowed(:create_note) + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) end + + subject { non_team_member } + + it_behaves_like 'a denied user' end - context 'when the user is project member, with at least guest access' do - let(:user) { guest } + context 'when merge request is locked' do + let(:merge_request) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project) } - it 'can create a note' do - expect(permissions(user, merge_request_locked)).to be_allowed(:create_note) + it 'cannot create a note' do + expect(permissions(non_team_member, merge_request)).to be_disallowed(:create_note) end end end - context 'with external authorization enabled' do - let(:user) { create(:user) } + context 'when user is anonymous' do let(:project) { create(:project, :public) } - let(:merge_request) { create(:merge_request, source_project: project) } - let(:policies) { described_class.new(user, merge_request) } - before do - enable_external_authorization_service_check - end + context 'when merge request is public' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - it 'can read the issue iid without accessing the external service' do - expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + subject { permissions(nil, merge_request) } - expect(policies).to be_allowed(:read_merge_request_iid) + specify do + is_expected.to be_disallowed(:create_todo, :update_subscription) + end end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 2a03ae89389..320e9e0cd66 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1120,6 +1120,44 @@ RSpec.describe API::MergeRequests do end.not_to exceed_query_limit(control) end end + + context 'when user is an inherited member from the group' do + let_it_be(:group) { create(:group) } + + shared_examples 'user cannot view merge requests' do + it 'returns 403 forbidden' do + get api("/projects/#{group_project.id}/merge_requests", inherited_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'and user is a guest' do + let_it_be(:inherited_user) { create(:user) } + + before_all do + group.add_guest(inherited_user) + end + + context 'when project is public with private merge requests' do + let(:group_project) do + create(:project, + :public, + :repository, + group: group, + merge_requests_access_level: ProjectFeature::DISABLED) + end + + it_behaves_like 'user cannot view merge requests' + end + + context 'when project is private' do + let(:group_project) { create(:project, :private, :repository, group: group) } + + it_behaves_like 'user cannot view merge requests' + end + end + end end describe "GET /groups/:id/merge_requests" do @@ -2219,6 +2257,59 @@ RSpec.describe API::MergeRequests do expect(response).to have_gitlab_http_status(:created) end end + + context 'when user is an inherited member from the group' do + let_it_be(:group) { create(:group) } + + shared_examples 'user cannot create merge requests' do + it 'returns 403 forbidden' do + post api("/projects/#{group_project.id}/merge_requests", inherited_user), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'and user is a guest' do + let_it_be(:inherited_user) { create(:user) } + let_it_be(:params) do + { + title: 'Test merge request', + source_branch: 'feature_conflict', + target_branch: 'master', + author_id: inherited_user.id + } + end + + before_all do + group.add_guest(inherited_user) + end + + context 'when project is public with private merge requests' do + let(:group_project) do + create(:project, + :public, + :repository, + group: group, + merge_requests_access_level: ProjectFeature::DISABLED, + only_allow_merge_if_pipeline_succeeds: false) + end + + it_behaves_like 'user cannot create merge requests' + end + + context 'when project is private' do + let(:group_project) do + create(:project, + :private, + :repository, + group: group, + only_allow_merge_if_pipeline_succeeds: false) + end + + it_behaves_like 'user cannot create merge requests' + end + end + end end describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index 5f4b8899a33..7a05da8e13f 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -86,6 +86,18 @@ RSpec.describe API::ProjectPackages do expect(json_response).to include(a_hash_including('_links' => a_hash_including('web_path' => include(nested_project.namespace.full_path)))) end end + + context 'with JOB-TOKEN auth' do + let(:job) { create(:ci_build, :running, user: user) } + + subject { get api(url, job_token: job.token) } + + it_behaves_like 'returns packages', :project, :maintainer + it_behaves_like 'returns packages', :project, :developer + it_behaves_like 'returns packages', :project, :reporter + it_behaves_like 'returns packages', :project, :no_type + it_behaves_like 'returns packages', :project, :guest + end end context 'project is private' do @@ -116,6 +128,19 @@ RSpec.describe API::ProjectPackages do end end end + + context 'with JOB-TOKEN auth' do + let(:job) { create(:ci_build, :running, user: user) } + + subject { get api(url, job_token: job.token) } + + it_behaves_like 'returns packages', :project, :maintainer + it_behaves_like 'returns packages', :project, :developer + it_behaves_like 'returns packages', :project, :reporter + it_behaves_like 'rejects packages access', :project, :no_type, :not_found + # TODO uncomment when https://gitlab.com/gitlab-org/gitlab/-/issues/370998 is resolved + # it_behaves_like 'rejects packages access', :project, :guest, :not_found + end end context 'with pagination params' do @@ -177,6 +202,8 @@ RSpec.describe API::ProjectPackages do end describe 'GET /projects/:id/packages/:package_id' do + let(:single_package_schema) { 'public_api/v4/packages/package' } + subject { get api(package_url, user) } shared_examples 'no destroy url' do @@ -217,7 +244,7 @@ RSpec.describe API::ProjectPackages do subject expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/packages/package') + expect(response).to match_response_schema(single_package_schema) end it 'returns 404 when the package does not exist' do @@ -233,6 +260,18 @@ RSpec.describe API::ProjectPackages do end it_behaves_like 'no destroy url' + + context 'with JOB-TOKEN auth' do + let(:job) { create(:ci_build, :running, user: user) } + + subject { get api(package_url, job_token: job.token) } + + it_behaves_like 'returns package', :project, :maintainer + it_behaves_like 'returns package', :project, :developer + it_behaves_like 'returns package', :project, :reporter + it_behaves_like 'returns package', :project, :no_type + it_behaves_like 'returns package', :project, :guest + end end context 'project is private' do @@ -259,7 +298,7 @@ RSpec.describe API::ProjectPackages do subject expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/packages/package') + expect(response).to match_response_schema(single_package_schema) end it_behaves_like 'no destroy url' @@ -273,6 +312,19 @@ RSpec.describe API::ProjectPackages do it_behaves_like 'destroy url' end + context 'with JOB-TOKEN auth' do + let(:job) { create(:ci_build, :running, user: user) } + + subject { get api(package_url, job_token: job.token) } + + it_behaves_like 'returns package', :project, :maintainer + it_behaves_like 'returns package', :project, :developer + it_behaves_like 'returns package', :project, :reporter + # TODO uncomment when https://gitlab.com/gitlab-org/gitlab/-/issues/370998 is resolved + # it_behaves_like 'rejects packages access', :project, :guest, :not_found + it_behaves_like 'rejects packages access', :project, :no_type, :not_found + end + context 'with pipeline' do let!(:package1) { create(:npm_package, :with_build, project: project) } @@ -355,6 +407,26 @@ RSpec.describe API::ProjectPackages do expect(response).to have_gitlab_http_status(:no_content) end + + context 'with JOB-TOKEN auth' do + let(:job) { create(:ci_build, :running, user: user) } + + it 'returns 403 for a user without enough permissions' do + project.add_developer(user) + + expect { delete api(package_url, job_token: job.token) }.not_to change { ::Packages::Package.pending_destruction.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 204' do + project.add_maintainer(user) + + expect { delete api(package_url, job_token: job.token) }.to change { ::Packages::Package.pending_destruction.count }.by(1) + + expect(response).to have_gitlab_http_status(:no_content) + end + end end context 'with a maven package' do diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb index 4326fa5533f..cc808b7e61c 100644 --- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb +++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do expect(pipeline.statuses).to match_array [test, bridge] expect(bridge.options).to eq(expected_bridge_options) expect(bridge.yaml_variables) - .to include(key: 'CROSS', value: 'downstream', public: true) + .to include(key: 'CROSS', value: 'downstream') end end diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb index 1735f4cfc97..4953b18bfcc 100644 --- a/spec/services/ci/list_config_variables_service_spec.rb +++ b/spec/services/ci/list_config_variables_service_spec.rb @@ -40,8 +40,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac it 'returns variable list' do expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' }) expect(subject['KEY2']).to eq({ value: 'val 2', description: '' }) - expect(subject['KEY3']).to eq({ value: 'val 3', description: nil }) - expect(subject['KEY4']).to eq({ value: 'val 4', description: nil }) + expect(subject['KEY3']).to eq({ value: 'val 3' }) + expect(subject['KEY4']).to eq({ value: 'val 4' }) end end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 6bc4f171d9c..704a4bbe0b8 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -81,6 +81,26 @@ RSpec.shared_examples 'returns packages' do |container_type, user_type| end end +RSpec.shared_examples 'returns package' do |container_type, user_type| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(single_package_schema) + end + end +end + RSpec.shared_examples 'returns packages with subgroups' do |container_type, user_type| context "with subgroups for #{user_type}" do before do diff --git a/vendor/gems/omniauth-azure-oauth2/.gitlab-ci.yml b/vendor/gems/omniauth-azure-oauth2/.gitlab-ci.yml new file mode 100644 index 00000000000..575fec39767 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/.gitlab-ci.yml @@ -0,0 +1,28 @@ +workflow: + rules: + - if: $CI_MERGE_REQUEST_ID + +.rspec: + cache: + key: omniauth-azure-oauth2 + paths: + - vendor/gems/omniauth-azure-oauth2/vendor/ruby + before_script: + - cd vendor/gems/omniauth-azure-oauth2 + - ruby -v # Print out ruby version for debugging + - gem install bundler --no-document # Bundler is not installed with the image + - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby + - bundle config set with 'development' + - bundle config set --local frozen 'true' # Disallow Gemfile.lock changes on CI + - bundle config # Show bundler configuration + - bundle install -j $(nproc) + script: + - bundle exec rspec + +rspec-2.7: + image: "ruby:2.7" + extends: .rspec + +rspec-3.0: + image: "ruby:3.0" + extends: .rspec diff --git a/vendor/gems/omniauth-azure-oauth2/CHANGELOG.md b/vendor/gems/omniauth-azure-oauth2/CHANGELOG.md new file mode 100644 index 00000000000..ca274303b18 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/CHANGELOG.md @@ -0,0 +1,31 @@ +# Version 0.0.9 +* Expand JWT dep. Thanks @ronaldsalas + +# Version 0.0.9 +* Added support for dynamic tenant urls. Thanks @marcus-fellinger-esc + +# Version 0.0.8 +* Upgrade to omniauth-oauth2 1.4.0 and fix callback url issue +* Allow prompt parameter, thanks @hilu +* Add tenant id to info +* Updated base url + +# Version 0.0.6 +* Use 'name' from Azure for name, and 'unique_name' for nickname per Auth Hash spec. Thanks @jayme-github + +# Version 0.0.5 +* loosen jwt requirement + +# Version 0.0.5 +* loosen jwt requirement + +# VERSION 0.0.4 +* fix for JWT scoping, thanks @tobsher + +# VERSION 0.0.3 +* added common endpoint and removed mandatory requirement for tenant-id +* upgraded jwt + +# VERSION 0.0.1 + +* Initial build
\ No newline at end of file diff --git a/vendor/gems/omniauth-azure-oauth2/Gemfile b/vendor/gems/omniauth-azure-oauth2/Gemfile new file mode 100644 index 00000000000..ef2f8b4147f --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in omniauth-azure-oauth2.gemspec +gemspec + +group :example do + gem 'sinatra' +end
\ No newline at end of file diff --git a/vendor/gems/omniauth-azure-oauth2/Gemfile.lock b/vendor/gems/omniauth-azure-oauth2/Gemfile.lock new file mode 100644 index 00000000000..0bd5d401175 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/Gemfile.lock @@ -0,0 +1,73 @@ +PATH + remote: . + specs: + omniauth-azure-oauth2 (0.0.10) + jwt (>= 1.0, < 3.0) + omniauth (~> 1.0, < 3) + omniauth-oauth2 (~> 1.4) + +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.5.0) + faraday (2.5.2) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.0) + hashie (5.0.0) + jwt (2.4.1) + multi_xml (0.6.0) + mustermann (2.0.2) + ruby2_keywords (~> 0.0.1) + oauth2 (2.0.6) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + rash_alt (>= 0.4, < 1) + version_gem (~> 1.1) + omniauth (1.9.1) + hashie (>= 3.4.6) + rack (>= 1.6.2, < 3) + omniauth-oauth2 (1.7.3) + oauth2 (>= 1.4, < 3) + omniauth (>= 1.9, < 3) + rack (2.2.4) + rack-protection (2.2.2) + rack + rake (13.0.6) + rash_alt (0.4.12) + hashie (>= 3.4) + rspec (3.11.0) + rspec-core (~> 3.11.0) + rspec-expectations (~> 3.11.0) + rspec-mocks (~> 3.11.0) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.11.0) + rspec-support (3.11.0) + ruby2_keywords (0.0.5) + sinatra (2.2.2) + mustermann (~> 2.0) + rack (~> 2.2) + rack-protection (= 2.2.2) + tilt (~> 2.0) + tilt (2.0.11) + version_gem (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + omniauth-azure-oauth2! + rake + rspec (>= 2.14.0) + sinatra + +BUNDLED WITH + 2.3.20 diff --git a/vendor/gems/omniauth-azure-oauth2/LICENSE b/vendor/gems/omniauth-azure-oauth2/LICENSE new file mode 100644 index 00000000000..57ecd3eabb9 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2014 Deltek + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/vendor/gems/omniauth-azure-oauth2/README.md b/vendor/gems/omniauth-azure-oauth2/README.md new file mode 100644 index 00000000000..a28e9ffdfd2 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/README.md @@ -0,0 +1,161 @@ +# OmniAuth Windows Azure Active Directory Strategy + +This is fork of [omniauth-azure-oauth2](https://github.com/marknadig/omniauth-azure-oauth2) to support: + +1. OmniAuth v1 and v2. OmniAuth v2 disables GET requests by default + and defaults to POST. GitLab already has patched v1 to use POST, + but other dependencies need to be updated: + https://gitlab.com/gitlab-org/gitlab/-/issues/30073. +2. We may deprecate this library entirely in the future: + https://gitlab.com/gitlab-org/gitlab/-/issues/366212 + +[![Build Status](https://travis-ci.org/KonaTeam/omniauth-azure-oauth2.svg?branch=master)](https://travis-ci.org/KonaTeam/omniauth-azure-oauth2) + +This gem provides a simple way to authenticate to Windows Azure Active Directory (WAAD) over OAuth2 using OmniAuth. + +One of the unique challenges of WAAD OAuth is that WAAD is multi tenant. Any given tenant can have multiple active +directories. The CLIENT-ID, REPLY-URL and keys will be unique to the tenant/AD/application combination. This gem simply +provides hooks for determining those unique values for each call. + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'omniauth-azure-oauth2' +``` + +## Usage + +First, you will need to add your site as an application in WAAD.: +[Adding, Updating, and Removing an Application](http://msdn.microsoft.com/en-us/library/azure/dn132599.aspx) + +Summary: +Select your Active Directory in https://manage.windowsazure.com/<tenantid> of type 'Web Application'. Name, sign-on url, +logo are not important. You will need the CLIENT-ID from the application configuration and you will need to generate +an expiring key (aka 'client secret'). REPLY URL is the oauth redirect uri which will be the omniauth callback path +https://example.com/users/auth/azure_oauth2/callback. The APP ID UI just needs to be unique to that tenant and identify +your site and isn't needed to configure the gem. +Permissions need Delegated Permissions to at least have "Enable sign-on and read user's profiles". + +Note: Seems like the terminology is still fluid, so follow the MS guidance (buwahaha) to set this up. + +The TenantInfo information can be a hash or class. It must provide client_id and client_secret. +Optionally a domain_hint and tenant_id. For a simple single-tenant app, this could be: + +```ruby +use OmniAuth::Builder do + provider :azure_oauth2, + { + client_id: ENV['AZURE_CLIENT_ID'], + client_secret: ENV['AZURE_CLIENT_SECRET'], + tenant_id: ENV['AZURE_TENANT_ID'] + } +end +``` + +Or the alternative format for use with [devise](https://github.com/plataformatec/devise): + +```ruby +config.omniauth :azure_oauth2, client_id: ENV['AZURE_CLIENT_ID'], + client_secret: ENV['AZURE_CLIENT_SECRET'], tenant_id: ENV['AZURE_TENANT_ID'] +``` + +For multi-tenant apps where you don't know the tenant_id in advance, simply leave out the tenant_id to use the +[common endpoint](http://msdn.microsoft.com/en-us/library/azure/dn645542.aspx). + +```ruby +use OmniAuth::Builder do + provider :azure_oauth2, + { + client_id: ENV['AZURE_CLIENT_ID'], + client_secret: ENV['AZURE_CLIENT_SECRET'] + } +end +``` + +For dynamic tenant assignment, pass a class that supports those same attributes and accepts the strategy as a parameter + +```ruby +class YouTenantProvider + def initialize(strategy) + @strategy = strategy + end + + def client_id + tenant.azure_client_id + end + + def client_secret + tenant.azure_client_secret + end + + def tenant_id + tenant.azure_tanant_id + end + + def domain_hint + tenant.azure_domain_hint + end + + private + + def tenant + # whatever strategy you want to figure out the right tenant from params/session + @tenant ||= Customer.find(@strategy.session[:customer_id]) + end +end + +use OmniAuth::Builder do + provider :azure_oauth2, YourTenantProvider +end +``` + +The base_azure_url can be overridden in the provider configuration for different locales; e.g. `base_azure_url: "https://login.microsoftonline.de"` + + +## Auth Hash Schema + +The following information is provided back to you for this provider: + +```ruby +{ + uid: '12345', + info: { + name: 'some one', + first_name: 'some', + last_name: 'one', + email: 'someone@example.com' + }, + credentials: { + token: 'thetoken', + refresh_token: 'refresh' + }, + extra: { raw_info: raw_api_response } +} +``` +## notes + +When you make a request to WAAD you must specify a resource. The gem currently assumes this is the AD identified as '00000002-0000-0000-c000-000000000000'. +This can be passed in as part of the config. It currently isn't designed to be dynamic. + +```ruby +use OmniAuth::Builder do + provider :azure_oauth2, TenantInfo, resource: 'myresource' +end +``` + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Make your changes, add tests, run tests (`rake`) +4. Commit your changes and tests (`git commit -am 'Added some feature'`) +5. Push to the branch (`git push origin my-new-feature`) +6. Create new Pull Request + + +## Misc +Run tests `bundle exec rake` +Push to rubygems `bundle exec rake release`. + diff --git a/vendor/gems/omniauth-azure-oauth2/Rakefile b/vendor/gems/omniauth-azure-oauth2/Rakefile new file mode 100644 index 00000000000..965431eb7c9 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/Rakefile @@ -0,0 +1,6 @@ +require File.join('bundler', 'gem_tasks') +require File.join('rspec', 'core', 'rake_task') + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec
\ No newline at end of file diff --git a/vendor/gems/omniauth-azure-oauth2/examples/sinatra.rb b/vendor/gems/omniauth-azure-oauth2/examples/sinatra.rb new file mode 100644 index 00000000000..3db9e5fe435 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/examples/sinatra.rb @@ -0,0 +1,31 @@ +$:.push File.dirname(__FILE__) + '/../lib' + +require 'omniauth-azure-oauth2' +require 'sinatra' + +class MyAzureProvider + def self.client_id + ENV['AZURE_CLIENT_ID'] + end + + def self.client_secret + ENV['AZURE_CLIENT_SECRET'] + end + + def self.tenant_id + ENV['AZURE_TENANT_ID'] + end + +end + +use Rack::Session::Cookie +use OmniAuth::Strategies::Azure, MyAzureProvider + +get '/' do + "<a href='/auth/azure_oauth2'>Log in with Azure</a>" +end + +get '/auth/azure_oauth2/callback' do + content_type 'text/plain' + request.env['omniauth.auth'].inspect +end
\ No newline at end of file diff --git a/vendor/gems/omniauth-azure-oauth2/lib/omniauth-azure-oauth2.rb b/vendor/gems/omniauth-azure-oauth2/lib/omniauth-azure-oauth2.rb new file mode 100644 index 00000000000..121c26842aa --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/lib/omniauth-azure-oauth2.rb @@ -0,0 +1 @@ +require File.join('omniauth', 'azure_oauth2')
\ No newline at end of file diff --git a/vendor/gems/omniauth-azure-oauth2/lib/omniauth/azure_oauth2.rb b/vendor/gems/omniauth-azure-oauth2/lib/omniauth/azure_oauth2.rb new file mode 100644 index 00000000000..69651ede9e7 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/lib/omniauth/azure_oauth2.rb @@ -0,0 +1 @@ +require File.join('omniauth', 'strategies', 'azure_oauth2')
\ No newline at end of file diff --git a/vendor/gems/omniauth-azure-oauth2/lib/omniauth/azure_oauth2/version.rb b/vendor/gems/omniauth-azure-oauth2/lib/omniauth/azure_oauth2/version.rb new file mode 100644 index 00000000000..cfaa9ddd458 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/lib/omniauth/azure_oauth2/version.rb @@ -0,0 +1,5 @@ +module OmniAuth + module AzureOauth2 + VERSION = "0.0.10" + end +end diff --git a/vendor/gems/omniauth-azure-oauth2/lib/omniauth/strategies/azure_oauth2.rb b/vendor/gems/omniauth-azure-oauth2/lib/omniauth/strategies/azure_oauth2.rb new file mode 100644 index 00000000000..f18babc0619 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/lib/omniauth/strategies/azure_oauth2.rb @@ -0,0 +1,73 @@ +require 'omniauth/strategies/oauth2' +require 'jwt' + +module OmniAuth + module Strategies + class AzureOauth2 < OmniAuth::Strategies::OAuth2 + BASE_AZURE_URL = 'https://login.microsoftonline.com' + + option :name, 'azure_oauth2' + + option :tenant_provider, nil + + # AD resource identifier + option :resource, '00000002-0000-0000-c000-000000000000' + + # tenant_provider must return client_id, client_secret and optionally tenant_id and base_azure_url + args [:tenant_provider] + + def client + if options.tenant_provider + provider = options.tenant_provider.new(self) + else + provider = options # if pass has to config, get mapped right on to options + end + + options.client_id = provider.client_id + options.client_secret = provider.client_secret + options.tenant_id = + provider.respond_to?(:tenant_id) ? provider.tenant_id : 'common' + options.base_azure_url = + provider.respond_to?(:base_azure_url) ? provider.base_azure_url : BASE_AZURE_URL + + options.authorize_params = provider.authorize_params if provider.respond_to?(:authorize_params) + options.authorize_params.domain_hint = provider.domain_hint if provider.respond_to?(:domain_hint) && provider.domain_hint + options.authorize_params.prompt = request.params['prompt'] if request.params['prompt'] + options.client_options.authorize_url = "#{options.base_azure_url}/#{options.tenant_id}/oauth2/authorize" + options.client_options.token_url = "#{options.base_azure_url}/#{options.tenant_id}/oauth2/token" + super + end + + uid { + raw_info['sub'] + } + + info do + { + name: raw_info['name'], + nickname: raw_info['unique_name'], + first_name: raw_info['given_name'], + last_name: raw_info['family_name'], + email: raw_info['email'] || raw_info['upn'], + oid: raw_info['oid'], + tid: raw_info['tid'] + } + end + + def token_params + azure_resource = request.env['omniauth.params'] && request.env['omniauth.params']['azure_resource'] + super.merge(resource: azure_resource || options.resource) + end + + def callback_url + full_host + script_name + callback_path + end + + def raw_info + # it's all here in JWT http://msdn.microsoft.com/en-us/library/azure/dn195587.aspx + @raw_info ||= ::JWT.decode(access_token.token, nil, false).first + end + + end + end +end diff --git a/vendor/gems/omniauth-azure-oauth2/omniauth-azure-oauth2.gemspec b/vendor/gems/omniauth-azure-oauth2/omniauth-azure-oauth2.gemspec new file mode 100644 index 00000000000..6e1bc583881 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/omniauth-azure-oauth2.gemspec @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +require File.expand_path(File.join('..', 'lib', 'omniauth', 'azure_oauth2', 'version'), __FILE__) + +Gem::Specification.new do |gem| + gem.authors = ["Mark Nadig"] + gem.email = ["mark@nadigs.net"] + gem.description = %q{An Windows Azure Active Directory OAuth2 strategy for OmniAuth} + gem.summary = %q{An Windows Azure Active Directory OAuth2 strategy for OmniAuth} + gem.homepage = "https://github.com/KonaTeam/omniauth-azure-oauth2" + + gem.files = Dir.glob("lib/**/*.*") + gem.test_files = Dir.glob("spec/**/**/*.*") + gem.name = "omniauth-azure-oauth2" + gem.require_paths = ["lib"] + gem.version = OmniAuth::AzureOauth2::VERSION + gem.license = "MIT" + + gem.add_runtime_dependency 'omniauth', '~> 1.0', '< 3' + gem.add_dependency 'jwt', ['>= 1.0', '< 3.0'] + + gem.add_runtime_dependency 'omniauth-oauth2', '~> 1.4' + + gem.add_development_dependency 'rspec', '>= 2.14.0' + gem.add_development_dependency 'rake' +end diff --git a/vendor/gems/omniauth-azure-oauth2/spec/omniauth/strategies/azure_oauth2_spec.rb b/vendor/gems/omniauth-azure-oauth2/spec/omniauth/strategies/azure_oauth2_spec.rb new file mode 100644 index 00000000000..d171d88ac6c --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/spec/omniauth/strategies/azure_oauth2_spec.rb @@ -0,0 +1,332 @@ +require 'spec_helper' +require 'omniauth-azure-oauth2' + +module OmniAuth + module Strategies + module JWT; end + end +end + +describe OmniAuth::Strategies::AzureOauth2 do + let(:request) { double('Request', :params => {}, :cookies => {}, :env => {}) } + let(:app) { + lambda do + [200, {}, ["Hello."]] + end + } + + before do + OmniAuth.config.test_mode = true + end + + after do + OmniAuth.config.test_mode = false + end + + describe 'static configuration' do + let(:options) { @options || {} } + subject do + OmniAuth::Strategies::AzureOauth2.new(app, {client_id: 'id', client_secret: 'secret', tenant_id: 'tenant'}.merge(options)) + end + + describe '#client' do + it 'has correct authorize url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/authorize') + end + + it 'has correct authorize params' do + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + end + + it 'has correct token url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/token') + end + + describe "overrides" do + it 'should override domain_hint' do + @options = {domain_hint: 'hint'} + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to eql('hint') + end + end + end + + end + + describe 'static configuration - german' do + let(:options) { @options || {} } + subject do + OmniAuth::Strategies::AzureOauth2.new(app, {client_id: 'id', client_secret: 'secret', tenant_id: 'tenant', base_azure_url: 'https://login.microsoftonline.de'}.merge(options)) + end + + describe '#client' do + it 'has correct authorize url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/authorize') + end + + it 'has correct authorize params' do + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + end + + it 'has correct token url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/token') + end + + it 'has correct token params' do + allow(subject).to receive(:request) { request } + subject.client + expect(subject.token_params[:resource]).to eql('00000002-0000-0000-c000-000000000000') + end + + describe "overrides" do + it 'should override domain_hint' do + @options = {domain_hint: 'hint'} + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to eql('hint') + end + end + end + end + + describe 'static common configuration' do + let(:options) { @options || {} } + subject do + OmniAuth::Strategies::AzureOauth2.new(app, {client_id: 'id', client_secret: 'secret'}.merge(options)) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/common/oauth2/authorize') + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/common/oauth2/token') + end + end + end + + describe 'dynamic configuration' do + let(:provider_klass) { + Class.new { + def initialize(strategy) + end + + def client_id + 'id' + end + + def client_secret + 'secret' + end + + def tenant_id + 'tenant' + end + + def authorize_params + { custom_option: 'value' } + end + } + } + + subject do + OmniAuth::Strategies::AzureOauth2.new(app, provider_klass) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/authorize') + end + + it 'has correct authorize params' do + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + expect(subject.authorize_params[:custom_option]).to eql('value') + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/token') + end + + it 'has correct token params' do + subject.client + expect(subject.token_params[:resource]).to eql('00000002-0000-0000-c000-000000000000') + end + + # todo: how to get this working? + # describe "overrides" do + # it 'should override domain_hint' do + # provider_klass.domain_hint = 'hint' + # subject.client + # expect(subject.authorize_params[:domain_hint]).to eql('hint') + # end + # end + end + + end + + describe 'dynamic configuration - german' do + let(:provider_klass) { + Class.new { + def initialize(strategy) + end + + def client_id + 'id' + end + + def client_secret + 'secret' + end + + def tenant_id + 'tenant' + end + + def base_azure_url + 'https://login.microsoftonline.de' + end + } + } + + subject do + OmniAuth::Strategies::AzureOauth2.new(app, provider_klass) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/authorize') + end + + it 'has correct authorize params' do + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/token') + end + + it 'has correct token params' do + subject.client + expect(subject.token_params[:resource]).to eql('00000002-0000-0000-c000-000000000000') + end + + # todo: how to get this working? + # describe "overrides" do + # it 'should override domain_hint' do + # provider_klass.domain_hint = 'hint' + # subject.client + # expect(subject.authorize_params[:domain_hint]).to eql('hint') + # end + # end + end + + end + + describe 'dynamic common configuration' do + let(:provider_klass) { + Class.new { + def initialize(strategy) + end + + def client_id + 'id' + end + + def client_secret + 'secret' + end + } + } + + subject do + OmniAuth::Strategies::AzureOauth2.new(app, provider_klass) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/common/oauth2/authorize') + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/common/oauth2/token') + end + end + end + + describe "raw_info" do + subject do + OmniAuth::Strategies::AzureOauth2.new(app, {client_id: 'id', client_secret: 'secret'}) + end + + let(:token) do + JWT.encode({"some" => "payload"}, "secret") + end + + let(:access_token) do + double(:token => token) + end + + before do + allow(subject).to receive(:access_token) { access_token } + allow(subject).to receive(:request) { request } + end + + it "does not clash if JWT strategy is used" do + expect do + subject.info + end.to_not raise_error + end + end + + describe 'token_params' do + let(:strategy) { OmniAuth::Strategies::AzureOauth2.new(app, client_id: 'id', client_secret: 'secret') } + let(:request) { double('Request', env: env) } + let(:env) { {} } + + subject { strategy.token_params } + + before { allow(strategy).to receive(:request).and_return request } + + it { is_expected.to be_a OmniAuth::Strategy::Options } + it 'has default resource' do + expect(subject.resource).to eq '00000002-0000-0000-c000-000000000000' + end + + context 'when custom crm url' do + let(:crm_url) { 'https://mydomain.crm.dynamics.com/' } + let(:env) { { 'omniauth.params' => { 'azure_resource' => crm_url } } } + + it 'has resource from url params' do + expect(subject.resource).to eq crm_url + end + end + end +end diff --git a/vendor/gems/omniauth-azure-oauth2/spec/spec_helper.rb b/vendor/gems/omniauth-azure-oauth2/spec/spec_helper.rb new file mode 100644 index 00000000000..9d0890421a2 --- /dev/null +++ b/vendor/gems/omniauth-azure-oauth2/spec/spec_helper.rb @@ -0,0 +1,2 @@ +require File.join('bundler', 'setup') +require 'rspec'
\ No newline at end of file diff --git a/vendor/gems/omniauth_crowd/.gitlab-ci.yml b/vendor/gems/omniauth_crowd/.gitlab-ci.yml index 1ad51409803..08a5da1a3d1 100644 --- a/vendor/gems/omniauth_crowd/.gitlab-ci.yml +++ b/vendor/gems/omniauth_crowd/.gitlab-ci.yml @@ -4,7 +4,7 @@ workflow: .rspec: cache: - key: omniauth-gitlab-ruby + key: omniauth_crowd paths: - vendor/gems/omniauth_crowd/vendor/ruby before_script: diff --git a/workhorse/go.mod b/workhorse/go.mod index 18dba215038..e2191c1a749 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -129,6 +129,7 @@ exclude ( // CVE-2021-42576 github.com/microcosm-cc/bluemonday v1.0.2 + github.com/nats-io/jwt v0.3.0 // CVE-2020-26892 github.com/nats-io/nats-server/v2 v2.1.2 diff --git a/workhorse/go.sum b/workhorse/go.sum index e98e18e96e5..d143f62f812 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -873,7 +873,6 @@ github.com/montanaflynn/stats v0.0.0-20151014174947-eeaced052adb/go.mod h1:wL8QJ github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= diff --git a/yarn.lock b/yarn.lock index 137b5fdc073..c143f007aec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2488,7 +2488,7 @@ acorn-walk@^8.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3" integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A== -acorn@^6.3.0, acorn@^6.4.1: +acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== |