New upstream version 12.6.8

This commit is contained in:
Pirate Praveen 2020-03-07 23:17:34 +05:30
parent e5f713a365
commit b4a1e844b3
69 changed files with 1111 additions and 192 deletions

View file

@ -200,6 +200,8 @@
- name: postgres:9.6 - name: postgres:9.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg10: .use-pg10:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
@ -207,6 +209,8 @@
- name: postgres:10.9 - name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg9-ee: .use-pg9-ee:
services: services:
@ -214,6 +218,8 @@
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
- name: elasticsearch:5.6.12 - name: elasticsearch:5.6.12
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg10-ee: .use-pg10-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
@ -222,6 +228,8 @@
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
- name: elasticsearch:5.6.12 - name: elasticsearch:5.6.12
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.only-ee: .only-ee:
only: only:

View file

@ -1,60 +1,57 @@
<!-- <!--
# Read me first! # Read me first!
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq Create this issue under https://gitlab.com/gitlab-org/security
Set the title to: `Description of the original issue` Set the title to: `Description of the original issue`
--> -->
### Prior to starting the security release work ## Prior to starting the security release work
- [ ] Read the [security process for developers] if you are not familiar with it. - [ ] Read the [security process for developers] if you are not familiar with it.
- [ ] Link to the original issue adding it to the [links section](#links) - [ ] Mark this [issue as related] to the Security Release tracking issue. You can find it on the topic of the `#releases` Slack channel.
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org` - [ ] Run `scripts/security-harness` in your local repository to prevent accidentally pushing to any remote besides `gitlab.com/gitlab-org/security`.
- [ ] Create a new branch prefixing it with `security-` - Fill out the [Links section](#links):
- [ ] Create a MR targeting `dev.gitlab.org` `master` - [ ] Next to **Issue on GitLab**, add a link to the `gitlab-org/gitlab` issue that describes the security vulnerability.
- [ ] Add a link to this issue in the original security issue on `gitlab.com`. - [ ] Next to **Security Release tracking issue**, add a link to the security release issue that will include this security issue.
#### Backports ## Development
- [ ] Create a new branch prefixing it with `security-`.
- [ ] Create a merge request targeting `master` on `gitlab.com/gitlab-org/security` and use the [Security Release merge request template].
- [ ] Follow the same [code review process]: Assign to a reviewer, then to a maintainer.
After your merge request has being approved according to our [approval guidelines], you're ready to prepare the backports
## Backports
- [ ] Once the MR is ready to be merged, create MRs targeting the latest 3 stable branches - [ ] Once the MR is ready to be merged, create MRs targeting the latest 3 stable branches
- [ ] At this point, it might be easy to squash the commits from the MR into one * At this point, it might be easy to squash the commits from the MR into one
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation] * You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation]
- [ ] Create each MR targeting the stable branch `X-Y-stable`, using the "Security Release" merge request template. - [ ] Create each MR targeting the stable branch `X-Y-stable`, using the [Security Release merge request template].
- Every merge request will have its own set of TODOs, so make sure to * Every merge request will have its own set of TODOs, so make sure to complete those.
complete those. - [ ] On the "Related merge requests" section, ensure all MRs are linked to this issue.
- [ ] Make sure all MRs have a link in the [links section](#links) * This section should only list the merge requests created for this issue: One targeting `master` and the 3 backports.
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script ## Documentation and final details
#### Documentation and final details - [ ] Ensure the [Links section](#links) is completed.
- [ ] Check the topic on #releases to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Add links to this issue and your MRs in the description of the security release issue
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details) - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details) - [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
- [ ] Once your `master` MR is merged, comment on the original security issue with a link to that MR indicating the issue is fixed. - [ ] Once your `master` MR is merged, comment on the original security issue with a link to that MR indicating the issue is fixed.
### Summary ## Summary
#### Links ### Links
| Description | Link | | Description | Link |
| -------- | -------- | | -------- | -------- |
| Original issue | #TODO | | Issue on [GitLab](https://gitlab.com/gitlab-org/gitlab/issues) | #TODO |
| Security release issue | #TODO | | Security Release tracking issue | #TODO |
| `master` MR | !TODO |
| `master` MR (EE) | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
#### Details ### Details
| Description | Details | Further details| | Description | Details | Further details|
| -------- | -------- | -------- | | -------- | -------- | -------- |
@ -65,6 +62,10 @@ Set the title to: `Description of the original issue`
| Thanks | | | | Thanks | | |
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md [security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[RM list]: https://about.gitlab.com/release-managers/ [secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
[security Release merge request template]: https://gitlab.com/gitlab-org/security/gitlab/blob/master/.gitlab/merge_request_templates/Security%20Release.md
[code review process]: https://docs.gitlab.com/ee/development/code_review.html
[approval guidelines]: https://docs.gitlab.com/ee/development/code_review.html#approval-guidelines
[issue as related]: https://docs.gitlab.com/ee/user/project/issues/related_issues.html#adding-a-related-issue
/label ~security /label ~security

View file

@ -1,35 +1,37 @@
<!-- <!--
# README first! # README first!
This MR should be created on `dev.gitlab.org`. This MR should be created on `gitlab.com/gitlab-org/security/gitlab`.
See [the general developer security release guidelines](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md). See [the general developer security release guidelines](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md).
This merge request _must not_ close the corresponding security issue _unless_ it
targets master.
When submitting a merge request for CE, a corresponding EE merge request is
always required. This makes it easier to merge security merge requests, as
manually merging CE into EE is no longer required.
--> -->
## Related issues ## Related issues
<!-- Mention the issue(s) this MR is related to --> <!-- Mention the GitLab Security issue this MR is related to -->
## Developer checklist ## Developer checklist
- [ ] Link to the developer security workflow issue on `dev.gitlab.org` - [ ] **Make sure this merge request mentions the [GitLab Security] issue it belongs to (i.e. `Related to <issue_id>`).**
- [ ] MR targets `master`, or `X-Y-stable` for backports - [ ] Merge request targets `master`, or `X-Y-stable` for backports.
- [ ] Milestone is set for the version this MR applies to - [ ] Milestone is set for the version this merge request applies to. A closed milestone can be assigned via [quick actions].
- [ ] Title of this MR is the same as for all backports - [ ] Title of this merge request is the same as for all backports.
- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security` - [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security`
- [ ] Add a link to this MR in the `links` section of related issue - [ ] Assign to a reviewer and maintainer, per our [Code Review process].
- [ ] Set up an EE MR (always required for CE merge requests): EE_MR_LINK_HERE - [ ] For the MR targeting `master`:
- [ ] Assign to a reviewer (that is not a release manager) - [ ] Ping appsec team member who created the issue and ask for a non-blocking review with `Please review this MR`.
- [ ] Ensure it's approved according to our [Approval Guidelines].
- [ ] Merge request _must not_ close the corresponding security issue, _unless_ it targets `master`.
## Reviewer checklist **Note:** Reviewer/maintainer should not be a Release Manager
## Maintainer checklist
- [ ] Correct milestone is applied and the title is matching across all backports - [ ] Correct milestone is applied and the title is matching across all backports
- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines - [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines
/label ~security /label ~security
[GitLab Security]: https://gitlab.com/gitlab-org/security/gitlab
[approval guidelines]: https://docs.gitlab.com/ee/development/code_review.html#approval-guidelines
[Code Review process]: https://docs.gitlab.com/ee/development/code_review.html
[quick actions]: https://docs.gitlab.com/ee/user/project/quick_actions.html#quick-actions-for-issues-merge-requests-and-epics

View file

@ -1,5 +1,9 @@
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 12.6.7
- No changes.
## 12.6.6 ## 12.6.6
- No changes. - No changes.

View file

@ -2,6 +2,28 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 12.6.8
### Security (16 changes)
- Respect member access level for group shares.
- Prevent an endless checking loop for two merge requests targeting each other.
- Update user 2fa when accepting a group invite.
- Fix for XSS in branch names.
- Prevent directory traversal through FileUploader.
- Run project badge images through the asset proxy.
- Check merge requests read permissions before showing them in the pipeline widget.
- Update container registry authentication to account for login request when checking permissions.
- Remove OID filtering during LFS imports.
- Protect against denial of service using pipeline webhook recursion.
- Expire account confirmation token.
- Prevent XSS in admin grafana URL setting.
- Don't require base_sha in DiffRefsType.
- Sanitize output by dependency linkers.
- Recalculate ProjectAuthorizations for all users.
- Escape special chars in Sentry error header.
## 12.6.7 ## 12.6.7
### Security (1 change) ### Security (1 change)

View file

@ -1 +1 @@
1.77.1 12.6.8

View file

@ -1 +1 @@
12.6.7 12.6.8

View file

@ -1,7 +1,7 @@
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { GlFormInput, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { __, sprintf, n__ } from '~/locale'; import { __, sprintf, n__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
@ -17,6 +17,7 @@ export default {
GlFormInput, GlFormInput,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlSprintf,
TooltipOnTruncate, TooltipOnTruncate,
Icon, Icon,
Stacktrace, Stacktrace,
@ -51,16 +52,6 @@ export default {
computed: { computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']), ...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
...mapGetters('details', ['stacktrace']), ...mapGetters('details', ['stacktrace']),
reported() {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
},
firstReleaseLink() { firstReleaseLink() {
return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`; return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`;
}, },
@ -120,7 +111,16 @@ export default {
</div> </div>
<div v-else-if="showDetails" class="error-details"> <div v-else-if="showDetails" class="error-details">
<div class="top-area align-items-center justify-content-between py-3"> <div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> <div v-if="!loadingStacktrace && stacktrace" data-qa-selector="reported_text">
<gl-sprintf :message="__('Reported %{timeAgo} by %{reportedBy}')">
<template #reportedBy>
<strong>{{ error.culprit }}</strong>
</template>
<template #timeAgo>
{{ timeFormatted(stacktraceData.date_received) }}
</template>
</gl-sprintf>
</div>
<form ref="sentryIssueForm" :action="projectIssuesPath" method="POST"> <form ref="sentryIssueForm" :action="projectIssuesPath" method="POST">
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" /> <gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" /> <input name="issue[description]" :value="issueDescription" type="hidden" />

View file

@ -1,5 +1,6 @@
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import simplePoll from '../../../lib/utils/simple_poll'; import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
@ -44,11 +45,10 @@ export default {
fastForwardMergeText() { fastForwardMergeText() {
return sprintf( return sprintf(
__( __(
`Fast-forward merge is not possible. Rebase the source branch onto %{startTag}${this.mr.targetBranch}%{endTag} to allow this merge request to be merged.`, 'Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged.',
), ),
{ {
startTag: '<span class="label-branch">', targetBranch: `<span class="label-branch">${escape(this.mr.targetBranch)}</span>`,
endTag: '</span>',
}, },
false, false,
); );

View file

@ -9,6 +9,7 @@ module UploadsActions
included do included do
prepend_before_action :set_request_format_from_path_extension prepend_before_action :set_request_format_from_path_extension
rescue_from FileUploader::InvalidSecret, with: :render_404
end end
def create def create

View file

@ -8,7 +8,7 @@ module Types
field :head_sha, GraphQL::STRING_TYPE, null: false, field :head_sha, GraphQL::STRING_TYPE, null: false,
description: 'SHA of the HEAD at the time the comment was made' description: 'SHA of the HEAD at the time the comment was made'
field :base_sha, GraphQL::STRING_TYPE, null: false, field :base_sha, GraphQL::STRING_TYPE, null: true,
description: 'Merge base of the branch the comment was made on' description: 'Merge base of the branch the comment was made on'
field :start_sha, GraphQL::STRING_TYPE, null: false, field :start_sha, GraphQL::STRING_TYPE, null: false,
description: 'SHA of the branch being compared against' description: 'SHA of the branch being compared against'

View file

@ -6,6 +6,9 @@ class ApplicationSetting < ApplicationRecord
include TokenAuthenticatable include TokenAuthenticatable
include ChronicDurationAttribute include ChronicDurationAttribute
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required } add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token add_authentication_token_field :static_objects_external_storage_auth_token
@ -35,6 +38,14 @@ class ApplicationSetting < ApplicationRecord
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
validates :grafana_url,
system_hook_url: {
blocked_message: "is blocked: %{exception_message}. " + GRAFANA_URL_ERROR_MESSAGE
},
if: :grafana_url_absolute?
validate :validate_grafana_url
validates :uuid, presence: true validates :uuid, presence: true
validates :outbound_local_requests_whitelist, validates :outbound_local_requests_whitelist,
@ -345,6 +356,19 @@ class ApplicationSetting < ApplicationRecord
end end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') } after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def validate_grafana_url
unless parsed_grafana_url
self.errors.add(
:grafana_url,
"must be a valid relative or absolute URL. #{GRAFANA_URL_ERROR_MESSAGE}"
)
end
end
def grafana_url_absolute?
parsed_grafana_url&.absolute?
end
def sourcegraph_url_is_com? def sourcegraph_url_is_com?
!!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/) !!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/)
end end
@ -369,6 +393,12 @@ class ApplicationSetting < ApplicationRecord
def recaptcha_or_login_protection_enabled def recaptcha_or_login_protection_enabled
recaptcha_enabled || login_recaptcha_protection_enabled recaptcha_enabled || login_recaptcha_protection_enabled
end end
private
def parsed_grafana_url
@parsed_grafana_url ||= Gitlab::Utils.parse_url(grafana_url)
end
end end
ApplicationSetting.prepend_if_ee('EE::ApplicationSetting') ApplicationSetting.prepend_if_ee('EE::ApplicationSetting')

View file

@ -32,7 +32,9 @@ class Badge < ApplicationRecord
end end
def rendered_image_url(project = nil) def rendered_image_url(project = nil)
Gitlab::AssetProxy.proxy_url(
build_rendered_url(image_url, project) build_rendered_url(image_url, project)
)
end end
private private

View file

@ -497,18 +497,29 @@ class Group < Namespace
group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids) group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids)
cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query) cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
cte_alias = cte.table.alias(GroupGroupLink.table_name)
link = GroupGroupLink link = GroupGroupLink
.with(cte.to_arel) .with(cte.to_arel)
.select(smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
'group_access'))
.from([group_member_table, cte.alias_to(group_group_link_table)]) .from([group_member_table, cte.alias_to(group_group_link_table)])
.where(group_member_table[:user_id].eq(user.id)) .where(group_member_table[:user_id].eq(user.id))
.where(group_member_table[:requested_at].eq(nil))
.where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id])) .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
.where(group_member_table[:source_type].eq('Namespace'))
.reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access])) .reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access]))
.first .first
link&.group_access link&.group_access
end end
def smallest_value_arel(args, column_alias)
Arel::Nodes::As.new(
Arel::Nodes::NamedFunction.new('LEAST', args),
Arel::Nodes::SqlLiteral.new(column_alias))
end
def self.groups_including_descendants_by(group_ids) def self.groups_including_descendants_by(group_ids)
Gitlab::ObjectHierarchy Gitlab::ObjectHierarchy
.new(Group.where(id: group_ids)) .new(Group.where(id: group_ids))

View file

@ -66,6 +66,7 @@ class GroupMember < Member
def after_accept_invite def after_accept_invite
notification_service.accept_group_invite(self) notification_service.accept_group_invite(self)
update_two_factor_requirement
super super
end end

View file

@ -128,7 +128,11 @@ module Ci
def all_related_merge_requests def all_related_merge_requests
strong_memoize(:all_related_merge_requests) do strong_memoize(:all_related_merge_requests) do
pipeline.ref ? pipeline.all_merge_requests_by_recency.to_a : [] if pipeline.ref && can?(current_user, :read_merge_request, pipeline.project)
pipeline.all_merge_requests_by_recency.to_a
else
[]
end
end end
end end
end end

View file

@ -3,12 +3,24 @@
module Auth module Auth
class ContainerRegistryAuthenticationService < BaseService class ContainerRegistryAuthenticationService < BaseService
AUDIENCE = 'container_registry' AUDIENCE = 'container_registry'
REGISTRY_LOGIN_ABILITIES = [
:read_container_image,
:create_container_image,
:destroy_container_image,
:update_container_image,
:admin_container_image,
:build_read_container_image,
:build_create_container_image,
:build_destroy_container_image
].freeze
def execute(authentication_abilities:) def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities @authentication_abilities = authentication_abilities
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
return error('DENIED', status: 403, message: 'access forbidden') unless has_registry_ability?
unless scopes.any? || current_user || project unless scopes.any? || current_user || project
return error('DENIED', status: 403, message: 'access forbidden') return error('DENIED', status: 403, message: 'access forbidden')
end end
@ -197,5 +209,11 @@ module Auth
def has_authentication_ability?(capability) def has_authentication_ability?(capability)
@authentication_abilities.to_a.include?(capability) @authentication_abilities.to_a.include?(capability)
end end
def has_registry_ability?
@authentication_abilities.any? do |ability|
REGISTRY_LOGIN_ABILITIES.include?(ability)
end
end
end end
end end

View file

@ -16,17 +16,14 @@ module Projects
@lfs_download_object = lfs_download_object @lfs_download_object = lfs_download_object
end end
# rubocop: disable CodeReuse/ActiveRecord
def execute def execute
return unless project&.lfs_enabled? && lfs_download_object return unless project&.lfs_enabled? && lfs_download_object
return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid? return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
return if LfsObject.exists?(oid: lfs_oid)
wrap_download_errors do wrap_download_errors do
download_lfs_file! download_lfs_file!
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
private private
@ -39,14 +36,24 @@ module Projects
def download_lfs_file! def download_lfs_file!
with_tmp_file do |tmp_file| with_tmp_file do |tmp_file|
download_and_save_file!(tmp_file) download_and_save_file!(tmp_file)
project.all_lfs_objects << LfsObject.new(oid: lfs_oid,
size: lfs_size, project.lfs_objects << find_or_create_lfs_object(tmp_file)
file: tmp_file)
success success
end end
end end
def find_or_create_lfs_object(tmp_file)
lfs_obj = LfsObject.safe_find_or_create_by!(
oid: lfs_oid,
size: lfs_size
)
lfs_obj.update!(file: tmp_file) unless lfs_obj.file.file
lfs_obj
end
def download_and_save_file!(file) def download_and_save_file!(file)
digester = Digest::SHA256.new digester = Digest::SHA256.new
response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment| response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|

View file

@ -26,12 +26,12 @@ module Projects
return [] return []
end end
# Getting all Lfs pointers already in the database and linking them to the project # Downloading the required information and gathering it inside an
linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys) # LfsDownloadObject for each oid
# Retrieving those oids not present in the database which we need to download #
missing_oids = lfs_pointers_in_repository.except(*linked_oids) LfsDownloadLinkListService
# Downloading the required information and gathering it inside a LfsDownloadObject for each oid .new(project, remote_uri: current_endpoint_uri)
LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids) .execute(lfs_pointers_in_repository)
rescue LfsDownloadLinkListService::DownloadLinksError => e rescue LfsDownloadLinkListService::DownloadLinksError => e
raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}" raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
end end

View file

@ -11,8 +11,14 @@ class WebHookService
end end
end end
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
attr_accessor :hook, :data, :hook_name, :request_options attr_accessor :hook, :data, :hook_name, :request_options
def self.hook_to_event(hook_name)
hook_name.to_s.singularize.titleize
end
def initialize(hook, data, hook_name) def initialize(hook, data, hook_name)
@hook = hook @hook = hook
@data = data @data = data
@ -110,7 +116,7 @@ class WebHookService
@headers ||= begin @headers ||= begin
{ {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'X-Gitlab-Event' => hook_name.singularize.titleize GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name)
}.tap do |hash| }.tap do |hash|
hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present? hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present?
end end

View file

@ -16,6 +16,9 @@ class FileUploader < GitlabUploader
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}.freeze DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}.freeze
VALID_SECRET_PATTERN = %r{\A\h{10,32}\z}.freeze
InvalidSecret = Class.new(StandardError)
after :remove, :prune_store_dir after :remove, :prune_store_dir
@ -153,6 +156,10 @@ class FileUploader < GitlabUploader
def secret def secret
@secret ||= self.class.generate_secret @secret ||= self.class.generate_secret
raise InvalidSecret unless @secret =~ VALID_SECRET_PATTERN
@secret
end end
# return a new uploader with a file copy on another project # return a new uploader with a file copy on another project

View file

@ -23,7 +23,8 @@
# protect against Server-side Request Forgery (SSRF), or check for the right port. # protect against Server-side Request Forgery (SSRF), or check for the right port.
# #
# Configuration options: # Configuration options:
# * <tt>message</tt> - A custom error message (default is: "must be a valid URL"). # * <tt>message</tt> - A custom error message, used when the URL is blank. (default is: "must be a valid URL").
# * <tt>blocked_message</tt> - A custom error message, used when the URL is blocked. Default: +'is blocked: %{exception_message}'+.
# * <tt>schemes</tt> - Array of URI schemes. Default: +['http', 'https']+ # * <tt>schemes</tt> - Array of URI schemes. Default: +['http', 'https']+
# * <tt>allow_localhost</tt> - Allow urls pointing to +localhost+. Default: +true+ # * <tt>allow_localhost</tt> - Allow urls pointing to +localhost+. Default: +true+
# * <tt>allow_local_network</tt> - Allow urls pointing to private network addresses. Default: +true+ # * <tt>allow_local_network</tt> - Allow urls pointing to private network addresses. Default: +true+
@ -59,7 +60,8 @@ class AddressableUrlValidator < ActiveModel::EachValidator
}.freeze }.freeze
DEFAULT_OPTIONS = BLOCKER_VALIDATE_OPTIONS.merge({ DEFAULT_OPTIONS = BLOCKER_VALIDATE_OPTIONS.merge({
message: 'must be a valid URL' message: 'must be a valid URL',
blocked_message: 'is blocked: %{exception_message}'
}).freeze }).freeze
def initialize(options) def initialize(options)
@ -80,7 +82,7 @@ class AddressableUrlValidator < ActiveModel::EachValidator
Gitlab::UrlBlocker.validate!(value, blocker_args) Gitlab::UrlBlocker.validate!(value, blocker_args)
rescue Gitlab::UrlBlocker::BlockedUrlError => e rescue Gitlab::UrlBlocker::BlockedUrlError => e
record.errors.add(attribute, "is blocked: #{e.message}") record.errors.add(attribute, options.fetch(:blocked_message) % { exception_message: e.message })
end end
private private

View file

@ -1,4 +1,4 @@
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f| = form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting) = form_errors(@application_setting)
%fieldset %fieldset

View file

@ -83,7 +83,7 @@
= _('Requests Profiles') = _('Requests Profiles')
- if Gitlab::CurrentSettings.current_application_settings.grafana_enabled? - if Gitlab::CurrentSettings.current_application_settings.grafana_enabled?
= nav_link do = nav_link do
= link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard') do = link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard'), rel: 'noopener noreferrer' do
%span %span
= _('Metrics Dashboard') = _('Metrics Dashboard')
= render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar' = render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar'

View file

@ -8,7 +8,9 @@
.form-group.row.d-flex.gl-pl-3.gl-pr-3.branch-selector .form-group.row.d-flex.gl-pl-3.gl-pr-3.branch-selector
.align-self-center .align-self-center
%span= s_('From %{source_title} into').html_safe % { source_title: "<code>#{source_title}</code>".html_safe } %span
= _('From <code>%{source_title}</code> into').html_safe % { source_title: source_title }
- if issuable.new_record? - if issuable.new_record?
%code= target_title %code= target_title
&nbsp; &nbsp;

View file

@ -80,8 +80,16 @@ Devise.setup do |config|
# When allow_unconfirmed_access_for is zero, the user won't be able to sign in without confirming. # When allow_unconfirmed_access_for is zero, the user won't be able to sign in without confirming.
# You can use this to let your user access some features of your application # You can use this to let your user access some features of your application
# without confirming the account, but blocking it after a certain period # without confirming the account, but blocking it after a certain period
# (ie 2 days). # (e.g. 3 days).
config.allow_unconfirmed_access_for = 30.days config.allow_unconfirmed_access_for = 3.days
# A period that the user is allowed to confirm their account before their
# token becomes invalid. For example, if set to 1.day, the user can confirm
# their account within 1 days after the mail was sent, but on the second day
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
config.confirm_within = 1.day
# Defines which key will be used when confirming an account # Defines which key will be used when confirming an account
# config.confirmation_keys = [ :email ] # config.confirmation_keys = [ :email ]

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class CleanGrafanaUrl < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
execute(
<<-SQL
UPDATE
application_settings
SET
grafana_url = default
WHERE
position('javascript:' IN btrim(application_settings.grafana_url)) = 1
SQL
)
end
def down
# no-op
end
end

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
class ScheduleRecalculateProjectAuthorizationsSecondRun < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'RecalculateProjectAuthorizationsWithMinMaxUserId'
BATCH_SIZE = 2_500
DELAY_INTERVAL = 2.minutes.to_i
disable_ddl_transaction!
class User < ActiveRecord::Base
include ::EachBatch
self.table_name = 'users'
end
def up
say "Scheduling #{MIGRATION} jobs"
User.each_batch(of: BATCH_SIZE) do |batch, index|
delay = index * DELAY_INTERVAL
range = batch.pluck('MIN(id)', 'MAX(id)').first
BackgroundMigrationWorker.perform_in(delay, MIGRATION, range)
end
end
def down
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_02_04_113223) do ActiveRecord::Schema.define(version: 2020_02_14_085940) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"

View file

@ -113,8 +113,9 @@ If you have set up Grafana, you can enable a link to access it easily from the s
and expand "Metrics - Grafana". and expand "Metrics - Grafana".
1. Check the "Enable access to Grafana" checkbox. 1. Check the "Enable access to Grafana" checkbox.
1. If Grafana is enabled through Omnibus GitLab and on the same server, 1. If Grafana is enabled through Omnibus GitLab and on the same server,
leave "Grafana URL" unchanged. In any other case, enter the full URL leave **Grafana URL** unchanged. It should be `/-/grafana`.
path of the Grafana instance.
In any other case, enter the full URL of the Grafana instance.
1. Click **Save changes**. 1. Click **Save changes**.
1. The new link will be available in the admin area under **Monitoring > Metrics Dashboard**. 1. The new link will be available in the admin area under **Monitoring > Metrics Dashboard**.

View file

@ -1124,7 +1124,7 @@ type DiffRefs {
""" """
Merge base of the branch the comment was made on Merge base of the branch the comment was made on
""" """
baseSha: String! baseSha: String
""" """
SHA of the HEAD at the time the comment was made SHA of the HEAD at the time the comment was made

View file

@ -7118,13 +7118,9 @@
], ],
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
"ofType": null "ofType": null
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null

View file

@ -195,7 +195,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `headSha` | String! | SHA of the HEAD at the time the comment was made | | `headSha` | String! | SHA of the HEAD at the time the comment was made |
| `baseSha` | String! | Merge base of the branch the comment was made on | | `baseSha` | String | Merge base of the branch the comment was made on |
| `startSha` | String! | SHA of the branch being compared against | | `startSha` | String! | SHA of the branch being compared against |
### Discussion ### Discussion

View file

@ -4,6 +4,8 @@ module API
class Triggers < Grape::API class Triggers < Grape::API
include PaginationParams include PaginationParams
HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
@ -19,6 +21,8 @@ module API
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42283') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42283')
forbidden! if gitlab_pipeline_hook_request?
# validate variables # validate variables
params[:variables] = params[:variables].to_h params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) } unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
@ -128,5 +132,11 @@ module API
destroy_conditionally!(trigger) destroy_conditionally!(trigger)
end end
end end
helpers do
def gitlab_pipeline_hook_request?
request.get_header(HTTP_GITLAB_EVENT_HEADER) == WebHookService.hook_to_event(:pipeline_hooks)
end
end
end end
end end

33
lib/gitlab/asset_proxy.rb Normal file
View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
# This is based on https://github.com/jch/html-pipeline/blob/v2.12.2/lib/html/pipeline/camo_filter.rb
# and Banzai::Filter::AssetProxyFilter which we use to proxy images in Markdown
module Gitlab
module AssetProxy
class << self
def proxy_url(url)
return url unless Gitlab.config.asset_proxy.enabled
return url if asset_host_whitelisted?(url)
"#{Gitlab.config.asset_proxy.url}/#{asset_url_hash(url)}/#{hexencode(url)}"
end
private
def asset_host_whitelisted?(url)
parsed_url = URI.parse(url)
Gitlab.config.asset_proxy.domain_regexp&.match?(parsed_url.host)
end
def asset_url_hash(url)
OpenSSL::HMAC.hexdigest('sha1', Gitlab.config.asset_proxy.secret_key, url)
end
def hexencode(str)
str.unpack1('H*')
end
end
end
end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop:disable Style/Documentation
class RecalculateProjectAuthorizationsWithMinMaxUserId
def perform(min_user_id, max_user_id)
User.where(id: min_user_id..max_user_id).find_each do |user|
service = Users::RefreshAuthorizedProjectsService.new(
user,
incorrect_auth_found_callback:
->(project_id, access_level) do
logger.info(message: 'Removing ProjectAuthorizations',
user_id: user.id,
project_id: project_id,
access_level: access_level)
end,
missing_auth_found_callback:
->(project_id, access_level) do
logger.info(message: 'Creating ProjectAuthorizations',
user_id: user.id,
project_id: project_id,
access_level: access_level)
end
)
service.execute
end
end
private
def logger
@logger ||= Gitlab::BackgroundMigration::Logger.build
end
end
end
end

View file

@ -7,6 +7,8 @@ module Gitlab
GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze
REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
include ActionView::Helpers::SanitizeHelper
class_attribute :file_type class_attribute :file_type
def self.support?(blob_name) def self.support?(blob_name)
@ -62,7 +64,10 @@ module Gitlab
end end
def link_tag(name, url) def link_tag(name, url)
%{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}.html_safe sanitize(
%{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>},
attributes: %w[href rel target]
)
end end
# Links package names based on regex. # Links package names based on regex.

View file

@ -62,13 +62,18 @@ module Gitlab
cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte) cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte)
members = Member.arel_table members = Member.arel_table
namespaces = Namespace.arel_table namespaces = Namespace.arel_table
group_group_links = GroupGroupLink.arel_table
# Namespaces the user is a member of. # Namespaces the user is a member of.
cte << user.groups cte << user.groups
.select([namespaces[:id], members[:access_level]]) .select([namespaces[:id], members[:access_level]])
.except(:order) .except(:order)
cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level']) # Namespaces shared with any of the group
cte << Group.select([namespaces[:id],
least(members[:access_level],
group_group_links[:group_access],
'access_level')])
.joins(join_group_group_links) .joins(join_group_group_links)
.joins(join_members_on_group_group_links) .joins(join_members_on_group_group_links)

View file

@ -67,7 +67,13 @@ module Gitlab
return false unless can_access_git? return false unless can_access_git?
return false unless project return false unless project
# Checking for an internal project to prevent an infinite loop:
# https://gitlab.com/gitlab-org/gitlab/issues/36805
if project.internal?
return false unless user.can?(:push_code, project)
else
return false if !user.can?(:push_code, project) && !project.branch_allows_collaboration?(user, ref) return false if !user.can?(:push_code, project) && !project.branch_allows_collaboration?(user, ref)
end
if protected?(ProtectedBranch, project, ref) if protected?(ProtectedBranch, project, ref)
protected_branch_accessible_to?(ref, action: :push) protected_branch_accessible_to?(ref, action: :push)

View file

@ -130,5 +130,14 @@ module Gitlab
IPAddr.new(str) IPAddr.new(str)
rescue IPAddr::InvalidAddressError rescue IPAddr::InvalidAddressError
end end
# Converts a string to an Addressable::URI object.
# If the string is not a valid URI, it returns nil.
# Param uri_string should be a String object.
# This method returns an Addressable::URI object or nil.
def parse_url(uri_string)
Addressable::URI.parse(uri_string)
rescue Addressable::URI::InvalidURIError, TypeError
end
end end
end end

View file

@ -7562,6 +7562,9 @@ msgstr ""
msgid "Failure" msgid "Failure"
msgstr "" msgstr ""
msgid "Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged."
msgstr ""
msgid "Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged." msgid "Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged."
msgstr "" msgstr ""
@ -7987,7 +7990,7 @@ msgstr ""
msgid "From %{providerTitle}" msgid "From %{providerTitle}"
msgstr "" msgstr ""
msgid "From %{source_title} into" msgid "From <code>%{source_title}</code> into"
msgstr "" msgstr ""
msgid "From Bitbucket" msgid "From Bitbucket"

View file

@ -242,8 +242,10 @@ describe SnippetsController do
context 'when the snippet description contains a file' do context 'when the snippet description contains a file' do
include FileMoverHelpers include FileMoverHelpers
let(:picture_file) { "/-/system/user/#{user.id}/secret56/picture.jpg" } let(:picture_secret) { SecureRandom.hex }
let(:text_file) { "/-/system/user/#{user.id}/secret78/text.txt" } let(:text_secret) { SecureRandom.hex }
let(:picture_file) { "/-/system/user/#{user.id}/#{picture_secret}/picture.jpg" }
let(:text_file) { "/-/system/user/#{user.id}/#{text_secret}/text.txt" }
let(:description) do let(:description) do
"Description with picture: ![picture](/uploads#{picture_file}) and "\ "Description with picture: ![picture](/uploads#{picture_file}) and "\
"text: [text.txt](/uploads#{text_file})" "text: [text.txt](/uploads#{text_file})"
@ -266,8 +268,8 @@ describe SnippetsController do
snippet = subject snippet = subject
expected_description = "Description with picture: "\ expected_description = "Description with picture: "\
"![picture](/uploads/-/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\ "![picture](/uploads/-/system/personal_snippet/#{snippet.id}/#{picture_secret}/picture.jpg) and "\
"text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/secret78/text.txt)" "text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/#{text_secret}/text.txt)"
expect(snippet.description).to eq(expected_description) expect(snippet.description).to eq(expected_description)
end end

View file

@ -5,9 +5,9 @@ require "spec_helper"
describe "User creates a merge request", :js do describe "User creates a merge request", :js do
include ProjectForksHelper include ProjectForksHelper
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:title) { "Some feature" } let(:title) { "Some feature" }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
@ -38,6 +38,26 @@ describe "User creates a merge request", :js do
end end
end end
context "XSS branch name exists" do
before do
project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", "master")
end
it "doesn't execute the dodgy branch name" do
visit(project_new_merge_request_path(project))
find(".js-source-branch").click
click_link("<img/src='x'/onerror=alert('oops')>")
find(".js-target-branch").click
click_link("feature")
click_button("Compare branches")
expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError)
end
end
context "to a forked project" do context "to a forked project" do
let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) } let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }

View file

@ -1,6 +1,6 @@
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlLoadingIcon, GlLink } from '@gitlab/ui'; import { GlLoadingIcon, GlLink, GlSprintf } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Stacktrace from '~/error_tracking/components/stacktrace.vue'; import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import ErrorDetails from '~/error_tracking/components/error_details.vue'; import ErrorDetails from '~/error_tracking/components/error_details.vue';
@ -16,7 +16,7 @@ describe('ErrorDetails', () => {
function mountComponent() { function mountComponent() {
wrapper = shallowMount(ErrorDetails, { wrapper = shallowMount(ErrorDetails, {
stubs: { LoadingButton }, stubs: { LoadingButton, GlSprintf },
localVue, localVue,
store, store,
propsData: { propsData: {
@ -188,5 +188,28 @@ describe('ErrorDetails', () => {
}); });
}); });
}); });
describe('Escape unsafe chars for culprit field', () => {
const findReportedText = () => wrapper.find('[data-qa-selector="reported_text"]');
const culprit = '<script>console.log("surprise!")</script>';
beforeEach(() => {
store.state.details.loadingStacktrace = false;
store.state.details.loading = false;
store.state.details.error = {
id: 1,
culprit,
};
mountComponent();
});
it('should not convert interpolated text to html entities', () => {
expect(findReportedText().findAll('script').length).toEqual(0);
expect(findReportedText().findAll('strong').length).toEqual(1);
});
it('should render text instead of converting to html entities', () => {
expect(findReportedText().text()).toContain(culprit);
});
});
}); });
}); });

View file

@ -5,5 +5,9 @@ require 'spec_helper'
describe GitlabSchema.types['DiffRefs'] do describe GitlabSchema.types['DiffRefs'] do
it { expect(described_class.graphql_name).to eq('DiffRefs') } it { expect(described_class.graphql_name).to eq('DiffRefs') }
it { expect(described_class).to have_graphql_fields(:base_sha, :head_sha, :start_sha) } it { is_expected.to have_graphql_fields(:head_sha, :base_sha, :start_sha) }
it { expect(described_class.fields['headSha'].type).to be_non_null }
it { expect(described_class.fields['baseSha'].type).not_to be_non_null }
it { expect(described_class.fields['startSha'].type).to be_non_null }
end end

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::AssetProxy do
context 'when asset proxy is disabled' do
before do
stub_asset_proxy_setting(enabled: false)
end
it 'returns the original URL' do
url = 'http://example.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
end
context 'when asset proxy is enabled' do
before do
stub_asset_proxy_setting(whitelist: %w(gitlab.com *.mydomain.com))
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
secret_key: 'shared-secret',
domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist)
)
end
it 'returns a proxied URL' do
url = 'http://example.com/test.png'
proxied_url = 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67'
expect(described_class.proxy_url(url)).to eq(proxied_url)
end
context 'whitelisted domain' do
it 'returns original URL for single domain whitelist' do
url = 'http://gitlab.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
it 'returns original URL for wildcard subdomain whitelist' do
url = 'http://test.mydomain.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
end
end
end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizationsWithMinMaxUserId, :migration, schema: 20200204113224 do
let(:users_table) { table(:users) }
let(:min) { 1 }
let(:max) { 5 }
before do
min.upto(max) do |i|
users_table.create!(id: i, email: "user#{i}@example.com", projects_limit: 10)
end
end
describe '#perform' do
it 'initializes Users::RefreshAuthorizedProjectsService with correct users' do
min.upto(max) do |i|
user = User.find(i)
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).with(user, any_args).and_call_original)
end
described_class.new.perform(min, max)
end
it 'executes Users::RefreshAuthorizedProjectsService' do
expected_call_counts = max - min + 1
service = instance_double(Users::RefreshAuthorizedProjectsService)
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).exactly(expected_call_counts).times.and_return(service))
expect(service).to receive(:execute).exactly(expected_call_counts).times
described_class.new.perform(min, max)
end
end
end

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::DependencyLinker::BaseLinker do
let(:linker_class) do
Class.new(described_class) do
def link_dependencies
link_regex(%r{^(?<name>https?://[^ ]+)}, &:itself)
end
end
end
let(:plain_content) do
<<~CONTENT
http://\\njavascript:alert(1)
https://gitlab.com/gitlab-org/gitlab
CONTENT
end
let(:highlighted_content) do
<<~CONTENT
<span><span>http://</span><span>\\n</span><span>javascript:alert(1)</span></span>
<span><span>https://gitlab.com/gitlab-org/gitlab</span></span>
CONTENT
end
let(:linker) { linker_class.new(plain_content, highlighted_content) }
describe '#link' do
subject { linker.link }
it 'only converts valid links' do
expect(subject).to eq(
<<~CONTENT
<span><span>#{link('http://')}</span><span>#{link('\n', url: '%5Cn')}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span>
<span><span>#{link('https://gitlab.com/gitlab-org/gitlab')}</span></span>
CONTENT
)
end
end
def link(text, url: text)
attrs = [
'rel="nofollow noreferrer noopener"',
'target="_blank"'
]
attrs.unshift(%{href="#{url}"}) if url
%{<a #{attrs.join(' ')}>#{text}</a>}
end
end

View file

@ -109,6 +109,20 @@ describe Gitlab::ProjectAuthorizations do
end end
end end
context 'with lower group access level than max access level for share' do
let(:user) { create(:user) }
it 'creates proper authorizations' do
group.add_reporter(user)
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to eq(Gitlab::Access::REPORTER)
expect(mapping[project_child.id]).to eq(Gitlab::Access::REPORTER)
end
end
context 'parent group user' do context 'parent group user' do
let(:user) { parent_group_user } let(:user) { parent_group_user }

View file

@ -30,6 +30,17 @@ describe Gitlab::UserAccess do
end end
end end
describe 'push to branch in an internal project' do
it 'will not infinitely loop when a project is internal' do
project.visibility_level = Gitlab::VisibilityLevel::INTERNAL
project.save!
expect(project).not_to receive(:branch_allows_collaboration?)
access.can_push_to_branch?('master')
end
end
describe 'push to empty project' do describe 'push to empty project' do
let(:empty_project) { create(:project_empty_repo) } let(:empty_project) { create(:project_empty_repo) }
let(:project_access) { described_class.new(user, project: empty_project) } let(:project_access) { described_class.new(user, project: empty_project) }

View file

@ -252,4 +252,18 @@ describe Gitlab::Utils do
expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124')) expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124'))
end end
end end
describe '.parse_url' do
it 'returns Addressable::URI object' do
expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI)
end
it 'returns nil when URI cannot be parsed' do
expect(described_class.parse_url('://gitlab.com')).to be nil
end
it 'returns nil with invalid parameter' do
expect(described_class.parse_url(1)).to be nil
end
end
end end

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200214085940_clean_grafana_url.rb')
describe CleanGrafanaUrl, :migration do
let(:application_settings_table) { table(:application_settings) }
[
'javascript:alert(window.opener.document.location)',
' javascript:alert(window.opener.document.location)'
].each do |grafana_url|
it "sets grafana_url back to its default value when grafana_url is '#{grafana_url}'" do
application_settings = application_settings_table.create!(grafana_url: grafana_url)
migrate!
expect(application_settings.reload.grafana_url).to eq('/-/grafana')
end
end
['/-/grafana', '/some/relative/url', 'http://localhost:9000'].each do |grafana_url|
it "does not modify grafana_url when grafana_url is '#{grafana_url}'" do
application_settings = application_settings_table.create!(grafana_url: grafana_url)
migrate!
expect(application_settings.reload.grafana_url).to eq(grafana_url)
end
end
context 'when application_settings table has no rows' do
it 'does not fail' do
migrate!
end
end
end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200204113224_schedule_recalculate_project_authorizations_second_run.rb')
describe ScheduleRecalculateProjectAuthorizationsSecondRun, :migration, :sidekiq do
let(:users_table) { table(:users) }
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
1.upto(4) do |i|
users_table.create!(id: i, name: "user#{i}", email: "user#{i}@example.com", projects_limit: 1)
end
end
it 'schedules background migration' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION).to be_scheduled_migration(1, 2)
expect(described_class::MIGRATION).to be_scheduled_migration(3, 4)
end
end
end
end

View file

@ -19,6 +19,7 @@ describe ApplicationSetting do
let(:http) { 'http://example.com' } let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' } let(:https) { 'https://example.com' }
let(:ftp) { 'ftp://example.com' } let(:ftp) { 'ftp://example.com' }
let(:javascript) { 'javascript:alert(window.opener.document.location)' }
it { is_expected.to allow_value(nil).for(:home_page_url) } it { is_expected.to allow_value(nil).for(:home_page_url) }
it { is_expected.to allow_value(http).for(:home_page_url) } it { is_expected.to allow_value(http).for(:home_page_url) }
@ -74,6 +75,53 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value('abc').for(:minimum_password_length) } it { is_expected.not_to allow_value('abc').for(:minimum_password_length) }
it { is_expected.to allow_value(10).for(:minimum_password_length) } it { is_expected.to allow_value(10).for(:minimum_password_length) }
context 'grafana_url validations' do
before do
subject.instance_variable_set(:@parsed_grafana_url, nil)
end
it { is_expected.to allow_value(http).for(:grafana_url) }
it { is_expected.to allow_value(https).for(:grafana_url) }
it { is_expected.not_to allow_value(ftp).for(:grafana_url) }
it { is_expected.not_to allow_value(javascript).for(:grafana_url) }
it { is_expected.to allow_value('/-/grafana').for(:grafana_url) }
it { is_expected.to allow_value('http://localhost:9000').for(:grafana_url) }
context 'when local URLs are not allowed in system hooks' do
before do
stub_application_setting(allow_local_requests_from_system_hooks: false)
end
it { is_expected.not_to allow_value('http://localhost:9000').for(:grafana_url) }
end
context 'with invalid grafana URL' do
it 'adds an error' do
subject.grafana_url = ' ' + http
expect(subject.save).to be false
expect(subject.errors[:grafana_url]).to eq([
'must be a valid relative or absolute URL. ' \
'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
])
end
end
context 'with blocked grafana URL' do
it 'adds an error' do
subject.grafana_url = javascript
expect(subject.save).to be false
expect(subject.errors[:grafana_url]).to eq([
'is blocked: Only allowed schemes are http, https. Please check your ' \
'Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
])
end
end
end
context 'when snowplow is enabled' do context 'when snowplow is enabled' do
before do before do
setting.snowplow_enabled = true setting.snowplow_enabled = true

View file

@ -91,6 +91,22 @@ describe Badge do
let(:method) { :image_url } let(:method) { :image_url }
it_behaves_like 'rendered_links' it_behaves_like 'rendered_links'
context 'when asset proxy is enabled' do
let(:placeholder_url) { 'http://www.example.com/image' }
before do
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
secret_key: 'shared-secret'
)
end
it 'returns a proxied URL' do
expect(badge.rendered_image_url).to start_with('https://assets.example.com')
end
end
end end
end end
end end

View file

@ -562,6 +562,18 @@ describe Group do
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER) expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER) expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
end end
context 'with lower group access level than max access level for share' do
let(:user) { create(:user) }
it 'returns correct access level' do
group.add_reporter(user)
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end
end end
context 'with user in the parent group' do context 'with user in the parent group' do
@ -583,6 +595,33 @@ describe Group do
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end end
end end
context 'unrelated project owner' do
let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) }
let(:user) { unrelated_project.owner }
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'user without accepted access request' do
let!(:user) { create(:user) }
before do
create(:group_member, :developer, :access_request, user: user, group: group)
end
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end end
context 'when feature flag share_group_with_group is disabled' do context 'when feature flag share_group_with_group is disabled' do

View file

@ -65,10 +65,10 @@ describe GroupMember do
end end
describe '#update_two_factor_requirement' do describe '#update_two_factor_requirement' do
let(:user) { build :user }
let(:group_member) { build :group_member, user: user }
it 'is called after creation and deletion' do it 'is called after creation and deletion' do
user = build :user
group_member = build :group_member, user: user
expect(user).to receive(:update_two_factor_requirement) expect(user).to receive(:update_two_factor_requirement)
group_member.save group_member.save
@ -79,6 +79,21 @@ describe GroupMember do
end end
end end
describe '#after_accept_invite' do
it 'calls #update_two_factor_requirement' do
email = 'foo@email.com'
user = build(:user, email: email)
group = create(:group, require_two_factor_authentication: true)
group_member = create(:group_member, group: group, invite_token: '1234', invite_email: email)
expect(user).to receive(:require_two_factor_authentication_from_group).and_call_original
group_member.accept_invite!(user)
expect(user.require_two_factor_authentication_from_group).to be_truthy
end
end
context 'access levels' do context 'access levels' do
context 'with parent group' do context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do it_behaves_like 'inherited access level as a member of entity' do

View file

@ -4766,6 +4766,38 @@ describe Project do
end end
end end
context 'with cross internal project merge requests' do
let(:project) { create(:project, :repository, :internal) }
let(:forked_project) { fork_project(project, nil, repository: true) }
let(:user) { double(:user) }
it "does not endlessly loop for internal projects with MRs to each other", :sidekiq_inline do
allow(user).to receive(:can?).and_return(true, false, true)
allow(user).to receive(:id).and_return(1)
create(
:merge_request,
target_project: project,
target_branch: 'merge-test',
source_project: forked_project,
source_branch: 'merge-test',
allow_collaboration: true
)
create(
:merge_request,
target_project: forked_project,
target_branch: 'merge-test',
source_project: project,
source_branch: 'merge-test',
allow_collaboration: true
)
expect(user).to receive(:can?).at_most(5).times
project.branch_allows_collaboration?(user, "merge-test")
end
end
context 'with cross project merge requests' do context 'with cross project merge requests' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:target_project) { create(:project, :repository) } let(:target_project) { create(:project, :repository) }

View file

@ -6,6 +6,7 @@ describe Ci::PipelinePresenter do
include Gitlab::Routing include Gitlab::Routing
let(:user) { create(:user) } let(:user) { create(:user) }
let(:current_user) { user }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
@ -15,7 +16,7 @@ describe Ci::PipelinePresenter do
before do before do
project.add_developer(user) project.add_developer(user)
allow(presenter).to receive(:current_user) { user } allow(presenter).to receive(:current_user) { current_user }
end end
it 'inherits from Gitlab::View::Presenter::Delegated' do it 'inherits from Gitlab::View::Presenter::Delegated' do
@ -215,10 +216,90 @@ describe Ci::PipelinePresenter do
describe '#all_related_merge_requests' do describe '#all_related_merge_requests' do
it 'memoizes the returned relation' do it 'memoizes the returned relation' do
query_count = ActiveRecord::QueryRecorder.new do query_count = ActiveRecord::QueryRecorder.new do
2.times { presenter.send(:all_related_merge_requests).count } 3.times { presenter.send(:all_related_merge_requests).count }
end.count end.count
expect(query_count).to eq(1) expect(query_count).to eq(2)
end
context 'permissions' do
let!(:merge_request) do
create(:merge_request, project: project, source_project: project)
end
subject(:all_related_merge_requests) do
presenter.send(:all_related_merge_requests)
end
shared_examples 'private merge requests' do
context 'when not logged in' do
let(:current_user) {}
it { is_expected.to be_empty }
end
context 'when logged in as a non_member' do
let(:current_user) { create(:user) }
it { is_expected.to be_empty }
end
context 'when logged in as a guest' do
let(:current_user) { create(:user) }
before do
project.add_guest(current_user)
end
it { is_expected.to be_empty }
end
context 'when logged in as a developer' do
it { is_expected.to contain_exactly(merge_request) }
end
context 'when logged in as a maintainer' do
let(:current_user) { create(:user) }
before do
project.add_maintainer(current_user)
end
it { is_expected.to contain_exactly(merge_request) }
end
end
context 'with a private project' do
it_behaves_like 'private merge requests'
end
context 'with a public project with private merge requests' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project
.project_feature
.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'private merge requests'
end
context 'with a public project with public merge requests' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project
.project_feature
.update!(merge_requests_access_level: ProjectFeature::ENABLED)
end
context 'when not logged in' do
let(:current_user) {}
it { is_expected.to contain_exactly(merge_request) }
end
end
end end
end end

View file

@ -132,6 +132,18 @@ describe API::Triggers do
end end
end end
end end
context 'when is triggered by a pipeline hook' do
it 'does not create a new pipeline' do
expect do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"),
params: { ref: 'refs/heads/other-branch' },
headers: { WebHookService::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
end.not_to change(Ci::Pipeline, :count)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end end
describe 'GET /projects/:id/triggers' do describe 'GET /projects/:id/triggers' do

View file

@ -769,6 +769,15 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when deploy token has read_registry as a scope' do context 'when deploy token has read_registry as a scope' do
let(:current_user) { create(:deploy_token, projects: [project]) } let(:current_user) { create(:deploy_token, projects: [project]) }
shared_examples 'able to login' do
context 'registry provides read_container_image authentication_abilities' do
let(:current_params) { {} }
let(:authentication_abilities) { [:read_container_image] }
it_behaves_like 'an authenticated'
end
end
context 'for public project' do context 'for public project' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
@ -783,6 +792,8 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
it_behaves_like 'able to login'
end end
context 'for internal project' do context 'for internal project' do
@ -799,6 +810,8 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
it_behaves_like 'able to login'
end end
context 'for private project' do context 'for private project' do
@ -815,18 +828,38 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
it_behaves_like 'able to login'
end end
end end
context 'when deploy token does not have read_registry scope' do context 'when deploy token does not have read_registry scope' do
let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) } let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) }
shared_examples 'unable to login' do
context 'registry provides no container authentication_abilities' do
let(:current_params) { {} }
let(:authentication_abilities) { [] }
it_behaves_like 'a forbidden'
end
context 'registry provides inapplicable container authentication_abilities' do
let(:current_params) { {} }
let(:authentication_abilities) { [:download_code] }
it_behaves_like 'a forbidden'
end
end
context 'for public project' do context 'for public project' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
context 'when pulling' do context 'when pulling' do
it_behaves_like 'a pullable' it_behaves_like 'a pullable'
end end
it_behaves_like 'unable to login'
end end
context 'for internal project' do context 'for internal project' do
@ -835,6 +868,8 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when pulling' do context 'when pulling' do
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
it_behaves_like 'unable to login'
end end
context 'for private project' do context 'for private project' do
@ -843,6 +878,15 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when pulling' do context 'when pulling' do
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
context 'when logging in' do
let(:current_params) { {} }
let(:authentication_abilities) { [] }
it_behaves_like 'a forbidden'
end
it_behaves_like 'unable to login'
end end
end end

View file

@ -133,6 +133,21 @@ describe Projects::LfsPointers::LfsDownloadService do
end end
end end
context 'when an lfs object with the same oid already exists' do
let!(:existing_lfs_object) { create(:lfs_object, oid: oid) }
before do
stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'no lfs object is created'
it 'does not update the file attached to the existing LfsObject' do
expect { subject.execute }
.not_to change { existing_lfs_object.reload.file.file.file }
end
end
context 'when credentials present' do context 'when credentials present' do
let(:download_link_with_credentials) { "http://user:password@gitlab.com/#{oid}" } let(:download_link_with_credentials) { "http://user:password@gitlab.com/#{oid}" }
let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) } let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
@ -210,17 +225,5 @@ describe Projects::LfsPointers::LfsDownloadService do
subject.execute subject.execute
end end
end end
context 'when an lfs object with the same oid already exists' do
before do
create(:lfs_object, oid: oid)
end
it 'does not download the file' do
expect(subject).not_to receive(:download_lfs_file!)
subject.execute
end
end
end end
end end

View file

@ -24,7 +24,6 @@ describe Projects::LfsPointers::LfsObjectDownloadListService do
describe '#execute' do describe '#execute' do
context 'when no lfs pointer is linked' do context 'when no lfs pointer is linked' do
before do before do
allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
end end
@ -35,12 +34,6 @@ describe Projects::LfsPointers::LfsObjectDownloadListService do
subject.execute subject.execute
end end
it 'links existent lfs objects to the project' do
expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
subject.execute
end
it 'retrieves the download links of non existent objects' do it 'retrieves the download links of non existent objects' do
expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids) expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
@ -48,32 +41,6 @@ describe Projects::LfsPointers::LfsObjectDownloadListService do
end end
end end
context 'when some lfs objects are linked' do
before do
allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
end
it 'retrieves the download links of non existent objects' do
expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
subject.execute
end
end
context 'when all lfs objects are linked' do
before do
allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
end
it 'retrieves no download links' do
expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
expect(subject.execute).to be_empty
end
end
context 'when lfsconfig file exists' do context 'when lfsconfig file exists' do
before do before do
allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n") allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")

View file

@ -7,6 +7,7 @@ RSpec.shared_context 'GroupPolicy context' do
let_it_be(:maintainer) { create(:user) } let_it_be(:maintainer) { create(:user) }
let_it_be(:owner) { create(:user) } let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
let_it_be(:non_group_member) { create(:user) }
let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) } let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) }
let(:guest_permissions) do let(:guest_permissions) do

View file

@ -69,13 +69,39 @@ shared_examples 'handle uploads' do
end end
describe "GET #show" do describe "GET #show" do
let(:filename) { "rails_sample.jpg" }
let(:upload_service) do
UploadService.new(model, jpg, uploader_class).execute
end
let(:show_upload) do let(:show_upload) do
get :show, params: params.merge(secret: secret, filename: "rails_sample.jpg") get :show, params: params.merge(secret: secret, filename: filename)
end end
before do before do
allow(FileUploader).to receive(:generate_secret).and_return(secret) allow(FileUploader).to receive(:generate_secret).and_return(secret)
UploadService.new(model, jpg, uploader_class).execute upload_service
end
context 'when the secret is invalid' do
let(:secret) { "../../../../../../../../" }
let(:filename) { "Gemfile.lock" }
let(:upload_service) { nil }
it 'responds with status 404' do
show_upload
expect(response).to have_gitlab_http_status(:not_found)
end
it 'is a working exploit without the validation' do
allow_any_instance_of(FileUploader).to receive(:secret) { secret }
show_upload
expect(response).to have_gitlab_http_status(:ok)
end
end end
context 'when accessing a specific upload via different model' do context 'when accessing a specific upload via different model' do

View file

@ -7,7 +7,7 @@ describe FileMover do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:filename) { 'banana_sample.gif' } let(:filename) { 'banana_sample.gif' }
let(:secret) { 'secret55' } let(:secret) { SecureRandom.hex }
let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", secret, filename) } let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", secret, filename) }
let(:temp_description) do let(:temp_description) do
@ -47,8 +47,8 @@ describe FileMover do
subject subject
expect(snippet.reload.description) expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\ .to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ") "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) ")
end end
it 'updates existing upload record' do it 'updates existing upload record' do
@ -75,8 +75,8 @@ describe FileMover do
subject subject
expect(snippet.reload.description) expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\ .to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ") "same ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) ")
end end
it 'does not change the upload record' do it 'does not change the upload record' do
@ -101,8 +101,8 @@ describe FileMover do
subject subject
expect(snippet.reload.description) expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\ .to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ") "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) ")
end end
it 'creates new target upload record an delete the old upload' do it 'creates new target upload record an delete the old upload' do
@ -121,8 +121,8 @@ describe FileMover do
subject subject
expect(snippet.reload.description) expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\ .to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ") "same ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) ")
end end
it 'does not change the upload record' do it 'does not change the upload record' do
@ -138,12 +138,8 @@ describe FileMover do
let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", '..', 'another_subdir_of_temp') } let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", '..', 'another_subdir_of_temp') }
it 'does not trigger move if path is outside designated directory' do it 'does not trigger move if path is outside designated directory' do
stub_file_mover("uploads/-/system/user/#{user.id}/another_subdir_of_temp")
expect(FileUtils).not_to receive(:move) expect(FileUtils).not_to receive(:move)
expect { subject }.to raise_error(FileUploader::InvalidSecret)
subject
expect(snippet.reload.description).to eq(temp_description)
end end
end end

View file

@ -6,7 +6,8 @@ describe FileUploader do
let(:group) { create(:group, name: 'awesome') } let(:group) { create(:group, name: 'awesome') }
let(:project) { create(:project, :legacy_storage, namespace: group, name: 'project') } let(:project) { create(:project, :legacy_storage, namespace: group, name: 'project') }
let(:uploader) { described_class.new(project, :avatar) } let(:uploader) { described_class.new(project, :avatar) }
let(:upload) { double(model: project, path: 'secret/foo.jpg') } let(:upload) { double(model: project, path: "#{secret}/foo.jpg") }
let(:secret) { "55dc16aa0edd05693fd98b5051e83321" } # this would be nicer as SecureRandom.hex, but the shared_examples breaks
subject { uploader } subject { uploader }
@ -14,7 +15,7 @@ describe FileUploader do
include_examples 'builds correct paths', include_examples 'builds correct paths',
store_dir: %r{awesome/project/\h+}, store_dir: %r{awesome/project/\h+},
upload_path: %r{\h+/<filename>}, upload_path: %r{\h+/<filename>},
absolute_path: %r{#{described_class.root}/awesome/project/secret/foo.jpg} absolute_path: %r{#{described_class.root}/awesome/project/55dc16aa0edd05693fd98b5051e83321/foo.jpg}
end end
context 'legacy storage' do context 'legacy storage' do
@ -51,11 +52,11 @@ describe FileUploader do
end end
describe 'initialize' do describe 'initialize' do
let(:uploader) { described_class.new(double, secret: 'secret') } let(:uploader) { described_class.new(double, secret: secret) }
it 'accepts a secret parameter' do it 'accepts a secret parameter' do
expect(described_class).not_to receive(:generate_secret) expect(described_class).not_to receive(:generate_secret)
expect(uploader.secret).to eq('secret') expect(uploader.secret).to eq(secret)
end end
end end
@ -154,8 +155,39 @@ describe FileUploader do
describe '#secret' do describe '#secret' do
it 'generates a secret if none is provided' do it 'generates a secret if none is provided' do
expect(described_class).to receive(:generate_secret).and_return('secret') expect(described_class).to receive(:generate_secret).and_return(secret)
expect(uploader.secret).to eq('secret') expect(uploader.secret).to eq(secret)
expect(uploader.secret.size).to eq(32)
end
context "validation" do
before do
uploader.instance_variable_set(:@secret, secret)
end
context "32-byte hexadecimal" do
let(:secret) { SecureRandom.hex }
it "returns the secret" do
expect(uploader.secret).to eq(secret)
end
end
context "10-byte hexadecimal" do
let(:secret) { SecureRandom.hex(10) }
it "returns the secret" do
expect(uploader.secret).to eq(secret)
end
end
context "invalid secret supplied" do
let(:secret) { "%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fgrafana%2Fconf%2F" }
it "raises an exception" do
expect { uploader.secret }.to raise_error(described_class::InvalidSecret)
end
end
end end
end end

View file

@ -6,12 +6,13 @@ describe PersonalFileUploader do
let(:model) { create(:personal_snippet) } let(:model) { create(:personal_snippet) }
let(:uploader) { described_class.new(model) } let(:uploader) { described_class.new(model) }
let(:upload) { create(:upload, :personal_snippet_upload) } let(:upload) { create(:upload, :personal_snippet_upload) }
let(:secret) { SecureRandom.hex }
subject { uploader } subject { uploader }
shared_examples '#base_dir' do shared_examples '#base_dir' do
before do before do
subject.instance_variable_set(:@secret, 'secret') subject.instance_variable_set(:@secret, secret)
end end
it 'is prefixed with uploads/-/system' do it 'is prefixed with uploads/-/system' do
@ -23,12 +24,12 @@ describe PersonalFileUploader do
shared_examples '#to_h' do shared_examples '#to_h' do
before do before do
subject.instance_variable_set(:@secret, 'secret') subject.instance_variable_set(:@secret, secret)
end end
it 'is correct' do it 'is correct' do
allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name')) allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
expected_url = "/uploads/-/system/personal_snippet/#{model.id}/secret/file_name" expected_url = "/uploads/-/system/personal_snippet/#{model.id}/#{secret}/file_name"
expect(uploader.to_h).to eq( expect(uploader.to_h).to eq(
alt: 'file_name', alt: 'file_name',

View file

@ -5,6 +5,9 @@ require 'spec_helper'
describe AddressableUrlValidator do describe AddressableUrlValidator do
let!(:badge) { build(:badge, link_url: 'http://www.example.com') } let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
let(:validator) { described_class.new(validator_options.reverse_merge(attributes: [:link_url])) }
let(:validator_options) { {} }
subject { validator.validate(badge) } subject { validator.validate(badge) }
include_examples 'url validator examples', described_class::DEFAULT_OPTIONS[:schemes] include_examples 'url validator examples', described_class::DEFAULT_OPTIONS[:schemes]
@ -114,6 +117,19 @@ describe AddressableUrlValidator do
end end
end end
context 'when blocked_message is set' do
let(:message) { 'is not allowed due to: %{exception_message}' }
let(:validator_options) { { blocked_message: message } }
it 'blocks url with provided error message' do
badge.link_url = 'javascript:alert(window.opener.document.location)'
subject
expect(badge.errors.first[1]).to eq 'is not allowed due to: Only allowed schemes are http, https'
end
end
context 'when allow_nil is set to true' do context 'when allow_nil is set to true' do
let(:validator) { described_class.new(attributes: [:link_url], allow_nil: true) } let(:validator) { described_class.new(attributes: [:link_url], allow_nil: true) }