diff options
172 files changed, 2337 insertions, 729 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c65b584e3c1..57e946befb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.5.1 (2018-11-26) + +### Security (17 changes) + +- Escape user fullname while rendering autocomplete template to prevent XSS. +- Fix CRLF vulnerability in Project hooks. +- Fix possible XSS attack in Markdown urls with spaces. +- Redact sensitive information on gitlab-workhorse log. +- Do not follow redirects in Prometheus service when making http requests to the configured api url. +- Don't expose confidential information in commit message list. +- Provide email notification when a user changes their email address. +- Restrict Personal Access Tokens to API scope on web requests. +- Resolve reflected XSS in Ouath authorize window. +- Fix SSRF in project integrations. +- Fixed ability to comment on locked/confidential issues. +- Fixed ability of guest users to edit/delete comments on locked or confidential issues. +- Fix milestone promotion authorization check. +- Configure mermaid to not render HTML content in diagrams. +- Fix a possible symlink time of check to time of use race condition in GitLab Pages. +- Removed ability to see private group names when the group id is entered in the url. +- Fix stored XSS for Environments. + + ## 11.5.0 (2018-11-22) ### Security (10 changes, 1 of them is from the community) @@ -264,6 +287,36 @@ entry. - Disables stop environment button while the deploy is in progress. +## 11.4.8 (2018-11-27) + +### Security (24 changes) + +- Escape entity title while autocomplete template rendering to prevent XSS. !2571 +- Resolve reflected XSS in Ouath authorize window. +- Fix XSS in merge request source branch name. +- Escape user fullname while rendering autocomplete template to prevent XSS. +- Fix CRLF vulnerability in Project hooks. +- Fix possible XSS attack in Markdown urls with spaces. +- Redact sensitive information on gitlab-workhorse log. +- Do not follow redirects in Prometheus service when making http requests to the configured api url. +- Persist only SHA digest of PersonalAccessToken#token. +- Don't expose confidential information in commit message list. +- Provide email notification when a user changes their email address. +- Restrict Personal Access Tokens to API scope on web requests. +- Redact personal tokens in unsubscribe links. +- Fix SSRF in project integrations. +- Fixed ability to comment on locked/confidential issues. +- Fixed ability of guest users to edit/delete comments on locked or confidential issues. +- Fix milestone promotion authorization check. +- Monkey kubeclient to not follow any redirects. +- Configure mermaid to not render HTML content in diagrams. +- Fix a possible symlink time of check to time of use race condition in GitLab Pages. +- Removed ability to see private group names when the group id is entered in the url. +- Fix stored XSS for Environments. +- Prevent SSRF attacks in HipChat integration. +- Validate Wiki attachments are valid temporary files. + + ## 11.4.7 (2018-11-20) - No changes. @@ -544,6 +597,45 @@ entry. - Check frozen string in style builds. (gfyoung) +## 11.3.11 (2018-11-26) + +### Security (33 changes) + +- Filter user sensitive data from discussions JSON. !2537 +- Escape entity title while autocomplete template rendering to prevent XSS. !2557 +- Restrict Personal Access Tokens to API scope on web requests. +- Fix XSS in merge request source branch name. +- Escape user fullname while rendering autocomplete template to prevent XSS. +- Fix CRLF vulnerability in Project hooks. +- Fix possible XSS attack in Markdown urls with spaces. +- Redact sensitive information on gitlab-workhorse log. +- Set timeout for syntax highlighting. +- Do not follow redirects in Prometheus service when making http requests to the configured api url. +- Persist only SHA digest of PersonalAccessToken#token. +- Sanitize JSON data properly to fix XSS on Issue details page. +- Don't expose confidential information in commit message list. +- Markdown API no longer displays confidential title references unless authorized. +- Provide email notification when a user changes their email address. +- Properly filter private references from system notes. +- Redact personal tokens in unsubscribe links. +- Resolve reflected XSS in Ouath authorize window. +- Fix SSRF in project integrations. +- Fix stored XSS in merge requests from imported repository. +- Fixed ability to comment on locked/confidential issues. +- Fixed ability of guest users to edit/delete comments on locked or confidential issues. +- Fix milestone promotion authorization check. +- Monkey kubeclient to not follow any redirects. +- Configure mermaid to not render HTML content in diagrams. +- Redact confidential events in the API. +- Fix xss vulnerability sourced from package.json. +- Fix a possible symlink time of check to time of use race condition in GitLab Pages. +- Removed ability to see private group names when the group id is entered in the url. +- Fix stored XSS for Environments. +- Block loopback addresses in UrlBlocker. +- Prevent SSRF attacks in HipChat integration. +- Validate Wiki attachments are valid temporary files. + + ## 11.3.10 (2018-11-18) ### Security (1 change) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index f0bb29e7638..3a3cd8cc8b0 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.3.0 +1.3.1 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 6da4de57dc6..016dac34bf9 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -8.4.1 +8.4.3 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 21c8c7b46b8..b26a34e4705 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -7.1.1 +7.2.1 @@ -7,6 +7,11 @@ gem_versions = {} gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2' gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10' gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9' + +# The 2.0.6 version of rack requires monkeypatch to be present in +# `config.ru`. This can be removed once a new update for Rack +# is available that contains https://github.com/rack/rack/pull/1201. +gem_versions['rack'] = rails5? ? '2.0.6' : '1.6.11' # --- The end of special code for migrating to Rails 5.0 --- source 'https://rubygems.org' @@ -154,6 +159,8 @@ gem 'icalendar' gem 'diffy', '~> 3.1.0' # Application server +gem 'rack', gem_versions['rack'] + group :unicorn do gem 'unicorn', '~> 5.1.0' gem 'unicorn-worker-killer', '~> 0.4.4' diff --git a/Gemfile.lock b/Gemfile.lock index f622c6292b2..96b453344a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1088,6 +1088,7 @@ DEPENDENCIES pry-rails (~> 0.3.4) puma (~> 3.12) puma_worker_killer + rack (= 2.0.6) rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock index 2542e085815..1289a28b719 100644 --- a/Gemfile.rails4.lock +++ b/Gemfile.rails4.lock @@ -1079,6 +1079,7 @@ DEPENDENCIES pry-rails (~> 0.3.4) puma (~> 3.12) puma_worker_killer + rack (= 1.6.11) rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 3f7a1ef1bfc..0da7ae1b229 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -10,10 +10,10 @@ const Api = { projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', projectLabelsPath: '/:namespace_path/:project_path/labels', - mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', + projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', + projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', + projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', mergeRequestsPath: '/api/:version/merge_requests', - mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', - mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', groupLabelsPath: '/groups/:namespace_path/-/labels', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', @@ -99,36 +99,36 @@ const Api = { }, // Return Merge Request for project - mergeRequest(projectPath, mergeRequestId, params = {}) { - const url = Api.buildUrl(Api.mergeRequestPath) + projectMergeRequest(projectPath, mergeRequestId, params = {}) { + const url = Api.buildUrl(Api.projectMergeRequestPath) .replace(':id', encodeURIComponent(projectPath)) .replace(':mrid', mergeRequestId); return axios.get(url, { params }); }, - mergeRequests(params = {}) { - const url = Api.buildUrl(Api.mergeRequestsPath); - - return axios.get(url, { params }); - }, - - mergeRequestChanges(projectPath, mergeRequestId) { - const url = Api.buildUrl(Api.mergeRequestChangesPath) + projectMergeRequestChanges(projectPath, mergeRequestId) { + const url = Api.buildUrl(Api.projectMergeRequestChangesPath) .replace(':id', encodeURIComponent(projectPath)) .replace(':mrid', mergeRequestId); return axios.get(url); }, - mergeRequestVersions(projectPath, mergeRequestId) { - const url = Api.buildUrl(Api.mergeRequestVersionsPath) + projectMergeRequestVersions(projectPath, mergeRequestId) { + const url = Api.buildUrl(Api.projectMergeRequestVersionsPath) .replace(':id', encodeURIComponent(projectPath)) .replace(':mrid', mergeRequestId); return axios.get(url); }, + mergeRequests(params = {}) { + const url = Api.buildUrl(Api.mergeRequestsPath); + + return axios.get(url, { params }); + }, + newLabel(namespacePath, projectPath, data, callback) { let url; diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index 720f30e18e6..35380ca49fb 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -26,6 +26,9 @@ export default function renderMermaid($els) { }, // mermaidAPI options theme: 'neutral', + flowchart: { + htmlLabels: false, + }, }); $els.each((i, el) => { diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index f0193d8e8ea..13449592e62 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -41,13 +41,13 @@ export default { return Api.project(`${namespace}/${project}`); }, getProjectMergeRequestData(projectId, mergeRequestId, params = {}) { - return Api.mergeRequest(projectId, mergeRequestId, params); + return Api.projectMergeRequest(projectId, mergeRequestId, params); }, getProjectMergeRequestChanges(projectId, mergeRequestId) { - return Api.mergeRequestChanges(projectId, mergeRequestId); + return Api.projectMergeRequestChanges(projectId, mergeRequestId); }, getProjectMergeRequestVersions(projectId, mergeRequestId) { - return Api.mergeRequestVersions(projectId, mergeRequestId); + return Api.projectMergeRequestVersions(projectId, mergeRequestId); }, getBranchData(projectId, currentBranchId) { return Api.branchSingle(projectId, currentBranchId); diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js index 4565c11a83f..8b5f7558654 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -23,13 +23,19 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search } export const receiveMergeRequestsSuccess = ({ commit }, data) => commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); -export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => { +export const fetchMergeRequests = ( + { dispatch, state: { state }, rootState: { currentProjectId } }, + { type, search = '' }, +) => { dispatch('requestMergeRequests'); dispatch('resetMergeRequests'); - const scope = type ? scopes[type] : 'all'; + const scope = type && scopes[type]; + const request = scope + ? Api.mergeRequests({ scope, state, search }) + : Api.projectMergeRequest(currentProjectId, '', { state, search }); - return Api.mergeRequests({ scope, state, search }) + return request .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) .catch(() => dispatch('receiveMergeRequestsError', { type, search })); }; diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue index 2d09cf5760f..f7fbb9503a0 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -128,7 +128,7 @@ export default { }; </script> <template> - <div class="prepend-top-default js-environment-container"> + <div class="prepend-top-default append-bottom-default js-environment-container"> <div class="environment-information"> <ci-icon :status="iconStatus" /> <p class="inline append-bottom-0" v-html="environment"></p> diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue index 7b077d5e621..ec52d272168 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -28,20 +28,22 @@ export default { <div class="bs-callout bs-callout-warning"> <p v-if="tags.length" class="js-stuck-with-tags append-bottom-0"> {{ - s__(`This job is stuck, because you don't have + s__(`This job is stuck because you don't have any active runners online with any of these tags assigned to them:`) }} - <span v-for="(tag, index) in tags" :key="index" class="badge badge-primary"> {{ tag }} </span> + <span v-for="(tag, index) in tags" :key="index" class="badge badge-primary append-right-4"> + {{ tag }} + </span> </p> <p v-else-if="hasNoRunnersForProject" class="js-stuck-no-runners append-bottom-0"> {{ - s__(`Job|This job is stuck, because the project + s__(`Job|This job is stuck because the project doesn't have any runners online assigned to it.`) }} </p> <p v-else class="js-stuck-no-active-runner append-bottom-0"> {{ - s__(`This job is stuck, because you don't + s__(`This job is stuck because you don't have any active runners that can run this job.`) }} </p> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9b40ffb26a2..dbb22127e82 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,11 +12,11 @@ class ApplicationController < ActionController::Base include WorkhorseHelper include EnforcesTwoFactorAuthentication include WithPerformanceBar + include SessionlessAuthentication # this can be removed after switching to rails 5 # https://gitlab.com/gitlab-org/gitlab-ce/issues/51908 include InvalidUTF8ErrorHandler unless Gitlab.rails5? - before_action :authenticate_sessionless_user! before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? before_action :validate_user_service_ticket! @@ -153,13 +153,6 @@ class ApplicationController < ActionController::Base end end - # This filter handles personal access tokens, and atom requests with rss tokens - def authenticate_sessionless_user! - user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user - - sessionless_sign_in(user) if user - end - def log_exception(exception) Raven.capture_exception(exception) if sentry_enabled? @@ -426,25 +419,11 @@ class ApplicationController < ActionController::Base Gitlab::I18n.with_user_locale(current_user, &block) end - def sessionless_sign_in(user) - if user && can?(user, :log_in) - # Notice we are passing store false, so the user is not - # actually stored in the session and a token is needed - # for every request. If you want the token to work as a - # sign in token, you can simply remove store: false. - sign_in(user, store: false, message: :sessionless_sign_in) - end - end - def set_page_title_header # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8 response.headers['Page-Title'] = URI.escape(page_title('GitLab')) end - def sessionless_user? - current_user && !session.keys.include?('warden.user.user.key') - end - def peek_request? request.path.start_with?('/-/peek') end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 777b147e2dd..0319948a12f 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -6,6 +6,7 @@ module NotesActions extend ActiveSupport::Concern included do + prepend_before_action :normalize_create_params, only: [:create] before_action :set_polling_interval_header, only: [:index] before_action :require_noteable!, only: [:index, :create] before_action :authorize_admin_note!, only: [:update, :destroy] @@ -247,6 +248,15 @@ module NotesActions DiscussionSerializer.new(project: project, noteable: noteable, current_user: current_user, note_entity: ProjectNoteEntity) end + # Avoids checking permissions in the wrong object - this ensures that the object we checked permissions for + # is the object we're actually creating a note in. + def normalize_create_params + params[:note].try do |note| + note[:noteable_id] = params[:target_id] + note[:noteable_type] = params[:target_type].classify + end + end + def note_project strong_memoize(:note_project) do next nil unless project diff --git a/app/controllers/concerns/sessionless_authentication.rb b/app/controllers/concerns/sessionless_authentication.rb new file mode 100644 index 00000000000..590eefc6dab --- /dev/null +++ b/app/controllers/concerns/sessionless_authentication.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# == SessionlessAuthentication +# +# Controller concern to handle PAT and RSS token authentication methods +# +module SessionlessAuthentication + # This filter handles personal access tokens, and atom requests with rss tokens + def authenticate_sessionless_user!(request_format) + user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format) + + sessionless_sign_in(user) if user + end + + def sessionless_user? + current_user && !session.keys.include?('warden.user.user.key') + end + + def sessionless_sign_in(user) + if user && can?(user, :log_in) + # Notice we are passing store false, so the user is not + # actually stored in the session and a token is needed + # for every request. If you want the token to work as a + # sign in token, you can simply remove store: false. + sign_in(user, store: false, message: :sessionless_sign_in) + end + end +end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index e9686ed8d06..57e612d89d3 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController include ParamsBackwardCompatibility include RendersMemberAccess + prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } before_action :set_non_archived_param before_action :default_sorting skip_cross_project_access_check :index, :starred diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index b82caf30a91..3fa582cf25b 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -4,6 +4,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController include ActionView::Helpers::NumberHelper before_action :authorize_read_project!, only: :index + before_action :authorize_read_group!, only: :index before_action :find_todos, only: [:index, :destroy_all] def index @@ -60,6 +61,15 @@ class Dashboard::TodosController < Dashboard::ApplicationController end end + def authorize_read_group! + group_id = params[:group_id] + + if group_id.present? + group = Group.find(group_id) + render_404 unless can?(current_user, :read_group, group) + end + end + def find_todos @todos ||= TodosFinder.new(current_user, todo_params).execute end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4ce9be44403..be2d9512c01 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -4,6 +4,9 @@ class DashboardController < Dashboard::ApplicationController include IssuesAction include MergeRequestsAction + prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) } + prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) } + before_action :event_filter, only: :activity before_action :projects, only: [:issues, :merge_requests] before_action :set_show_full_reference, only: [:issues, :merge_requests] diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index a1ec144410b..6ea4758ec32 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -3,6 +3,7 @@ class GraphqlController < ApplicationController # Unauthenticated users have access to the API for public data skip_before_action :authenticate_user! + prepend_before_action(only: [:execute]) { authenticate_sessionless_user!(:api) } before_action :check_graphql_feature_flag! diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 062c8c4e9e1..c5d8ac2ed77 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -9,6 +9,9 @@ class GroupsController < Groups::ApplicationController respond_to :html + prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) } + prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) } + before_action :authenticate_user!, only: [:new, :create] before_action :group, except: [:index, :new, :create] diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index b50f140dc80..ab4ca56bb49 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -9,7 +9,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController before_action :verify_user_oauth_applications_enabled, except: :index before_action :authenticate_user! before_action :add_gon_variables - before_action :load_scopes, only: [:index, :create, :edit] + before_action :load_scopes, only: [:index, :create, :edit, :update] helper_method :can? diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 84a2a461da7..8ba18aacc58 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -6,6 +6,7 @@ class Projects::CommitsController < Projects::ApplicationController include ExtractsPath include RendersCommits + prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } before_action :whitelist_query_limiting, except: :commits_root before_action :require_non_empty_project before_action :assign_ref_vars, except: :commits_root diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d6d7110355b..c6ab6b4642e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -9,10 +9,6 @@ class Projects::IssuesController < Projects::ApplicationController include IssuesCalendar include SpammableActions - def self.authenticate_user_only_actions - %i[new] - end - def self.issue_except_actions %i[index calendar new create bulk_update] end @@ -21,7 +17,10 @@ class Projects::IssuesController < Projects::ApplicationController %i[index calendar] end - prepend_before_action :authenticate_user!, only: authenticate_user_only_actions + prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } + prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) } + prepend_before_action :authenticate_new_issue!, only: [:new] + prepend_before_action :store_uri, only: [:new, :show] before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! @@ -232,16 +231,18 @@ class Projects::IssuesController < Projects::ApplicationController ] + [{ label_ids: [], assignee_ids: [] }] end - def authenticate_user! + def authenticate_new_issue! return if current_user notice = "Please sign in to create the new issue." + redirect_to new_user_session_path, notice: notice + end + + def store_uri if request.get? && !request.xhr? store_location_for :user, request.fullpath end - - redirect_to new_user_session_path, notice: notice end def serializer diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 20998c97730..8e68014a30d 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -11,7 +11,10 @@ class Projects::MilestonesController < Projects::ApplicationController before_action :authorize_read_milestone! # Allow admin milestone - before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels, :promote] + before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels] + + # Allow to promote milestone + before_action :authorize_promote_milestone!, only: :promote respond_to :html @@ -78,7 +81,7 @@ class Projects::MilestonesController < Projects::ApplicationController def promote promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) - flash[:notice] = flash_notice_for(promoted_milestone, project.group) + flash[:notice] = flash_notice_for(promoted_milestone, project_group) respond_to do |format| format.html do @@ -109,6 +112,12 @@ class Projects::MilestonesController < Projects::ApplicationController protected + def project_group + strong_memoize(:project_group) do + project.group + end + end + def milestones strong_memoize(:milestones) do MilestonesFinder.new(search_params).execute @@ -125,13 +134,17 @@ class Projects::MilestonesController < Projects::ApplicationController return render_404 unless can?(current_user, :admin_milestone, @project) end + def authorize_promote_milestone! + return render_404 unless can?(current_user, :admin_milestone, project_group) + end + def milestone_params params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end def search_params - if request.format.json? && @project.group && can?(current_user, :read_group, @project.group) - groups = @project.group.self_and_ancestors_ids + if request.format.json? && project_group && can?(current_user, :read_group, project_group) + groups = project_group.self_and_ancestors_ids end params.permit(:state).merge(project_ids: @project.id, group_ids: groups) diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index c8442ff3592..2b28670a49b 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -3,6 +3,8 @@ class Projects::TagsController < Projects::ApplicationController include SortingHelper + prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } + # Authorize before_action :require_non_empty_project before_action :authorize_download_code! diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7f4a9f5151b..8bf93bfd68d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -7,6 +7,8 @@ class ProjectsController < Projects::ApplicationController include PreviewMarkdown include SendFileUpload + prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } + before_action :whitelist_query_limiting, only: [:create] before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :redirect_git_extension, only: [:show] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 5b70c69d7f4..8b040dc080e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -14,6 +14,7 @@ class UsersController < ApplicationController calendar_activities: true skip_before_action :authenticate_user! + prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } before_action :user, except: [:exists] before_action :authorize_read_user_profile!, only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets] diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 94a030d9d57..9666080092b 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -2,6 +2,7 @@ module MilestonesHelper include EntityDateHelper + include Gitlab::Utils::StrongMemoize def milestones_filter_path(opts = {}) if @project @@ -243,4 +244,16 @@ module MilestonesHelper dashboard_milestone_path(milestone.safe_title, title: milestone.title) end end + + def can_admin_project_milestones? + strong_memoize(:can_admin_project_milestones) do + can?(current_user, :admin_milestone, @project) + end + end + + def can_admin_group_milestones? + strong_memoize(:can_admin_group_milestones) do + can?(current_user, :admin_milestone, @project.group) + end + end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index d3284e90568..1b3c1f9a8a9 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -26,7 +26,7 @@ module Emails mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id)) end - def note_snippet_email(recipient_id, note_id) + def note_project_snippet_email(recipient_id, note_id) setup_note_mail(note_id, recipient_id) @snippet = @note.noteable diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 7c84bd734bb..da08214963f 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -15,6 +15,8 @@ module Ci WRITE_LOCK_SLEEP = 0.01.seconds WRITE_LOCK_TTL = 1.minute + FailedToPersistDataError = Class.new(StandardError) + # Note: The ordering of this enum is related to the precedence of persist store. # The bottom item takes the higest precedence, and the top item takes the lowest precedence. enum data_store: { @@ -109,16 +111,19 @@ module Ci def unsafe_persist_to!(new_store) return if data_store == new_store.to_s - raise ArgumentError, 'Can not persist empty data' unless size > 0 - old_store_class = self.class.get_store_class(data_store) + current_data = get_data - get_data.tap do |the_data| - self.raw_data = nil - self.data_store = new_store - unsafe_set_data!(the_data) + unless current_data&.bytesize.to_i == CHUNK_SIZE + raise FailedToPersistDataError, 'Data is not fullfilled in a bucket' end + old_store_class = self.class.get_store_class(data_store) + + self.raw_data = nil + self.data_store = new_store + unsafe_set_data!(current_data) + old_store_class.delete_data(self) end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 6e2adc76ec6..a8c9e54f00c 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -15,7 +15,7 @@ module CacheMarkdownField # Increment this number every time the renderer changes its output CACHE_REDCARPET_VERSION = 3 CACHE_COMMONMARK_VERSION_START = 10 - CACHE_COMMONMARK_VERSION = 11 + CACHE_COMMONMARK_VERSION = 12 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 7078496ff52..4a128dde5cd 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -8,6 +8,7 @@ class EnvironmentStatus delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment + delegate :status, to: :deployment, allow_nil: true delegate :deployed_at, to: :deployment, allow_nil: true def self.for_merge_request(mr, user) @@ -43,22 +44,6 @@ class EnvironmentStatus .merge_request_diff_files.where(deleted_file: false) end - ## - # Since frontend has not supported all statuses yet, BE has to - # proxy some status to a supported status. - def status - return unless deployment - - case deployment.status - when 'created' - 'running' - when 'canceled' - 'failed' - else - deployment.status - end - end - private PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze diff --git a/app/models/note.rb b/app/models/note.rb index 592efb714f3..a6ae4f58ac4 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -324,7 +324,7 @@ class Note < ActiveRecord::Base end def to_ability_name - for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore + for_snippet? ? noteable.class.name.underscore : noteable_type.underscore end def can_be_discussion_note? diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index 8ef74539209..7351674201e 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true class PoolRepository < ActiveRecord::Base - POOL_PREFIX = '@pools' - belongs_to :shard validates :shard, presence: true - # For now, only pool repositories are tracked in the database. However, we may - # want to add other repository types in the future - self.table_name = 'repositories' + has_many :member_projects, class_name: 'Project' - has_many :pool_member_projects, class_name: 'Project', foreign_key: :pool_repository_id + after_create :correct_disk_path def shard_name shard&.name @@ -19,4 +15,15 @@ class PoolRepository < ActiveRecord::Base def shard_name=(name) self.shard = Shard.by_name(name) end + + private + + def correct_disk_path + update!(disk_path: storage.disk_path) + end + + def storage + Storage::HashedProject + .new(self, prefix: Storage::HashedProject::POOL_PATH_PREFIX) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 84c98e463a8..c6351e1c7fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -878,9 +878,9 @@ class Project < ActiveRecord::Base end def readme_url - readme = repository.readme - if readme - Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme.path)) + readme_path = repository.readme_path + if readme_path + Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme_path)) end end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 211e5c3fcbf..60cb2d380d5 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -71,7 +71,7 @@ class PrometheusService < MonitoringService end def prometheus_client - RestClient::Resource.new(api_url) if api_url && manual_configuration? && active? + RestClient::Resource.new(api_url, max_redirects: 0) if api_url && manual_configuration? && active? end def prometheus_available? diff --git a/app/models/repository.rb b/app/models/repository.rb index 427dac99b79..35dd120856d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -35,7 +35,7 @@ class Repository # # For example, for entry `:commit_count` there's a method called `commit_count` which # stores its data in the `commit_count` cache key. - CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide + CACHED_METHODS = %i(size commit_count rendered_readme readme_path contribution_guide changelog license_blob license_key gitignore gitlab_ci_yml branch_names tag_names branch_count tag_count avatar exists? root_ref has_visible_content? @@ -48,7 +48,7 @@ class Repository # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # the corresponding methods to call for refreshing caches. METHOD_CACHES_FOR_FILE_TYPES = { - readme: :rendered_readme, + readme: %i(rendered_readme readme_path), changelog: :changelog, license: %i(license_blob license_key license), contributing: :contribution_guide, @@ -591,6 +591,11 @@ class Repository head_tree&.readme end + def readme_path + readme&.path + end + cache_method :readme_path + def rendered_readme return unless readme diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb index 90710f73fd3..911fb7e9ce9 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed_project.rb @@ -5,17 +5,19 @@ module Storage attr_accessor :project delegate :gitlab_shell, :repository_storage, to: :project - ROOT_PATH_PREFIX = '@hashed'.freeze + REPOSITORY_PATH_PREFIX = '@hashed' + POOL_PATH_PREFIX = '@pools' - def initialize(project) + def initialize(project, prefix: REPOSITORY_PATH_PREFIX) @project = project + @prefix = prefix end # Base directory # # @return [String] directory where repository is stored def base_dir - "#{ROOT_PATH_PREFIX}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash + "#{@prefix}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash end # Disk path is used to build repository and project's wiki path on disk diff --git a/app/policies/commit_policy.rb b/app/policies/commit_policy.rb index 67e9bc12804..4d4f0ba9267 100644 --- a/app/policies/commit_policy.rb +++ b/app/policies/commit_policy.rb @@ -2,4 +2,6 @@ class CommitPolicy < BasePolicy delegate { @subject.project } + + rule { can?(:download_code) }.enable :read_commit end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index bbc2b48b856..f22843b6463 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -9,8 +9,17 @@ class NotePolicy < BasePolicy condition(:editable, scope: :subject) { @subject.editable? } + condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") } + rule { ~editable }.prevent :admin_note + # If user can't read the issue/MR/etc then they should not be allowed to do anything to their own notes + rule { ~can_read_noteable }.policy do + prevent :read_note + prevent :admin_note + prevent :resolve_note + end + rule { is_author }.policy do enable :read_note enable :admin_note diff --git a/app/views/devise/mailer/email_changed.html.haml b/app/views/devise/mailer/email_changed.html.haml new file mode 100644 index 00000000000..5398430fdfd --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.haml @@ -0,0 +1,12 @@ += email_default_heading("Hello, #{@resource.name}!") + +- if @resource.try(:unconfirmed_email?) + %p + We're contacting you to notify you that your email is being changed to #{@resource.reload.unconfirmed_email}. +- else + %p + We're contacting you to notify you that your email has been changed to #{@resource.email}. + +%p + If you did not initiate this change, please contact your administrator + immediately. diff --git a/app/views/devise/mailer/email_changed.text.erb b/app/views/devise/mailer/email_changed.text.erb new file mode 100644 index 00000000000..18137389e7b --- /dev/null +++ b/app/views/devise/mailer/email_changed.text.erb @@ -0,0 +1,10 @@ +Hello, <%= @resource.name %>! + +<% if @resource.try(:unconfirmed_email?) %> +We're contacting you to notify you that your email is being changed to <%= @resource.reload.unconfirmed_email %>. +<% else %> +We're contacting you to notify you that your email has been changed to <%= @resource.email %>. +<% end %> + +If you did not initiate this change, please contact your administrator +immediately. diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml index 836981fc6fd..586b0f6ebfa 100644 --- a/app/views/groups/labels/edit.html.haml +++ b/app/views/groups/labels/edit.html.haml @@ -1,4 +1,6 @@ -- page_title 'Edit', @label.name, 'Labels' +- add_to_breadcrumbs _("Labels"), group_labels_path(@group) +- breadcrumb_title _("Edit") +- page_title "Edit", @label.name, _("Labels") %h3.page-title Edit Label diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml index 538c353cf2d..bb0b8d2b94d 100644 --- a/app/views/groups/labels/new.html.haml +++ b/app/views/groups/labels/new.html.haml @@ -1,5 +1,6 @@ -- breadcrumb_title "Labels" -- page_title 'New Label' +- add_to_breadcrumbs _("Labels"), group_labels_path(@group) +- breadcrumb_title _("New") +- page_title _("New Label") %h3.page-title New Label diff --git a/app/views/groups/milestones/edit.html.haml b/app/views/groups/milestones/edit.html.haml index 5f6d7d209d0..c703d5f7f93 100644 --- a/app/views/groups/milestones/edit.html.haml +++ b/app/views/groups/milestones/edit.html.haml @@ -1,7 +1,10 @@ -- page_title "Milestones" +- breadcrumb_title _("Edit") +- page_title _("Milestones") + - render "header_title" %h3.page-title Edit Milestone +%hr = render "form" diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index d758e314d41..248cb3b0ba5 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -1,7 +1,12 @@ -- breadcrumb_title "Milestones" -- page_title "Milestones" +- @no_container = true +- add_to_breadcrumbs _("Milestones"), group_milestones_path(@group) +- breadcrumb_title _("New") +- page_title _("Milestones"), @milestone.name, _("Milestones") -%h3.page-title - New Milestone +%div{ class: container_class } + %h3.page-title + New Milestone -= render "form" + %hr + + = render "form" diff --git a/app/views/notify/note_snippet_email.html.haml b/app/views/notify/note_project_snippet_email.html.haml index 5e69f01a486..5e69f01a486 100644 --- a/app/views/notify/note_snippet_email.html.haml +++ b/app/views/notify/note_project_snippet_email.html.haml diff --git a/app/views/notify/note_snippet_email.text.erb b/app/views/notify/note_project_snippet_email.text.erb index 413d9e6e9ac..413d9e6e9ac 100644 --- a/app/views/notify/note_snippet_email.text.erb +++ b/app/views/notify/note_project_snippet_email.text.erb diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index c6789e32dbe..1a74b120c26 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -8,62 +8,50 @@ - ref = local_assigns.fetch(:ref) { merge_request&.source_branch } - link = commit_path(project, commit, merge_request: merge_request) -- cache_key = [project.full_path, - ref, - commit.id, - Gitlab::CurrentSettings.current_application_settings, - @path.presence, - current_controller?(:commits), - merge_request&.iid, - view_details, - commit.status(ref), - I18n.locale].compact - -= cache(cache_key, expires_in: 1.day) do - %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" } - - .avatar-cell.d-none.d-sm-block - = author_avatar(commit, size: 36, has_tooltip: false) - - .commit-detail.flex-list - .commit-content.qa-commit-content - - if view_details && merge_request - = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title" - - else - = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") - %span.commit-row-message.d-block.d-sm-none - · - = commit.short_id - - if commit.status(ref) - .d-block.d-sm-none - = render_commit_status(commit, ref: ref) - - if commit.description? - %button.text-expander.js-toggle-button - = sprite_icon('ellipsis_h', size: 12) +%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" } + + .avatar-cell.d-none.d-sm-block + = author_avatar(commit, size: 36, has_tooltip: false) + + .commit-detail.flex-list + .commit-content.qa-commit-content + - if view_details && merge_request + = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title" + - else + = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") + %span.commit-row-message.d-block.d-sm-none + · + = commit.short_id + - if commit.status(ref) + .d-block.d-sm-none + = render_commit_status(commit, ref: ref) + - if commit.description? + %button.text-expander.js-toggle-button + = sprite_icon('ellipsis_h', size: 12) - .committer - - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom') - - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } - #{ commit_text.html_safe } + .committer + - commit_author_link = commit_author_link(commit, avatar: false, size: 24) + - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom') + - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } + #{ commit_text.html_safe } - - if commit.description? - %pre.commit-row-description.js-toggle-content.append-bottom-8 - = preserve(markdown_field(commit, :description)) + - if commit.description? + %pre.commit-row-description.js-toggle-content.append-bottom-8 + = preserve(markdown_field(commit, :description)) - .commit-actions.flex-row.d-none.d-sm-flex - - if request.xhr? - = render partial: 'projects/commit/signature', object: commit.signature - - else - = render partial: 'projects/commit/ajax_signature', locals: { commit: commit } + .commit-actions.flex-row.d-none.d-sm-flex + - if request.xhr? + = render partial: 'projects/commit/signature', object: commit.signature + - else + = render partial: 'projects/commit/ajax_signature', locals: { commit: commit } - - if commit.status(ref) - = render_commit_status(commit, ref: ref) + - if commit.status(ref) + = render_commit_status(commit, ref: ref) - .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } } + .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } } - .commit-sha-group - .label.label-monospace - = commit.short_id - = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body") - = link_to_browse_code(project, commit) + .commit-sha-group + .label.label-monospace + = commit.short_id + = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body") + = link_to_browse_code(project, commit) diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index b8ee4305142..b9d45e83032 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Labels", project_labels_path(@project) +- breadcrumb_title "Edit" - page_title "Edit", @label.name, "Labels" %div{ class: container_class } diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 02f59f30a39..c6739231e36 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,5 +1,6 @@ - @no_container = true -- breadcrumb_title "Labels" +- add_to_breadcrumbs "Labels", project_labels_path(@project) +- breadcrumb_title "New" - page_title "New Label" %div{ class: container_class } diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index af3f25c6a30..4006a468792 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,6 +1,9 @@ - @no_container = true +- breadcrumb_title "Edit" +- add_to_breadcrumbs "Milestones", project_milestones_path(@project) - page_title "Edit", @milestone.title, "Milestones" + %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index c301f517013..01cc951e8c2 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,5 +1,6 @@ - @no_container = true -- breadcrumb_title "Milestones" +- add_to_breadcrumbs "Milestones", project_milestones_path(@project) +- breadcrumb_title "New" - page_title "New Milestone" %div{ class: container_class } diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 80aa1500d53..aa9e29e5371 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,5 +1,7 @@ - @content_class = "limit-container-width" unless fluid_layout -- page_title _("Edit"), @page.title.capitalize, _("Wiki") +- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, @page) +- breadcrumb_title @page.persisted? ? _("Edit") : _("New") +- page_title @page.persisted? ? _("Edit") : _("New"), @page.title.capitalize, _("Wiki") = wiki_page_errors(@error) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 3dd2842be4f..ed7fefba56d 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -35,8 +35,8 @@ .col-sm-2 .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end - if @project - - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - - if @project.group + - if can_admin_project_milestones? and milestone.active? + - if can_admin_group_milestones? %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), disabled: true, type: 'button', diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 0ce13ee7a53..ef8664e6f47 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -3,31 +3,31 @@ .d-none.d-sm-block - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do - Edit + = _("Edit") - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do - Delete - = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-success", title: "New snippet" do - New snippet + = link_to snippet_path(@snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do + = _("Delete") + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: _("New snippet") do + = _("New snippet") - if @snippet.submittable_as_spam_by?(current_user) - = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' + = link_to _('Submit as spam'), mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam') .d-block.d-sm-none.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } - Options + = _("Options") = icon('caret-down') .dropdown-menu.dropdown-menu-full-width %ul %li - = link_to new_snippet_path, title: "New snippet" do - New snippet + = link_to new_snippet_path, title: _("New snippet") do + = _("New snippet") - if can?(current_user, :admin_personal_snippet, @snippet) %li - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do - Delete + = link_to snippet_path(@snippet), method: :delete, data: { confirm: _("Are you sure?") }, title: _('Delete Snippet') do + = _("Delete") - if can?(current_user, :update_personal_snippet, @snippet) %li = link_to edit_snippet_path(@snippet) do - Edit + = _("Edit") - if @snippet.submittable_as_spam_by?(current_user) %li - = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post + = link_to _('Submit as spam'), mark_as_spam_snippet_path(@snippet), method: :post diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index dfea8b40bd8..69d41f8fe5e 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -5,6 +5,6 @@ = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project } - if @snippets.empty? %li - .nothing-here-block Nothing here. + .nothing-here-block= _("Nothing here.") = paginate @snippets, theme: 'gitlab' diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index dc4b0fd9ba0..c312226dd6c 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -4,7 +4,7 @@ .nav-links.snippet-scope-menu.mobile-separator.nav.nav-tabs %li{ class: active_when(params[:scope].nil?) } = link_to subject_snippets_path(subject) do - All + = _("All") %span.badge.badge-pill - if include_private = subject.snippets.count @@ -14,18 +14,18 @@ - if include_private %li{ class: active_when(params[:scope] == "are_private") } = link_to subject_snippets_path(subject, scope: 'are_private') do - Private + = _("Private") %span.badge.badge-pill = subject.snippets.are_private.count %li{ class: active_when(params[:scope] == "are_internal") } = link_to subject_snippets_path(subject, scope: 'are_internal') do - Internal + = _("Internal") %span.badge.badge-pill = subject.snippets.are_internal.count %li{ class: active_when(params[:scope] == "are_public") } = link_to subject_snippets_path(subject, scope: 'are_public') do - Public + = _("Public") %span.badge.badge-pill = subject.snippets.are_public.count diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 18ebeb78f87..ebc6c0a2605 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,5 +1,6 @@ -- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" +- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") + %h3.page-title - Edit Snippet + = _("Edit Snippet") %hr = render 'shared/snippets/form', url: snippet_path(@snippet) diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 9b4a7dbe68d..4f418e2381f 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,13 +1,13 @@ -- page_title "By #{@user.name}", "Snippets" +- page_title _("By %{user_name}") % { user_name: @user.name }, _("Snippets") %ol.breadcrumb %li.breadcrumb-item = link_to snippets_path do - Snippets + = _("Snippets") %li.breadcrumb-item = @user.name .float-right.d-none.d-sm-block = link_to user_path(@user) do - #{@user.name} profile page + = _("%{user_name} profile page") % { user_name: @user.name } = render 'snippets' diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 6bc748d346e..114c777bdc2 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,6 +1,6 @@ - @hide_top_links = true - @hide_breadcrumbs = true -- page_title "New Snippet" +- page_title _("New Snippet") .page-title-holder %h1.page-title= _('New Snippet') diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index 220ba2b49e6..01b95145937 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -1,7 +1,7 @@ - if current_user - if note.emoji_awardable? .note-actions-item - = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do + = link_to '#', title: _('Add reaction'), class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do = icon('spinner spin') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') @@ -9,7 +9,7 @@ - if note_editable .note-actions-item - = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do + = button_tag title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do %span.link-highlight = custom_icon('icon_pencil') diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 578327883e5..36b4e00e8d5 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,8 +1,8 @@ - @hide_top_links = true - @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout -- add_to_breadcrumbs "Snippets", dashboard_snippets_path +- add_to_breadcrumbs _("Snippets"), dashboard_snippets_path - breadcrumb_title @snippet.to_reference -- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" +- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") = render 'shared/snippets/header' diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 938cb579e9f..01acbf8eadd 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -7,7 +7,7 @@ %li %span.light %i.fa.fa-clock-o - = event.created_at.strftime('%-I:%M%P') + = event.created_at.to_time.in_time_zone.strftime('%-I:%M%P') - if event.visible_to_user?(current_user) - if event.push? #{event.action_name} #{event.ref_type} diff --git a/changelogs/unreleased/33705-merge-request-rebase-api.yml b/changelogs/unreleased/33705-merge-request-rebase-api.yml new file mode 100644 index 00000000000..322fe31ce87 --- /dev/null +++ b/changelogs/unreleased/33705-merge-request-rebase-api.yml @@ -0,0 +1,5 @@ +--- +title: Add a rebase API endpoint for merge requests +merge_request: 23296 +author: +type: added diff --git a/changelogs/unreleased/38495-calendar-activities-in-timezone.yml b/changelogs/unreleased/38495-calendar-activities-in-timezone.yml new file mode 100644 index 00000000000..778d637609c --- /dev/null +++ b/changelogs/unreleased/38495-calendar-activities-in-timezone.yml @@ -0,0 +1,5 @@ +--- +title: Show user contributions in correct timezone within user profile +merge_request: 23419 +author: +type: changed diff --git a/changelogs/unreleased/50839-webide-mr-dropdown-filter.yml b/changelogs/unreleased/50839-webide-mr-dropdown-filter.yml new file mode 100644 index 00000000000..1c6c8747197 --- /dev/null +++ b/changelogs/unreleased/50839-webide-mr-dropdown-filter.yml @@ -0,0 +1,5 @@ +--- +title: Scope default MR search in WebIDE dropdown to current project +merge_request: 23400 +author: +type: changed diff --git a/changelogs/unreleased/51061-readme-url-n-1-rpc-call-resolved.yml b/changelogs/unreleased/51061-readme-url-n-1-rpc-call-resolved.yml new file mode 100644 index 00000000000..86f91fcb427 --- /dev/null +++ b/changelogs/unreleased/51061-readme-url-n-1-rpc-call-resolved.yml @@ -0,0 +1,5 @@ +--- +title: Improves performance of Project#readme_url by caching the README path +merge_request: 23357 +author: +type: performance diff --git a/changelogs/unreleased/54571-runner-tags.yml b/changelogs/unreleased/54571-runner-tags.yml new file mode 100644 index 00000000000..1bb19d22e9c --- /dev/null +++ b/changelogs/unreleased/54571-runner-tags.yml @@ -0,0 +1,5 @@ +--- +title: Adds margins between tags when a job is stuck +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/check-if-fetched-data-does-is-complete.yml b/changelogs/unreleased/check-if-fetched-data-does-is-complete.yml new file mode 100644 index 00000000000..31c131045b9 --- /dev/null +++ b/changelogs/unreleased/check-if-fetched-data-does-is-complete.yml @@ -0,0 +1,5 @@ +--- +title: Validate chunk size when persist +merge_request: 23341 +author: +type: fixed diff --git a/changelogs/unreleased/gt-externalize-app-views-snippets.yml b/changelogs/unreleased/gt-externalize-app-views-snippets.yml new file mode 100644 index 00000000000..633aa9f2534 --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/snippets` +merge_request: 23351 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/include-new-link-in-breadcrumb.yml b/changelogs/unreleased/include-new-link-in-breadcrumb.yml new file mode 100644 index 00000000000..68c808d66d7 --- /dev/null +++ b/changelogs/unreleased/include-new-link-in-breadcrumb.yml @@ -0,0 +1,5 @@ +--- +title: Include new link in breadcrumb for issues, merge requests, milestones, and labels +merge_request: 18515 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/remove-deployment-status-hack-from-backend.yml b/changelogs/unreleased/remove-deployment-status-hack-from-backend.yml new file mode 100644 index 00000000000..2348bfab7d9 --- /dev/null +++ b/changelogs/unreleased/remove-deployment-status-hack-from-backend.yml @@ -0,0 +1,5 @@ +--- +title: Return real deployment status to frontend +merge_request: 23270 +author: +type: fixed diff --git a/changelogs/unreleased/security-182-update-workhorse.yml b/changelogs/unreleased/security-182-update-workhorse.yml new file mode 100644 index 00000000000..76850901b68 --- /dev/null +++ b/changelogs/unreleased/security-182-update-workhorse.yml @@ -0,0 +1,5 @@ +--- +title: Redact sensitive information on gitlab-workhorse log +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-2736-prometheus-ssrf.yml b/changelogs/unreleased/security-2736-prometheus-ssrf.yml new file mode 100644 index 00000000000..9d0dda8a75f --- /dev/null +++ b/changelogs/unreleased/security-2736-prometheus-ssrf.yml @@ -0,0 +1,5 @@ +--- +title: Do not follow redirects in Prometheus service when making http requests to the configured api url +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-bvl-exposure-in-commits-list.yml b/changelogs/unreleased/security-bvl-exposure-in-commits-list.yml new file mode 100644 index 00000000000..0361fb0c041 --- /dev/null +++ b/changelogs/unreleased/security-bvl-exposure-in-commits-list.yml @@ -0,0 +1,5 @@ +--- +title: Don't expose confidential information in commit message list +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-email-change-notification.yml b/changelogs/unreleased/security-email-change-notification.yml new file mode 100644 index 00000000000..45075ff20bb --- /dev/null +++ b/changelogs/unreleased/security-email-change-notification.yml @@ -0,0 +1,5 @@ +--- +title: Provide email notification when a user changes their email address +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-pat-web-access.yml b/changelogs/unreleased/security-fix-pat-web-access.yml new file mode 100644 index 00000000000..62ffb908fe5 --- /dev/null +++ b/changelogs/unreleased/security-fix-pat-web-access.yml @@ -0,0 +1,5 @@ +--- +title: Restrict Personal Access Tokens to API scope on web requests +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-uri-xss-applications.yml b/changelogs/unreleased/security-fix-uri-xss-applications.yml new file mode 100644 index 00000000000..0eaa1b1c4a3 --- /dev/null +++ b/changelogs/unreleased/security-fix-uri-xss-applications.yml @@ -0,0 +1,5 @@ +--- +title: Resolve reflected XSS in Ouath authorize window +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-webhook-ssrf-ipv6.yml b/changelogs/unreleased/security-fix-webhook-ssrf-ipv6.yml new file mode 100644 index 00000000000..32c85a2a7da --- /dev/null +++ b/changelogs/unreleased/security-fix-webhook-ssrf-ipv6.yml @@ -0,0 +1,5 @@ +--- +title: Fix SSRF in project integrations +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fj-crlf-injection.yml b/changelogs/unreleased/security-fj-crlf-injection.yml new file mode 100644 index 00000000000..861167b8a6e --- /dev/null +++ b/changelogs/unreleased/security-fj-crlf-injection.yml @@ -0,0 +1,5 @@ +--- +title: Fix CRLF vulnerability in Project hooks +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-guest-comments.yml b/changelogs/unreleased/security-guest-comments.yml new file mode 100644 index 00000000000..2c99512433b --- /dev/null +++ b/changelogs/unreleased/security-guest-comments.yml @@ -0,0 +1,5 @@ +--- +title: Fixed ability to comment on locked/confidential issues. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-guest-comments_2.yml b/changelogs/unreleased/security-guest-comments_2.yml new file mode 100644 index 00000000000..be6f2d6a490 --- /dev/null +++ b/changelogs/unreleased/security-guest-comments_2.yml @@ -0,0 +1,5 @@ +--- +title: Fixed ability of guest users to edit/delete comments on locked or confidential issues. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-issue_51301.yml b/changelogs/unreleased/security-issue_51301.yml new file mode 100644 index 00000000000..cf8ebb54b1c --- /dev/null +++ b/changelogs/unreleased/security-issue_51301.yml @@ -0,0 +1,5 @@ +--- +title: Fix milestone promotion authorization check +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-mermaid-xss.yml b/changelogs/unreleased/security-mermaid-xss.yml new file mode 100644 index 00000000000..bcf93ef37ff --- /dev/null +++ b/changelogs/unreleased/security-mermaid-xss.yml @@ -0,0 +1,5 @@ +--- +title: Configure mermaid to not render HTML content in diagrams +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-pages-toctou-race.yml b/changelogs/unreleased/security-pages-toctou-race.yml new file mode 100644 index 00000000000..1c055f6087f --- /dev/null +++ b/changelogs/unreleased/security-pages-toctou-race.yml @@ -0,0 +1,6 @@ +--- +title: Fix a possible symlink time of check to time of use race condition in GitLab + Pages +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-private-group.yml b/changelogs/unreleased/security-private-group.yml new file mode 100644 index 00000000000..dbb7794dfed --- /dev/null +++ b/changelogs/unreleased/security-private-group.yml @@ -0,0 +1,6 @@ +--- +title: Removed ability to see private group names when the group id is entered in + the url. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-stored-xss-for-environments.yml b/changelogs/unreleased/security-stored-xss-for-environments.yml new file mode 100644 index 00000000000..5d78ca00942 --- /dev/null +++ b/changelogs/unreleased/security-stored-xss-for-environments.yml @@ -0,0 +1,5 @@ +--- +title: Fix stored XSS for Environments +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-xss-in-markdown-following-unrecognized-html-element.yml b/changelogs/unreleased/security-xss-in-markdown-following-unrecognized-html-element.yml new file mode 100644 index 00000000000..3bd8123a346 --- /dev/null +++ b/changelogs/unreleased/security-xss-in-markdown-following-unrecognized-html-element.yml @@ -0,0 +1,5 @@ +--- +title: Fix possible XSS attack in Markdown urls with spaces +merge_request: 2599 +author: +type: security diff --git a/changelogs/unreleased/unicorn-monkey-patch.yml b/changelogs/unreleased/unicorn-monkey-patch.yml new file mode 100644 index 00000000000..6b0e00ca291 --- /dev/null +++ b/changelogs/unreleased/unicorn-monkey-patch.yml @@ -0,0 +1,5 @@ +--- +title: Add monkey patch to unicorn to fix eof? problem +merge_request: 23385 +author: +type: fixed diff --git a/config.ru b/config.ru index 405d01863ac..a5d055334dd 100644 --- a/config.ru +++ b/config.ru @@ -13,6 +13,10 @@ if defined?(Unicorn) # Max memory size (RSS) per worker use Unicorn::WorkerKiller::Oom, min, max end + + # Monkey patch for fixing Rack 2.0.6 bug: + # https://gitlab.com/gitlab-org/gitlab-ee/issues/8539 + Unicorn::StreamInput.send(:public, :eof?) # rubocop:disable GitlabSecurity/PublicSend end require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/application.rb b/config/application.rb index 5804d8fd27b..63a5b483fc2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -103,6 +103,9 @@ module Gitlab # - Webhook URLs (:hook) # - Sentry DSN (:sentry_dsn) # - File content from Web Editor (:content) + # + # NOTE: It is **IMPORTANT** to also update gitlab-workhorse's filter when adding parameters here to not + # introduce another security vulnerability: https://gitlab.com/gitlab-org/gitlab-workhorse/issues/182 config.filter_parameters += [/token$/, /password/, /secret/, /key$/] config.filter_parameters += %i( certificate diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 179e00cdbd0..67eabb0b4fc 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -103,6 +103,9 @@ Devise.setup do |config| # Send a notification email when the user's password is changed config.send_password_change_notification = true + # Send a notification email when the user's email is changed + config.send_email_changed_notification = true + # ==> Configuration for :validatable # Range for password length. Default is 6..128. config.password_length = 8..128 diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index f321b4ea763..6be5c00daaa 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -48,6 +48,13 @@ Doorkeeper.configure do # force_ssl_in_redirect_uri false + # Specify what redirect URI's you want to block during Application creation. + # Any redirect URI is whitelisted by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + forbid_redirect_uri { |uri| %w[data vbscript javascript].include?(uri.scheme.to_s.downcase) } + # Provide support for an owner to be assigned to each registered application (disabled by default) # Optional parameter confirmation: true (default false) if you want to enforce ownership of # a registered application diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb index 45963831c41..86cb930eca9 100644 --- a/config/initializers/rack_attack_global.rb +++ b/config/initializers/rack_attack_global.rb @@ -33,22 +33,22 @@ class Rack::Attack throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req| Gitlab::Throttle.settings.throttle_authenticated_api_enabled && req.api_request? && - req.authenticated_user_id + req.authenticated_user_id([:api]) end throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req| Gitlab::Throttle.settings.throttle_authenticated_web_enabled && req.web_request? && - req.authenticated_user_id + req.authenticated_user_id([:api, :rss, :ics]) end class Request def unauthenticated? - !authenticated_user_id + !authenticated_user_id([:api, :rss, :ics]) end - def authenticated_user_id - Gitlab::Auth::RequestAuthenticator.new(self).user&.id + def authenticated_user_id(request_formats) + Gitlab::Auth::RequestAuthenticator.new(self).user(request_formats)&.id end def api_request? diff --git a/db/migrate/20181108091549_cleanup_environments_external_url.rb b/db/migrate/20181108091549_cleanup_environments_external_url.rb new file mode 100644 index 00000000000..8d6c20a4b15 --- /dev/null +++ b/db/migrate/20181108091549_cleanup_environments_external_url.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CleanupEnvironmentsExternalUrl < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + update_column_in_batches(:environments, :external_url, nil) do |table, query| + query.where(table[:external_url].matches('javascript://%')) + end + end + + def down + end +end diff --git a/db/migrate/20181120082911_rename_repositories_pool_repositories.rb b/db/migrate/20181120082911_rename_repositories_pool_repositories.rb new file mode 100644 index 00000000000..165771c4775 --- /dev/null +++ b/db/migrate/20181120082911_rename_repositories_pool_repositories.rb @@ -0,0 +1,11 @@ +class RenameRepositoriesPoolRepositories < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + # This change doesn't require downtime as the table is not in use, so we're + # free to change an empty table + DOWNTIME = false + + def change + rename_table :repositories, :pool_repositories + end +end diff --git a/db/migrate/20181123135036_drop_not_null_constraint_pool_repository_disk_path.rb b/db/migrate/20181123135036_drop_not_null_constraint_pool_repository_disk_path.rb new file mode 100644 index 00000000000..bcd969e91c5 --- /dev/null +++ b/db/migrate/20181123135036_drop_not_null_constraint_pool_repository_disk_path.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class DropNotNullConstraintPoolRepositoryDiskPath < ActiveRecord::Migration[5.0] + DOWNTIME = false + + def change + change_column_null :pool_repositories, :disk_path, true + end +end diff --git a/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb b/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb new file mode 100644 index 00000000000..ff5510e8eb7 --- /dev/null +++ b/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class MigrateForbiddenRedirectUris < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + FORBIDDEN_SCHEMES = %w[data:// vbscript:// javascript://] + NEW_URI = 'http://forbidden-scheme-has-been-overwritten' + + disable_ddl_transaction! + + def up + update_forbidden_uris(:oauth_applications) + update_forbidden_uris(:oauth_access_grants) + end + + def down + # noop + end + + private + + def update_forbidden_uris(table_name) + update_column_in_batches(table_name, :redirect_uri, NEW_URI) do |table, query| + where_clause = FORBIDDEN_SCHEMES.map do |scheme| + table[:redirect_uri].matches("#{scheme}%") + end.inject(&:or) + + query.where(where_clause) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2fe94bf92fa..2e9b2a9ac89 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1506,6 +1506,13 @@ ActiveRecord::Schema.define(version: 20181126153547) do t.index ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree end + create_table "pool_repositories", id: :bigserial, force: :cascade do |t| + t.integer "shard_id", null: false + t.string "disk_path" + t.index ["disk_path"], name: "index_pool_repositories_on_disk_path", unique: true, using: :btree + t.index ["shard_id"], name: "index_pool_repositories_on_shard_id", using: :btree + end + create_table "programming_languages", force: :cascade do |t| t.string "name", null: false t.string "color", null: false @@ -1805,13 +1812,6 @@ ActiveRecord::Schema.define(version: 20181126153547) do t.index ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree end - create_table "repositories", id: :bigserial, force: :cascade do |t| - t.integer "shard_id", null: false - t.string "disk_path", null: false - t.index ["disk_path"], name: "index_repositories_on_disk_path", unique: true, using: :btree - t.index ["shard_id"], name: "index_repositories_on_shard_id", using: :btree - end - create_table "repository_languages", id: false, force: :cascade do |t| t.integer "project_id", null: false t.integer "programming_language_id", null: false @@ -2380,6 +2380,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade add_foreign_key "personal_access_tokens", "users" + add_foreign_key "pool_repositories", "shards", on_delete: :restrict add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade add_foreign_key "project_auto_devops", "projects", on_delete: :cascade @@ -2392,7 +2393,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade add_foreign_key "project_mirror_data", "projects", on_delete: :cascade add_foreign_key "project_statistics", "projects", on_delete: :cascade - add_foreign_key "projects", "repositories", column: "pool_repository_id", name: "fk_6e5c14658a", on_delete: :nullify + add_foreign_key "projects", "pool_repositories", name: "fk_6e5c14658a", on_delete: :nullify add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade add_foreign_key "protected_branch_push_access_levels", "protected_branches", name: "fk_9ffc86a3d9", on_delete: :cascade @@ -2404,7 +2405,6 @@ ActiveRecord::Schema.define(version: 20181126153547) do add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", on_delete: :cascade - add_foreign_key "repositories", "shards", on_delete: :restrict add_foreign_key "repository_languages", "projects", on_delete: :cascade add_foreign_key "resource_label_events", "issues", on_delete: :cascade add_foreign_key "resource_label_events", "labels", on_delete: :nullify diff --git a/doc/README.md b/doc/README.md index bf93c73843f..ba2bb89533b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -8,7 +8,7 @@ description: 'Learn how to use and administer GitLab, the most scalable Git-base Welcome to [GitLab](https://about.gitlab.com/) Documentation. Here you can access the complete documentation for GitLab, the single application for the -[entire DevOps lifecycle](#complete-devops-with-gitlab). +[entire DevOps lifecycle](#the-entire-devops-lifecycle). ## Overview @@ -72,11 +72,11 @@ GitLab provides statistics and insight into ways you can maximize the value of G The following documentation relates to the DevOps **Manage** stage: -| Manage Topics | Description | -|:----------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Authentication and Authorization](administration/auth/README.md) **[CORE ONLY]** | Supported authentication and authorization providers. | -| [GitLab Cycle Analytics](user/project/cycle_analytics.md) | Measure the time it takes to go from an [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have. | -| [Instance Statistics](user/instance_statistics/index.md) | Discover statistics on how many GitLab features you use and user activity. | +| Manage Topics | Description | +|:--------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Authentication and<br/>Authorization](administration/auth/README.md) **[CORE ONLY]** | Supported authentication and authorization providers. | +| [GitLab Cycle Analytics](user/project/cycle_analytics.md) | Measure the time it takes to go from an [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have. | +| [Instance Statistics](user/instance_statistics/index.md) | Discover statistics on how many GitLab features you use and user activity. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -93,17 +93,17 @@ management tools. The following documentation relates to the DevOps **Plan** stage: -| Plan Topics | Description | -|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| -| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. | -| [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. | -| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. | -| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md), [issue and merge request templates](user/project/description_templates.md), and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. | -| [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. | -| [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. | -| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. | -| [Time Tracking](workflow/time_tracking.md) | Track time spent on issues and merge requests. | -| [Todos](workflow/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. | +| Plan Topics | Description | +|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| +| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. | +| [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. | +| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. | +| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. | +| [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. | +| [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. | +| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. | +| [Time Tracking](workflow/time_tracking.md) | Track time spent on issues and merge requests. | +| [Todos](workflow/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -124,16 +124,16 @@ The following documentation relates to the DevOps **Create** stage: #### Projects and Groups -| Create Topics - Projects and Groups | Description | -|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------| -| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and [import and export projects between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. | -| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. | -| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. | -| [Projects](user/project/index.md), including [project access](public_access/public_access.md) and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. | -| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. | -| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. | -| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. | -| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. | +| Create Topics - Projects and Groups | Description | +|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------| +| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and<br/>[import and export<br/>projects between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. | +| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. | +| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. | +| [Projects](user/project/index.md), including [project access](public_access/public_access.md)<br/>and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. | +| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. | +| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. | +| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. | +| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -143,18 +143,18 @@ The following documentation relates to the DevOps **Create** stage: #### Repositories -| Create Topics - Repositories | Description | -|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------| -| [Branches](user/project/repository/branches/index.md) and the [default branch](user/project/repository/branches/index.md#default-branch) | How to use branches in GitLab. | -| [Commits](user/project/repository/index.md#commits) and [signing commits](user/project/repository/gpg_signed_commits/index.md) | Work with commits, and use GPG to sign your commits. | -| [Create branches](user/project/repository/web_editor.md#create-a-new-branch), [create](user/project/repository/web_editor.md#create-a-file) and [upload](user/project/repository/web_editor.md#upload-a-file) files, and [create directories](user/project/repository/web_editor.md#create-a-directory) | Create branches, create and upload files, and create directories within GitLab. | -| [Delete merged branches](user/project/repository/branches/index.md#delete-merged-branches) | Bulk delete branches after their changes are merged. | -| [File templates](user/project/repository/web_editor.md#template-dropdowns) | File templates for common files. | -| [Files](user/project/repository/index.md#files) | Files management. | -| [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files) | GitLab's support for `.ipynb` files. | -| [Protected branches](user/project/protected_branches.md) | Use protected branches. | -| [Repositories](user/project/repository/index.md) | Manage source code repositories in GitLab's user interface. | -| [Start a merge request](user/project/repository/web_editor.md#tips) | Start merge request when committing via GitLab's user interface. | +| Create Topics - Repositories | Description | +|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------| +| [Branches](user/project/repository/branches/index.md) and the [default branch](user/project/repository/branches/index.md#default-branch) | How to use branches in GitLab. | +| [Commits](user/project/repository/index.md#commits) and [signing commits](user/project/repository/gpg_signed_commits/index.md) | Work with commits, and use GPG to sign your commits. | +| [Create branches](user/project/repository/web_editor.md#create-a-new-branch), [create](user/project/repository/web_editor.md#create-a-file)<br/>and [upload](user/project/repository/web_editor.md#upload-a-file) files, and [create directories](user/project/repository/web_editor.md#create-a-directory) | Create branches, create and upload files, and create directories within GitLab. | +| [Delete merged branches](user/project/repository/branches/index.md#delete-merged-branches) | Bulk delete branches after their changes are merged. | +| [File templates](user/project/repository/web_editor.md#template-dropdowns) | File templates for common files. | +| [Files](user/project/repository/index.md#files) | Files management. | +| [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files) | GitLab's support for `.ipynb` files. | +| [Protected branches](user/project/protected_branches.md) | Use protected branches. | +| [Repositories](user/project/repository/index.md) | Manage source code repositories in GitLab's user interface. | +| [Start a merge request](user/project/repository/web_editor.md#tips) | Start merge request when committing via GitLab's user interface. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -268,15 +268,15 @@ configuration. Then customize everything from buildpacks to CI/CD. The following documentation relates to the DevOps **Configure** stage: -| Configure Topics | Description | -|:-------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------| -| [Auto DevOps](topics/autodevops/index.md) | Automatically employ a complete DevOps lifecycle. | -| [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) | Use Google Kubernetes Engine and GitLab. | -| [Executable Runbooks](user/project/clusters/runbooks/index.md) | Documented procedures that explain how to carry out particular processes. | -| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. | -| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. | -| [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. | -| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. | +| Configure Topics | Description | +|:-----------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------| +| [Auto DevOps](topics/autodevops/index.md) | Automatically employ a complete DevOps lifecycle. | +| [Easy creation of Kubernetes<br/>clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) | Use Google Kubernetes Engine and GitLab. | +| [Executable Runbooks](user/project/clusters/runbooks/index.md) | Documented procedures that explain how to carry out particular processes. | +| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. | +| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. | +| [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. | +| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index da70c74c4ce..fc03cf6cc39 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -408,6 +408,7 @@ Parameters: - `merge_request_iid` (required) - The internal ID of the merge request - `render_html` (optional) - If `true` response includes rendered HTML for title and description - `include_diverged_commits_count` (optional) - If `true` response includes the commits behind the target branch +- `include_rebase_in_progress` (optional) - If `true` response includes whether a rebase operation is in progress ```json { @@ -461,6 +462,7 @@ Parameters: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "user_notes_count": 1, @@ -505,7 +507,8 @@ Parameters: "head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f", "start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00" }, - "diverged_commits_count": 2 + "diverged_commits_count": 2, + "rebase_in_progress": false } ``` @@ -773,6 +776,7 @@ POST /projects/:id/merge_requests }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "user_notes_count": 1, @@ -900,6 +904,7 @@ Must include at least one non-required attribute from above. }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "user_notes_count": 1, @@ -1043,6 +1048,7 @@ Parameters: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", + "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "user_notes_count": 1, @@ -1158,6 +1164,7 @@ Parameters: }, "merge_when_pipeline_succeeds": false, "merge_status": "can_be_merged", + "merge_error": null, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "user_notes_count": 1, @@ -1206,6 +1213,62 @@ Parameters: } ``` +## Rebase a merge request + +Automatically rebase the `source_branch` of the merge request against its +`target_branch`. + +If you don't have permissions to push to the merge request's source branch - +you'll get a `403 Forbidden` response. + +``` +PUT /projects/:id/merge_requests/:merge_request_iid/rebase +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/rebase +``` + +This is an asynchronous request. The API will return an empty `202 Accepted` +response if the request is enqueued successfully. + +You can poll the [Get single MR](#get-single-mr) endpoint with the +`include_rebase_in_progress` parameter to check the status of the +asynchronous request. + +If the rebase operation is ongoing, the response will include the following: + +```json +{ + "rebase_in_progress": true + "merge_error": null +} +``` + +Once the rebase operation has completed successfully, the response will include +the following: + +```json +{ + "rebase_in_progress": false, + "merge_error": null, +} +``` + +If the rebase operation fails, the response will include the following: + +```json +{ + "rebase_in_progress": false, + "merge_error": "Rebase failed. Please rebase locally", +} +``` + ## Comments on merge requests Comments are done via the [notes](notes.md) resource. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 18c9cd116f1..a63656fafef 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -604,6 +604,8 @@ If you fail to restore this encryption key file along with the application data backup, users with two-factor authentication enabled and GitLab Runners will lose access to your GitLab server. +You may also want to restore any TLS keys, certificates, or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + Depending on your case, you might want to run the restore command with one or more of the following options: diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 6735710e2bb..2aa7c7ef815 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -238,13 +238,10 @@ by GitLab before installing any of the above applications. ## Getting the external IP address NOTE: **Note:** -You need a load balancer installed in your cluster in order to obtain the -external IP address with the following procedure. It can be deployed using the -[**Ingress** application](#installing-applications). - -NOTE: **Note:** -Knative will include its own load balancer in the form of [Istio](https://istio.io). -At this time, to determine the external IP address, you will need to follow the manual approach. +With the following procedure, a load balancer must be installed in your cluster +to obtain the external IP address. You can use either +[Ingress](#installing-applications), or Knative's own load balancer +([Istio](https://istio.io)) if using [Knative](#installing-applications). In order to publish your web application, you first need to find the external IP address associated to your load balancer. @@ -253,7 +250,7 @@ address associated to your load balancer. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6. -If you installed the Ingress [via the **Applications**](#installing-applications), +If you [installed Ingress or Knative](#installing-applications), you should see the Ingress IP address on this same page within a few minutes. If you don't see this, GitLab might not be able to determine the IP address of your ingress application in which case you should manually determine it. diff --git a/doc/user/project/merge_requests/img/merge_request_widget.png b/doc/user/project/merge_requests/img/merge_request_widget.png Binary files differindex 6c2317b29b5..58562fcb034 100644 --- a/doc/user/project/merge_requests/img/merge_request_widget.png +++ b/doc/user/project/merge_requests/img/merge_request_widget.png diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md index ecbc8534eea..ccef2853e3f 100644 --- a/doc/user/project/merge_requests/resolve_conflicts.md +++ b/doc/user/project/merge_requests/resolve_conflicts.md @@ -1,15 +1,31 @@ -# Merge conflict resolution +# Merge request conflict resolution -> [Introduced][ce-5479] in GitLab 8.11. +Merge conflicts occur when two branches have different changes that cannot be +merged automatically. -When a merge request has conflicts, GitLab may provide the option to resolve -those conflicts in the GitLab UI. (See -[conflicts available for resolution](#conflicts-available-for-resolution) for -more information on when this is available.) If this is an option, you will see -a **resolve these conflicts** link in the merge request widget: +Git is able to automatically merge changes between branches in most cases, but +there are situations where Git will require your assistance to resolve the +conflicts manually. Typically, this is necessary when people change the same +parts of the same files. + +GitLab will prevent merge requests from being merged until all conflicts are +resolved. Conflicts can be resolved locally, or in many cases within GitLab +(see [conflicts available for resolution](#conflicts-available-for-resolution) +for information on when this is available). ![Merge request widget](img/merge_request_widget.png) +NOTE: **Note:** +GitLab resolves conflicts by creating a merge commit in the source branch that +is not automatically merged into the target branch. This allows the merge +commit to be reviewed and tested before the changes are merged, preventing +unintended changes entering the target branch without review or breaking the +build. + +## Resolve conflicts: interactive mode + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479) in GitLab 8.11. + Clicking this will show a list of files with conflicts, with conflict sections highlighted: @@ -21,9 +37,9 @@ request into the source branch, resolving the conflicts using the options chosen. If the source branch is `feature` and the target branch is `master`, this is similar to performing `git checkout feature; git merge master` locally. -## Merge conflict editor +## Resolve conflicts: inline editor -> Introduced in GitLab 8.13. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6374) in GitLab 8.13. The merge conflict resolution editor allows for more complex merge conflicts, which require the user to manually modify a file in order to resolve a conflict, @@ -50,5 +66,3 @@ Additionally, GitLab does not detect conflicts in renames away from a path. For example, this will not create a conflict: on branch `a`, doing `git mv file1 file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be present in the branch after the merge request is merged. - -[ce-5479]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479 diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index c590ac4b0ba..020aa73f809 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -64,6 +64,8 @@ Below is the table of events users can be notified of: |------------------------------|-------------------------------------------------------------------|------------------------------| | New SSH key added | User | Security email, always sent. | | New email added | User | Security email, always sent. | +| Email changed | User | Security email, always sent. | +| Password changed | User | Security email, always sent. | | New user created | User | Sent on user creation, except for omniauth (LDAP)| | User added to project | User | Sent when user is added to project | | Project access level changed | User | Sent when user project access level is changed | diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4788b0e16a1..5dbfbb85e9e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -715,6 +715,10 @@ module API expose :diff_refs, using: Entities::DiffRefs + # Allow the status of a rebase to be determined + expose :merge_error + expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] } + expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] } def build_available?(options) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 16f07f16387..595b3641c52 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -74,6 +74,19 @@ module API options end + def authorize_push_to_merge_request!(merge_request) + forbidden!('Source branch does not exist') unless + merge_request.source_branch_exists? + + user_access = Gitlab::UserAccess.new( + current_user, + project: merge_request.source_project + ) + + forbidden!('Cannot push to source branch') unless + user_access.can_push_to_branch?(merge_request.source_branch) + end + params :merge_requests_params do optional :state, type: String, values: %w[opened closed locked merged all], default: 'all', desc: 'Return opened, closed, locked, merged, or all merge requests' @@ -239,6 +252,7 @@ module API requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' optional :render_html, type: Boolean, desc: 'Returns the description and title rendered HTML' optional :include_diverged_commits_count, type: Boolean, desc: 'Returns the commits count behind the target branch' + optional :include_rebase_in_progress, type: Boolean, desc: 'Returns whether a rebase operation is ongoing ' end desc 'Get a single merge request' do success Entities::MergeRequest @@ -246,7 +260,13 @@ module API get ':id/merge_requests/:merge_request_iid' do merge_request = find_merge_request_with_access(params[:merge_request_iid]) - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html], include_diverged_commits_count: params[:include_diverged_commits_count] + present merge_request, + with: Entities::MergeRequest, + current_user: current_user, + project: user_project, + render_html: params[:render_html], + include_diverged_commits_count: params[:include_diverged_commits_count], + include_rebase_in_progress: params[:include_rebase_in_progress] end desc 'Get the participants of a merge request' do @@ -378,6 +398,19 @@ module API .cancel(merge_request) end + desc 'Rebase the merge request against its target branch' do + detail 'This feature was added in GitLab 11.6' + end + put ':id/merge_requests/:merge_request_iid/rebase' do + merge_request = find_project_merge_request(params[:merge_request_iid]) + + authorize_push_to_merge_request!(merge_request) + + RebaseWorker.perform_async(merge_request.id, current_user.id) + + status :accepted + end + desc 'List issues that will be closed on merge' do success Entities::MRNote end diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb index a27f1d46863..c6a3a763c23 100644 --- a/lib/banzai/filter/spaced_link_filter.rb +++ b/lib/banzai/filter/spaced_link_filter.rb @@ -17,6 +17,9 @@ module Banzai # This is a small extension to the CommonMark spec. If they start allowing # spaces in urls, we could then remove this filter. # + # Note: Filter::SanitizationFilter should always be run sometime after this filter + # to prevent XSS attacks + # class SpacedLinkFilter < HTML::Pipeline::Filter include ActionView::Helpers::TagHelper diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index be75e34a673..96bea7ca935 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -12,13 +12,16 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::PlantumlFilter, + + # Must always be before the SanitizationFilter to prevent XSS attacks + Filter::SpacedLinkFilter, + Filter::SanitizationFilter, Filter::SyntaxHighlightFilter, Filter::MathFilter, Filter::ColorFilter, Filter::MermaidFilter, - Filter::SpacedLinkFilter, Filter::VideoLinkFilter, Filter::ImageLazyLoadFilter, Filter::ImageLinkFilter, diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb index cb9f2582936..176766d1a8b 100644 --- a/lib/gitlab/auth/request_authenticator.rb +++ b/lib/gitlab/auth/request_authenticator.rb @@ -13,12 +13,18 @@ module Gitlab @request = request end - def user - find_sessionless_user || find_user_from_warden + def user(request_formats) + request_formats.each do |format| + user = find_sessionless_user(format) + + return user if user + end + + find_user_from_warden end - def find_sessionless_user - find_user_from_access_token || find_user_from_feed_token + def find_sessionless_user(request_format) + find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format) rescue Gitlab::Auth::AuthenticationError nil end diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb index c304adc64db..adba9084845 100644 --- a/lib/gitlab/auth/user_auth_finders.rb +++ b/lib/gitlab/auth/user_auth_finders.rb @@ -27,8 +27,8 @@ module Gitlab current_request.env['warden']&.authenticate if verified_request? end - def find_user_from_feed_token - return unless rss_request? || ics_request? + def find_user_from_feed_token(request_format) + return unless valid_rss_format?(request_format) # NOTE: feed_token was renamed from rss_token but both needs to be supported because # users might have already added the feed to their RSS reader before the rename @@ -38,6 +38,17 @@ module Gitlab User.find_by_feed_token(token) || raise(UnauthorizedError) end + # We only allow Private Access Tokens with `api` scope to be used by web + # requests on RSS feeds or ICS files for backwards compatibility. + # It is also used by GraphQL/API requests. + def find_user_from_web_access_token(request_format) + return unless access_token && valid_web_access_format?(request_format) + + validate_access_token!(scopes: [:api]) + + access_token.user || raise(UnauthorizedError) + end + def find_user_from_access_token return unless access_token @@ -109,6 +120,26 @@ module Gitlab @current_request ||= ensure_action_dispatch_request(request) end + def valid_web_access_format?(request_format) + case request_format + when :rss + rss_request? + when :ics + ics_request? + when :api + api_request? + end + end + + def valid_rss_format?(request_format) + case request_format + when :rss + rss_request? + when :ics + ics_request? + end + end + def rss_request? current_request.path.ends_with?('.atom') || current_request.format.atom? end @@ -116,6 +147,10 @@ module Gitlab def ics_request? current_request.path.ends_with?('.ics') || current_request.format.ics? end + + def api_request? + current_request.path.starts_with?("/api/") + end end end end diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 86efe8ad114..b8040f73cee 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'resolv' +require 'ipaddress' module Gitlab class UrlBlocker @@ -10,11 +11,8 @@ module Gitlab def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: []) return true if url.nil? - begin - uri = Addressable::URI.parse(url) - rescue Addressable::URI::InvalidURIError - raise BlockedUrlError, "URI is invalid" - end + # Param url can be a string, URI or Addressable::URI + uri = parse_url(url) # Allow imports from the GitLab instance itself but only from the configured ports return true if internal?(uri) @@ -26,7 +24,9 @@ module Gitlab validate_hostname!(uri.hostname) begin - addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM) + addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr| + addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr + end rescue SocketError return true end @@ -49,6 +49,18 @@ module Gitlab private + def parse_url(url) + raise Addressable::URI::InvalidURIError if multiline?(url) + + Addressable::URI.parse(url) + rescue Addressable::URI::InvalidURIError, URI::InvalidURIError + raise BlockedUrlError, 'URI is invalid' + end + + def multiline?(url) + CGI.unescape(url.to_s) =~ /\n|\r/ + end + def validate_port!(port, ports) return if port.blank? # Only ports under 1024 are restricted @@ -73,13 +85,14 @@ module Gitlab def validate_hostname!(value) return if value.blank? + return if IPAddress.valid?(value) return if value =~ /\A\p{Alnum}/ - raise BlockedUrlError, "Hostname needs to start with an alphanumeric character" + raise BlockedUrlError, "Hostname or IP address invalid" end def validate_localhost!(addrs_info) - local_ips = ["127.0.0.1", "::1", "0.0.0.0"] + local_ips = ["::", "0.0.0.0"] local_ips.concat(Socket.ip_address_list.map(&:ip_address)) return if (local_ips & addrs_info.map(&:ip_address)).empty? @@ -94,7 +107,7 @@ module Gitlab end def validate_local_network!(addrs_info) - return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? } + return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? || addr.ipv6_unique_local? } raise BlockedUrlError, "Requests to the local network are not allowed" end @@ -111,12 +124,14 @@ module Gitlab end def internal_web?(uri) - uri.hostname == config.gitlab.host && + uri.scheme == config.gitlab.protocol && + uri.hostname == config.gitlab.host && (uri.port.blank? || uri.port == config.gitlab.port) end def internal_shell?(uri) - uri.hostname == config.gitlab_shell.ssh_host && + uri.scheme == 'ssh' && + uri.hostname == config.gitlab_shell.ssh_host && (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port) end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index e8ae5dfa540..451ba651674 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -6,7 +6,7 @@ namespace :gitlab do desc "GitLab | Cleanup | Clean namespaces" task dirs: :gitlab_environment do namespaces = Set.new(Namespace.pluck(:path)) - namespaces << Storage::HashedProject::ROOT_PATH_PREFIX + namespaces << Storage::HashedProject::REPOSITORY_PATH_PREFIX Gitaly::Server.all.each do |server| all_dirs = Gitlab::GitalyClient::StorageService @@ -49,7 +49,7 @@ namespace :gitlab do # TODO ignoring hashed repositories for now. But revisit to fully support # possible orphaned hashed repos - next if repo_with_namespace.start_with?(Storage::HashedProject::ROOT_PATH_PREFIX) + next if repo_with_namespace.start_with?(Storage::HashedProject::REPOSITORY_PATH_PREFIX) next if Project.find_by_full_path(repo_with_namespace) new_path = path + move_suffix diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a1413381b75..e6cc5ee79a0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -158,6 +158,9 @@ msgstr "" msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc." msgstr "" +msgid "%{user_name} profile page" +msgstr "" + msgid "+ %{count} more" msgstr "" @@ -1091,6 +1094,9 @@ msgstr "" msgid "Business metrics (Custom)" msgstr "" +msgid "By %{user_name}" +msgstr "" + msgid "ByAuthor|by" msgstr "" @@ -2582,6 +2588,9 @@ msgstr "" msgid "Edit application" msgstr "" +msgid "Edit comment" +msgstr "" + msgid "Edit environment" msgstr "" @@ -3544,6 +3553,9 @@ msgstr "" msgid "Interested parties can even contribute by pushing commits if they want to." msgstr "" +msgid "Internal" +msgstr "" + msgid "Internal - The group and any internal projects can be viewed by any logged in user." msgstr "" @@ -3646,7 +3658,7 @@ msgstr "" msgid "Job|The artifacts will be removed in" msgstr "" -msgid "Job|This job is stuck, because the project doesn't have any runners online assigned to it." +msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it." msgstr "" msgid "Jul" @@ -4372,6 +4384,9 @@ msgstr "" msgid "Notes|Show history only" msgstr "" +msgid "Nothing here." +msgstr "" + msgid "Notification events" msgstr "" @@ -4833,6 +4848,9 @@ msgstr "" msgid "Prioritized label" msgstr "" +msgid "Private" +msgstr "" + msgid "Private - Project access must be granted explicitly to each user." msgstr "" @@ -5259,6 +5277,9 @@ msgstr "" msgid "Provider" msgstr "" +msgid "Public" +msgstr "" + msgid "Public - The group and any public projects can be viewed without any authentication." msgstr "" @@ -6521,10 +6542,10 @@ msgstr "" msgid "This job is in pending state and is waiting to be picked by a runner" msgstr "" -msgid "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:" +msgid "This job is stuck because you don't have any active runners online with any of these tags assigned to them:" msgstr "" -msgid "This job is stuck, because you don't have any active runners that can run this job." +msgid "This job is stuck because you don't have any active runners that can run this job." msgstr "" msgid "This job is the most recent deployment to %{link}." diff --git a/qa/README.md b/qa/README.md index 746bd5cf94b..08ba59e117d 100644 --- a/qa/README.md +++ b/qa/README.md @@ -80,6 +80,15 @@ GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sa All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-environment-variables). +### Sending additional cookies + +The environment variable `QA_COOKIES` can be set to send additional cookies +on every request. This is necessary on gitlab.com to direct traffic to the +canary fleet. To do this set `QA_COOKIES="gitlab_canary=true"`. + +To set multiple cookies, separate them with the `;` character, for example: `QA_COOKIES="cookie1=value;cookie2=value2"` + + ### Building a Docker image to test Once you have made changes to the CE/EE repositories, you may want to build a diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 9aaf57e8d83..7fd2ba25527 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -117,6 +117,15 @@ module QA def perform(&block) visit(url) + if QA::Runtime::Env.qa_cookies + browser = Capybara.current_session.driver.browser + QA::Runtime::Env.qa_cookies.each do |cookie| + name, value = cookie.split("=") + value ||= "" + browser.manage.add_cookie name: name, value: value + end + end + yield.tap { clear! } if block_given? end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 7b2768548dd..3bc2b44ccd8 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -38,6 +38,10 @@ module QA ENV['CI'] || ENV['CI_SERVER'] end + def qa_cookies + ENV['QA_COOKIES'] && ENV['QA_COOKIES'].split(';') + end + def signup_disabled? enabled?(ENV['SIGNUP_DISABLED'], default: false) end diff --git a/rubocop/cop/migration/add_reference.rb b/rubocop/cop/migration/add_reference.rb index 4b67270c97a..1d471b9797e 100644 --- a/rubocop/cop/migration/add_reference.rb +++ b/rubocop/cop/migration/add_reference.rb @@ -8,7 +8,7 @@ module RuboCop class AddReference < RuboCop::Cop::Cop include MigrationHelpers - MSG = '`add_reference` requires `index: true`' + MSG = '`add_reference` requires `index: true` or `index: { options... }`' def on_send(node) return unless in_migration?(node) @@ -33,7 +33,12 @@ module RuboCop private def index_enabled?(pair) - hash_key_type(pair) == :sym && hash_key_name(pair) == :index && pair.children[1].true_type? + return unless hash_key_type(pair) == :sym + return unless hash_key_name(pair) == :index + + index = pair.children[1] + + index.true_type? || index.hash_type? end def hash_key_type(pair) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index efc3ce74627..1b585bcd4c6 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -107,59 +107,6 @@ describe ApplicationController do end end - describe "#authenticate_user_from_personal_access_token!" do - before do - stub_authentication_activity_metrics(debug: false) - end - - controller(described_class) do - def index - render text: 'authenticated' - end - end - - let(:personal_access_token) { create(:personal_access_token, user: user) } - - context "when the 'personal_access_token' param is populated with the personal access token" do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - get :index, private_token: personal_access_token.token - - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end - end - - context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - @request.headers["PRIVATE-TOKEN"] = personal_access_token.token - get :index - - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end - end - - it "doesn't log the user in otherwise" do - expect(authentication_metrics) - .to increment(:user_unauthenticated_counter) - - get :index, private_token: "token" - - expect(response.status).not_to eq(200) - expect(response.body).not_to eq('authenticated') - end - end - describe 'session expiration' do controller(described_class) do # The anonymous controller will report 401 and fail to run any actions. @@ -224,74 +171,6 @@ describe ApplicationController do end end - describe '#authenticate_sessionless_user!' do - before do - stub_authentication_activity_metrics(debug: false) - end - - describe 'authenticating a user from a feed token' do - controller(described_class) do - def index - render text: 'authenticated' - end - end - - context "when the 'feed_token' param is populated with the feed token" do - context 'when the request format is atom' do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - get :index, feed_token: user.feed_token, format: :atom - - expect(response).to have_gitlab_http_status 200 - expect(response.body).to eq 'authenticated' - end - end - - context 'when the request format is ics' do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - get :index, feed_token: user.feed_token, format: :ics - - expect(response).to have_gitlab_http_status 200 - expect(response.body).to eq 'authenticated' - end - end - - context 'when the request format is neither atom nor ics' do - it "doesn't log the user in" do - expect(authentication_metrics) - .to increment(:user_unauthenticated_counter) - - get :index, feed_token: user.feed_token - - expect(response.status).not_to have_gitlab_http_status 200 - expect(response.body).not_to eq 'authenticated' - end - end - end - - context "when the 'feed_token' param is populated with an invalid feed token" do - it "doesn't log the user" do - expect(authentication_metrics) - .to increment(:user_unauthenticated_counter) - - get :index, feed_token: 'token', format: :atom - - expect(response.status).not_to eq 200 - expect(response.body).not_to eq 'authenticated' - end - end - end - end - describe '#route_not_found' do it 'renders 404 if authenticated' do allow(controller).to receive(:current_user).and_return(user) @@ -557,36 +436,6 @@ describe ApplicationController do expect(response).to have_gitlab_http_status(200) end - - context 'for sessionless users' do - render_views - - before do - sign_out user - end - - it 'renders a 403 when the sessionless user did not accept the terms' do - get :index, feed_token: user.feed_token, format: :atom - - expect(response).to have_gitlab_http_status(403) - end - - it 'renders the error message when the format was html' do - get :index, - private_token: create(:personal_access_token, user: user).token, - format: :html - - expect(response.body).to have_content /accept the terms of service/i - end - - it 'renders a 200 when the sessionless user accepted the terms' do - accept_terms(user) - - get :index, feed_token: user.feed_token, format: :atom - - expect(response).to have_gitlab_http_status(200) - end - end end end diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb new file mode 100644 index 00000000000..2975205e09c --- /dev/null +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Dashboard::ProjectsController do + it_behaves_like 'authenticates sessionless user', :index, :atom +end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index b4a731fd3a3..e2c799f5205 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -42,6 +42,16 @@ describe Dashboard::TodosController do end end + context 'group authorization' do + it 'renders 404 when user does not have read access on given group' do + unauthorized_group = create(:group, :private) + + get :index, group_id: unauthorized_group.id + + expect(response).to have_gitlab_http_status(404) + end + end + context 'when using pagination' do let(:last_page) { user.todos.page.total_pages } let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) } diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index 187542ba30c..c857a78d5e8 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -1,21 +1,26 @@ require 'spec_helper' describe DashboardController do - let(:user) { create(:user) } - let(:project) { create(:project) } + context 'signed in' do + let(:user) { create(:user) } + let(:project) { create(:project) } - before do - project.add_maintainer(user) - sign_in(user) - end + before do + project.add_maintainer(user) + sign_in(user) + end - describe 'GET issues' do - it_behaves_like 'issuables list meta-data', :issue, :issues - it_behaves_like 'issuables requiring filter', :issues - end + describe 'GET issues' do + it_behaves_like 'issuables list meta-data', :issue, :issues + it_behaves_like 'issuables requiring filter', :issues + end - describe 'GET merge requests' do - it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests - it_behaves_like 'issuables requiring filter', :merge_requests + describe 'GET merge requests' do + it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests + it_behaves_like 'issuables requiring filter', :merge_requests + end end + + it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first + it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics end diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 1449036e148..949ad532365 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -52,15 +52,58 @@ describe GraphqlController do end end + context 'token authentication' do + before do + stub_authentication_activity_metrics(debug: false) + end + + let(:user) { create(:user, username: 'Simon') } + let(:personal_access_token) { create(:personal_access_token, user: user) } + + context "when the 'personal_access_token' param is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + run_test_query!(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(200) + expect(query_response).to eq('echo' => '"Simon" says: test success') + end + end + + context 'when the personal access token has no api scope' do + it 'does not log the user in' do + personal_access_token.update(scopes: [:read_user]) + + run_test_query!(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(200) + + expect(query_response).to eq('echo' => 'nil says: test success') + end + end + + context 'without token' do + it 'shows public data' do + run_test_query! + + expect(query_response).to eq('echo' => 'nil says: test success') + end + end + end + # Chosen to exercise all the moving parts in GraphqlController#execute - def run_test_query!(variables: { 'text' => 'test success' }) + def run_test_query!(variables: { 'text' => 'test success' }, private_token: nil) query = <<~QUERY query Echo($text: String) { echo(text: $text) } QUERY - post :execute, query: query, operationName: 'Echo', variables: variables + post :execute, query: query, operationName: 'Echo', variables: variables, private_token: private_token end def query_response diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 4de61b65f71..f6c85102830 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -606,4 +606,24 @@ describe GroupsController do end end end + + context 'token authentication' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do + before do + default_params.merge!(id: group) + end + end + + it_behaves_like 'authenticates sessionless user', :issues, :atom, public: true do + before do + default_params.merge!(id: group, author_id: user.id) + end + end + + it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics, public: true do + before do + default_params.merge!(id: group) + end + end + end end diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index ace8a954e92..b4219856fc0 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -40,6 +40,23 @@ describe Oauth::ApplicationsController do expect(response).to have_gitlab_http_status(302) expect(response).to redirect_to(profile_path) end + + context 'redirect_uri' do + render_views + + it 'shows an error for a forbidden URI' do + invalid_uri_params = { + doorkeeper_application: { + name: 'foo', + redirect_uri: 'javascript://alert()' + } + } + + post :create, invalid_uri_params + + expect(response.body).to include 'Redirect URI is forbidden by the server' + end + end end end diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index a43bdd3ea80..5c72dab698c 100644 --- a/spec/controllers/projects/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb @@ -5,87 +5,115 @@ describe Projects::CommitsController do let(:user) { create(:user) } before do - sign_in(user) project.add_maintainer(user) end - describe "GET commits_root" do - context "no ref is provided" do - it 'should redirect to the default branch of the project' do - get(:commits_root, - namespace_id: project.namespace, - project_id: project) + context 'signed in' do + before do + sign_in(user) + end + + describe "GET commits_root" do + context "no ref is provided" do + it 'should redirect to the default branch of the project' do + get(:commits_root, + namespace_id: project.namespace, + project_id: project) - expect(response).to redirect_to project_commits_path(project) + expect(response).to redirect_to project_commits_path(project) + end end end - end - describe "GET show" do - render_views + describe "GET show" do + render_views - context 'with file path' do - before do - get(:show, - namespace_id: project.namespace, - project_id: project, - id: id) - end + context 'with file path' do + before do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: id) + end - context "valid branch, valid file" do - let(:id) { 'master/README.md' } + context "valid branch, valid file" do + let(:id) { 'master/README.md' } - it { is_expected.to respond_with(:success) } - end + it { is_expected.to respond_with(:success) } + end - context "valid branch, invalid file" do - let(:id) { 'master/invalid-path.rb' } + context "valid branch, invalid file" do + let(:id) { 'master/invalid-path.rb' } - it { is_expected.to respond_with(:not_found) } - end + it { is_expected.to respond_with(:not_found) } + end - context "invalid branch, valid file" do - let(:id) { 'invalid-branch/README.md' } + context "invalid branch, valid file" do + let(:id) { 'invalid-branch/README.md' } - it { is_expected.to respond_with(:not_found) } + it { is_expected.to respond_with(:not_found) } + end end - end - context "when the ref name ends in .atom" do - context "when the ref does not exist with the suffix" do - before do - get(:show, - namespace_id: project.namespace, - project_id: project, - id: "master.atom") + context "when the ref name ends in .atom" do + context "when the ref does not exist with the suffix" do + before do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: "master.atom") + end + + it "renders as atom" do + expect(response).to be_success + expect(response.content_type).to eq('application/atom+xml') + end + + it 'renders summary with type=html' do + expect(response.body).to include('<summary type="html">') + end end - it "renders as atom" do - expect(response).to be_success - expect(response.content_type).to eq('application/atom+xml') - end + context "when the ref exists with the suffix" do + before do + commit = project.repository.commit('master') - it 'renders summary with type=html' do - expect(response.body).to include('<summary type="html">') + allow_any_instance_of(Repository).to receive(:commit).and_call_original + allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit) + + get(:show, + namespace_id: project.namespace, + project_id: project, + id: "master.atom") + end + + it "renders as HTML" do + expect(response).to be_success + expect(response.content_type).to eq('text/html') + end end end + end + end - context "when the ref exists with the suffix" do + context 'token authentication' do + context 'public project' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do before do - commit = project.repository.commit('master') + public_project = create(:project, :repository, :public) - allow_any_instance_of(Repository).to receive(:commit).and_call_original - allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit) - - get(:show, - namespace_id: project.namespace, - project_id: project, - id: "master.atom") + default_params.merge!(namespace_id: public_project.namespace, project_id: public_project, id: "master.atom") end + end + end + + context 'private project' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: false do + before do + private_project = create(:project, :repository, :private) + private_project.add_maintainer(user) - it "renders as HTML" do - expect(response).to be_success - expect(response.content_type).to eq('text/html') + default_params.merge!(namespace_id: private_project.namespace, project_id: private_project, id: "master.atom") end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 80138183c07..02930edbf72 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1068,4 +1068,40 @@ describe Projects::IssuesController do end end end + + context 'private project with token authentication' do + let(:private_project) { create(:project, :private) } + + it_behaves_like 'authenticates sessionless user', :index, :atom do + before do + default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + + it_behaves_like 'authenticates sessionless user', :calendar, :ics do + before do + default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + end + + context 'public project with token authentication' do + let(:public_project) { create(:project, :public) } + + it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do + before do + default_params.merge!(project_id: public_project, namespace_id: public_project.namespace) + end + end + + it_behaves_like 'authenticates sessionless user', :calendar, :ics, public: true do + before do + default_params.merge!(project_id: public_project, namespace_id: public_project.namespace) + end + end + end end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index ccd4fc4db3a..658aa2a6738 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -143,11 +143,27 @@ describe Projects::MilestonesController do end describe '#promote' do + let(:group) { create(:group) } + + before do + project.update(namespace: group) + end + + context 'when user does not have permission to promote milestone' do + before do + group.add_guest(user) + end + + it 'renders 404' do + post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid + + expect(response).to have_gitlab_http_status(404) + end + end + context 'promotion succeeds' do before do - group = create(:group) group.add_developer(user) - milestone.project.update(namespace: group) end it 'shows group milestone' do @@ -166,12 +182,17 @@ describe Projects::MilestonesController do end end - context 'promotion fails' do - it 'shows project milestone' do + context 'when user cannot admin group milestones' do + before do + project.add_developer(user) + end + + it 'renders 404' do + project.update(namespace: user.namespace) + post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid - expect(response).to redirect_to(project_milestone_path(project, milestone)) - expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.') + expect(response).to have_gitlab_http_status(404) end end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 9ac7b8ee8a8..d2a26068362 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -283,14 +283,14 @@ describe Projects::NotesController do def post_create(extra_params = {}) post :create, { - note: { note: 'some other note' }, - namespace_id: project.namespace, - project_id: project, - target_type: 'merge_request', - target_id: merge_request.id, - note_project_id: forked_project.id, - in_reply_to_discussion_id: existing_comment.discussion_id - }.merge(extra_params) + note: { note: 'some other note', noteable_id: merge_request.id }, + namespace_id: project.namespace, + project_id: project, + target_type: 'merge_request', + target_id: merge_request.id, + note_project_id: forked_project.id, + in_reply_to_discussion_id: existing_comment.discussion_id + }.merge(extra_params) end context 'when the note_project_id is not correct' do @@ -324,6 +324,30 @@ describe Projects::NotesController do end end + context 'when target_id and noteable_id do not match' do + let(:locked_issue) { create(:issue, :locked, project: project) } + let(:issue) {create(:issue, project: project)} + + before do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + project.project_member(user).destroy + end + + it 'uses target_id and ignores noteable_id' do + request_params = { + note: { note: 'some note', noteable_type: 'Issue', noteable_id: locked_issue.id }, + target_type: 'issue', + target_id: issue.id, + project_id: project, + namespace_id: project.namespace + } + + expect { post :create, request_params }.to change { issue.notes.count }.by(1) + .and change { locked_issue.notes.count }.by(0) + expect(response).to have_gitlab_http_status(302) + end + end + context 'when the merge request discussion is locked' do before do project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) @@ -376,35 +400,60 @@ describe Projects::NotesController do end describe 'PUT update' do - let(:request_params) do - { - namespace_id: project.namespace, - project_id: project, - id: note, - format: :json, - note: { - note: "New comment" + context "should update the note with a valid issue" do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } } - } - end + end - before do - sign_in(note.author) - project.add_developer(note.author) + before do + sign_in(note.author) + project.add_developer(note.author) + end + + it "updates the note" do + expect { put :update, request_params }.to change { note.reload.note } + end end + context "doesnt update the note" do + let(:issue) { create(:issue, :confidential, project: project) } + let(:note) { create(:note, noteable: issue, project: project) } - it "updates the note" do - expect { put :update, request_params }.to change { note.reload.note } + before do + sign_in(user) + project.add_guest(user) + end + + it "disallows edits when the issue is confidential and the user has guest permissions" do + request_params = { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } + } + expect { put :update, request_params }.not_to change { note.reload.note } + expect(response).to have_gitlab_http_status(404) + end end end describe 'DELETE destroy' do let(:request_params) do { - namespace_id: project.namespace, - project_id: project, - id: note, - format: :js + namespace_id: project.namespace, + project_id: project, + id: note, + format: :js } end diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index c48f41ca12e..6fbf75d0259 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -35,4 +35,26 @@ describe Projects::TagsController do it { is_expected.to respond_with(:not_found) } end end + + context 'private project with token authentication' do + let(:private_project) { create(:project, :repository, :private) } + + it_behaves_like 'authenticates sessionless user', :index, :atom do + before do + default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + end + + context 'public project with token authentication' do + let(:public_project) { create(:project, :repository, :public) } + + it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do + before do + default_params.merge!(project_id: public_project, namespace_id: public_project.namespace) + end + end + end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 3bc9cbe64c5..7849bec4762 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -882,6 +882,28 @@ describe ProjectsController do end end + context 'private project with token authentication' do + let(:private_project) { create(:project, :private) } + + it_behaves_like 'authenticates sessionless user', :show, :atom do + before do + default_params.merge!(id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + end + + context 'public project with token authentication' do + let(:public_project) { create(:project, :public) } + + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do + before do + default_params.merge!(id: public_project, namespace_id: public_project.namespace) + end + end + end + def project_moved_message(redirect_route, project) "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path." end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 071f96a729e..fe438e71e9e 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -395,6 +395,14 @@ describe UsersController do end end + context 'token authentication' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do + before do + default_params.merge!(username: user.username) + end + end + end + def user_moved_message(redirect_route, user) "User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path." end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 90754319f05..07c1fc31152 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -308,7 +308,7 @@ FactoryBot.define do trait :with_runner_session do after(:build) do |build| - build.build_runner_session(url: 'ws://localhost') + build.build_runner_session(url: 'https://localhost') end end end diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb new file mode 100644 index 00000000000..2ed0844ed47 --- /dev/null +++ b/spec/factories/pool_repositories.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :pool_repository do + shard + end +end diff --git a/spec/factories/shards.rb b/spec/factories/shards.rb new file mode 100644 index 00000000000..c095fa5f0a0 --- /dev/null +++ b/spec/factories/shards.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :shard do + name "default" + end +end diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb index ba5b80ed04b..b4b9a589ba3 100644 --- a/spec/features/issues/user_comments_on_issue_spec.rb +++ b/spec/features/issues/user_comments_on_issue_spec.rb @@ -40,6 +40,18 @@ describe "User comments on issue", :js do expect(page.find('pre code').text).to eq code_block_content end + + it "does not render html content in mermaid" do + html_content = "<img onerror=location=`javascript\\u003aalert\\u0028document.domain\\u0029` src=x>" + mermaid_content = "graph LR\n B-->D(#{html_content});" + comment = "```mermaid\n#{mermaid_content}\n```" + + add_note(comment) + + wait_for_requests + + expect(page.find('svg.mermaid')).to have_content html_content + end end context "when editing comments" do diff --git a/spec/features/issues/user_sees_breadcrumb_links_spec.rb b/spec/features/issues/user_sees_breadcrumb_links_spec.rb index ca234321235..43369f7609f 100644 --- a/spec/features/issues/user_sees_breadcrumb_links_spec.rb +++ b/spec/features/issues/user_sees_breadcrumb_links_spec.rb @@ -1,15 +1,15 @@ require 'rails_helper' -describe 'New issue breadcrumbs' do +describe 'New issue breadcrumb' do let(:project) { create(:project) } - let(:user) { project.creator } + let(:user) { project.creator } before do sign_in(user) - visit new_project_issue_path(project) + visit(new_project_issue_path(project)) end - it 'display a link to project issues and new issue pages' do + it 'displays link to project issues and new issue' do page.within '.breadcrumbs' do expect(find_link('Issues')[:href]).to end_with(project_issues_path(project)) expect(find_link('New')[:href]).to end_with(new_project_issue_path(project)) diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb index a25d701ee35..7008b361394 100644 --- a/spec/features/markdown/mermaid_spec.rb +++ b/spec/features/markdown/mermaid_spec.rb @@ -18,7 +18,7 @@ describe 'Mermaid rendering', :js do visit project_issue_path(project, issue) %w[A B C D].each do |label| - expect(page).to have_selector('svg foreignObject', text: label) + expect(page).to have_selector('svg text', text: label) end end end diff --git a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb index f17acb35a5a..18d204da17a 100644 --- a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb +++ b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb @@ -1,15 +1,15 @@ require 'rails_helper' -describe 'New merge request breadcrumbs' do +describe 'New merge request breadcrumb' do let(:project) { create(:project, :repository) } - let(:user) { project.creator } + let(:user) { project.creator } before do sign_in(user) - visit project_new_merge_request_path(project) + visit(project_new_merge_request_path(project)) end - it 'display a link to project merge requests and new merge request pages' do + it 'displays link to project merge requests and new merge request' do page.within '.breadcrumbs' do expect(find_link('Merge Requests')[:href]).to end_with(project_merge_requests_path(project)) expect(find_link('New')[:href]).to end_with(project_new_merge_request_path(project)) diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index 74290c0fff9..3e40179ad9a 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -65,7 +65,20 @@ describe 'Merge request > User sees deployment widget', :js do visit project_merge_request_path(project, merge_request) wait_for_requests - expect(page).to have_content("Deploying to #{environment.name}") + expect(page).to have_content("Will deploy to #{environment.name}") + expect(page).not_to have_css('.js-deploy-time') + end + end + + context 'when deployment was cancelled' do + let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + let!(:deployment) { create(:deployment, :canceled, environment: environment, sha: sha, ref: ref, deployable: build) } + + it 'displays that the environment name' do + visit project_merge_request_path(project, merge_request) + wait_for_requests + + expect(page).to have_content("Failed to deploy to #{environment.name}") expect(page).not_to have_css('.js-deploy-time') end end diff --git a/spec/features/milestones/user_promotes_milestone_spec.rb b/spec/features/milestones/user_promotes_milestone_spec.rb new file mode 100644 index 00000000000..df1bc502134 --- /dev/null +++ b/spec/features/milestones/user_promotes_milestone_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' + +describe 'User promotes milestone' do + set(:group) { create(:group) } + set(:user) { create(:user) } + set(:project) { create(:project, namespace: group) } + set(:milestone) { create(:milestone, project: project) } + + context 'when user can admin group milestones' do + before do + group.add_developer(user) + sign_in(user) + visit(project_milestones_path(project)) + end + + it "shows milestone promote button" do + expect(page).to have_selector('.js-promote-project-milestone-button') + end + end + + context 'when user cannot admin group milestones' do + before do + project.add_developer(user) + sign_in(user) + visit(project_milestones_path(project)) + end + + it "does not show milestone promote button" do + expect(page).not_to have_selector('.js-promote-project-milestone-button') + end + end +end diff --git a/spec/features/milestones/user_sees_breadcrumb_links_spec.rb b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb new file mode 100644 index 00000000000..d3906ea73bd --- /dev/null +++ b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe 'New project milestone breadcrumb' do + let(:project) { create(:project) } + let(:milestone) { create(:milestone, project: project) } + let(:user) { project.creator } + + before do + sign_in(user) + visit(new_project_milestone_path(project)) + end + + it 'displays link to project milestones and new project milestone' do + page.within '.breadcrumbs' do + expect(find_link('Milestones')[:href]).to end_with(project_milestones_path(project)) + expect(find_link('New')[:href]).to end_with(new_project_milestone_path(project)) + end + end +end diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb index 534cfe1eb12..2159adf49fc 100644 --- a/spec/features/projects/commits/user_browses_commits_spec.rb +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -4,10 +4,9 @@ describe 'User browses commits' do include RepoHelpers let(:user) { create(:user) } - let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:project) { create(:project, :public, :repository, namespace: user.namespace) } before do - project.add_maintainer(user) sign_in(user) end @@ -127,6 +126,26 @@ describe 'User browses commits' do .and have_selector('entry summary', text: commit.description[0..10].delete("\r\n")) end + context 'when a commit links to a confidential issue' do + let(:confidential_issue) { create(:issue, confidential: true, title: 'Secret issue!', project: project) } + + before do + project.repository.create_file(user, 'dummy-file', 'dummy content', + branch_name: 'feature', + message: "Linking #{confidential_issue.to_reference}") + end + + context 'when the user cannot see confidential issues but was cached with a link', :use_clean_rails_memory_store_fragment_caching do + it 'does not render the confidential issue' do + visit project_commits_path(project, 'feature') + sign_in(create(:user)) + visit project_commits_path(project, 'feature') + + expect(page).not_to have_link(href: project_issue_path(project, confidential_issue)) + end + end + end + context 'master branch' do before do visit_commits_page diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 24352be592a..d7c4abffddd 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -754,7 +754,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because no runners are active' do expect(page).to have_css('.js-stuck-no-active-runner') - expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + expect(page).to have_content("This job is stuck because you don't have any active runners that can run this job.") end end @@ -764,7 +764,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because of no runners with the specified tags' do expect(page).to have_css('.js-stuck-with-tags') - expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + expect(page).to have_content("This job is stuck because you don't have any active runners online with any of these tags assigned to them:") end end @@ -774,7 +774,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because of no runners with the specified tags' do expect(page).to have_css('.js-stuck-with-tags') - expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + expect(page).to have_content("This job is stuck because you don't have any active runners online with any of these tags assigned to them:") end end @@ -783,7 +783,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because not runners are available' do expect(page).to have_css('.js-stuck-no-active-runner') - expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + expect(page).to have_content("This job is stuck because you don't have any active runners that can run this job.") end end @@ -793,7 +793,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because runners are offline' do expect(page).to have_css('.js-stuck-no-runners') - expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.") + expect(page).to have_content("This job is stuck because the project doesn't have any runners online assigned to it.") end end end diff --git a/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb new file mode 100644 index 00000000000..0c0501f438a --- /dev/null +++ b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe 'New project label breadcrumb' do + let(:project) { create(:project) } + let(:user) { project.creator } + + before do + sign_in(user) + visit(project_labels_path(project)) + end + + it 'displays link to project labels and new project label' do + page.within '.breadcrumbs' do + expect(find_link('Labels')[:href]).to end_with(project_labels_path(project)) + end + end +end diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 091edf13cfe..7de38913bae 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -123,7 +123,7 @@ describe('Api', () => { }); }); - describe('mergerequest', () => { + describe('projectMergeRequest', () => { it('fetches a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -132,7 +132,7 @@ describe('Api', () => { title: 'test', }); - Api.mergeRequest(projectPath, mergeRequestId) + Api.projectMergeRequest(projectPath, mergeRequestId) .then(({ data }) => { expect(data.title).toBe('test'); }) @@ -141,7 +141,7 @@ describe('Api', () => { }); }); - describe('mergerequest changes', () => { + describe('projectMergeRequestChanges', () => { it('fetches the changes of a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -150,7 +150,7 @@ describe('Api', () => { title: 'test', }); - Api.mergeRequestChanges(projectPath, mergeRequestId) + Api.projectMergeRequestChanges(projectPath, mergeRequestId) .then(({ data }) => { expect(data.title).toBe('test'); }) @@ -159,7 +159,7 @@ describe('Api', () => { }); }); - describe('mergerequest versions', () => { + describe('projectMergeRequestVersions', () => { it('fetches the versions of a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -170,7 +170,7 @@ describe('Api', () => { }, ]); - Api.mergeRequestVersions(projectPath, mergeRequestId) + Api.projectMergeRequestVersions(projectPath, mergeRequestId) .then(({ data }) => { expect(data.length).toBe(1); expect(data[0].id).toBe(123); diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js index 04925d37ac4..9e2ba1f5ce9 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js @@ -14,10 +14,14 @@ import testAction from '../../../../helpers/vuex_action_helper'; describe('IDE merge requests actions', () => { let mockedState; + let mockedRootState; let mock; beforeEach(() => { mockedState = state(); + mockedRootState = { + currentProjectId: 7, + }; mock = new MockAdapter(axios); }); @@ -86,13 +90,16 @@ describe('IDE merge requests actions', () => { describe('success', () => { beforeEach(() => { - mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests); + mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(200, mergeRequests); }); it('calls API with params', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); - fetchMergeRequests({ dispatch() {}, state: mockedState }, { type: 'created' }); + fetchMergeRequests( + { dispatch() {}, state: mockedState, rootState: mockedRootState }, + { type: 'created' }, + ); expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { params: { @@ -107,7 +114,7 @@ describe('IDE merge requests actions', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); fetchMergeRequests( - { dispatch() {}, state: mockedState }, + { dispatch() {}, state: mockedState, rootState: mockedRootState }, { type: 'created', search: 'testing search' }, ); @@ -139,6 +146,49 @@ describe('IDE merge requests actions', () => { }); }); + describe('success without type', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/).replyOnce(200, mergeRequests); + }); + + it('calls API with project', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchMergeRequests( + { dispatch() {}, state: mockedState, rootState: mockedRootState }, + { type: null, search: 'testing search' }, + ); + + expect(apiSpy).toHaveBeenCalledWith( + jasmine.stringMatching(`projects/${mockedRootState.currentProjectId}/merge_requests`), + { + params: { + state: 'opened', + search: 'testing search', + }, + }, + ); + }); + + it('dispatches success with received data', done => { + testAction( + fetchMergeRequests, + { type: null }, + { ...mockedState, ...mockedRootState }, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { + type: 'receiveMergeRequestsSuccess', + payload: mergeRequests, + }, + ], + done, + ); + }); + }); + describe('error', () => { beforeEach(() => { mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index fcf3780f0ea..ba5d672f189 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -160,9 +160,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners that can run this job.", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-no-active-runner')).not.toBeNull(); done(); }, 0); }); @@ -195,9 +193,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); done(); }, 0); }); @@ -230,9 +226,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); done(); }, 0); }); diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index df24cef0b8b..91b0499375d 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -104,5 +104,17 @@ describe Banzai::Pipeline::GfmPipeline do expect(output).to include("src=\"test%20image.png\"") end + + it 'sanitizes the fixed link' do + markdown_xss = "[xss](javascript: alert%28document.domain%29)" + output = described_class.to_html(markdown_xss, project: project) + + expect(output).not_to include("javascript") + + markdown_xss = "<invalidtag>\n[xss](javascript:alert%28document.domain%29)" + output = described_class.to_html(markdown_xss, project: project) + + expect(output).not_to include("javascript") + end end end diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb index 242ab4a91dd..3d979132880 100644 --- a/spec/lib/gitlab/auth/request_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -19,17 +19,17 @@ describe Gitlab::Auth::RequestAuthenticator do allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user) allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) - expect(subject.user).to eq sessionless_user + expect(subject.user([:api])).to eq sessionless_user end it 'returns session user if no sessionless user found' do allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) - expect(subject.user).to eq session_user + expect(subject.user([:api])).to eq session_user end it 'returns nil if no user found' do - expect(subject.user).to be_blank + expect(subject.user([:api])).to be_blank end it 'bubbles up exceptions' do @@ -42,26 +42,26 @@ describe Gitlab::Auth::RequestAuthenticator do let!(:feed_token_user) { build(:user) } it 'returns access_token user first' do - allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user) + allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user) allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) - expect(subject.find_sessionless_user).to eq access_token_user + expect(subject.find_sessionless_user([:api])).to eq access_token_user end it 'returns feed_token user if no access_token user found' do allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) - expect(subject.find_sessionless_user).to eq feed_token_user + expect(subject.find_sessionless_user([:api])).to eq feed_token_user end it 'returns nil if no user found' do - expect(subject.find_sessionless_user).to be_blank + expect(subject.find_sessionless_user([:api])).to be_blank end it 'rescue Gitlab::Auth::AuthenticationError exceptions' do - allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError) + allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_raise(Gitlab::Auth::UnauthorizedError) - expect(subject.find_sessionless_user).to be_blank + expect(subject.find_sessionless_user([:api])).to be_blank end end end diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb index 454ad1589b9..5d3fbba264f 100644 --- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Auth::UserAuthFinders do 'rack.input' => '' } end - let(:request) { Rack::Request.new(env)} + let(:request) { Rack::Request.new(env) } def set_param(key, value) request.update_param(key, value) @@ -49,6 +49,7 @@ describe Gitlab::Auth::UserAuthFinders do describe '#find_user_from_feed_token' do context 'when the request format is atom' do before do + env['SCRIPT_NAME'] = 'url.atom' env['HTTP_ACCEPT'] = 'application/atom+xml' end @@ -56,17 +57,17 @@ describe Gitlab::Auth::UserAuthFinders do it 'returns user if valid feed_token' do set_param(:feed_token, user.feed_token) - expect(find_user_from_feed_token).to eq user + expect(find_user_from_feed_token(:rss)).to eq user end it 'returns nil if feed_token is blank' do - expect(find_user_from_feed_token).to be_nil + expect(find_user_from_feed_token(:rss)).to be_nil end it 'returns exception if invalid feed_token' do set_param(:feed_token, 'invalid_token') - expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError) end end @@ -74,34 +75,38 @@ describe Gitlab::Auth::UserAuthFinders do it 'returns user if valid rssd_token' do set_param(:rss_token, user.feed_token) - expect(find_user_from_feed_token).to eq user + expect(find_user_from_feed_token(:rss)).to eq user end it 'returns nil if rss_token is blank' do - expect(find_user_from_feed_token).to be_nil + expect(find_user_from_feed_token(:rss)).to be_nil end it 'returns exception if invalid rss_token' do set_param(:rss_token, 'invalid_token') - expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError) end end end context 'when the request format is not atom' do it 'returns nil' do + env['SCRIPT_NAME'] = 'json' + set_param(:feed_token, user.feed_token) - expect(find_user_from_feed_token).to be_nil + expect(find_user_from_feed_token(:rss)).to be_nil end end context 'when the request format is empty' do it 'the method call does not modify the original value' do + env['SCRIPT_NAME'] = 'url.atom' + env.delete('action_dispatch.request.formats') - find_user_from_feed_token + find_user_from_feed_token(:rss) expect(env['action_dispatch.request.formats']).to be_nil end @@ -111,8 +116,12 @@ describe Gitlab::Auth::UserAuthFinders do describe '#find_user_from_access_token' do let(:personal_access_token) { create(:personal_access_token, user: user) } + before do + env['SCRIPT_NAME'] = 'url.atom' + end + it 'returns nil if no access_token present' do - expect(find_personal_access_token).to be_nil + expect(find_user_from_access_token).to be_nil end context 'when validate_access_token! returns valid' do @@ -131,9 +140,59 @@ describe Gitlab::Auth::UserAuthFinders do end end + describe '#find_user_from_web_access_token' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + before do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + end + + it 'returns exception if token has no user' do + allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil) + + expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + + context 'no feed or API requests' do + it 'returns nil if the request is not RSS' do + expect(find_user_from_web_access_token(:rss)).to be_nil + end + + it 'returns nil if the request is not ICS' do + expect(find_user_from_web_access_token(:ics)).to be_nil + end + + it 'returns nil if the request is not API' do + expect(find_user_from_web_access_token(:api)).to be_nil + end + end + + it 'returns the user for RSS requests' do + env['SCRIPT_NAME'] = 'url.atom' + + expect(find_user_from_web_access_token(:rss)).to eq(user) + end + + it 'returns the user for ICS requests' do + env['SCRIPT_NAME'] = 'url.ics' + + expect(find_user_from_web_access_token(:ics)).to eq(user) + end + + it 'returns the user for API requests' do + env['SCRIPT_NAME'] = '/api/endpoint' + + expect(find_user_from_web_access_token(:api)).to eq(user) + end + end + describe '#find_personal_access_token' do let(:personal_access_token) { create(:personal_access_token, user: user) } + before do + env['SCRIPT_NAME'] = 'url.atom' + end + context 'passed as header' do it 'returns token if valid personal_access_token' do env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 8df0facdab3..39e0a17a307 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -10,8 +10,8 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?(import_url)).to be false end - it 'allows imports from configured SSH host and port' do - import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git" + it 'allows mirroring from configured SSH host and port' do + import_url = "ssh://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git" expect(described_class.blocked_url?(import_url)).to be false end @@ -29,24 +29,46 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true end + it 'returns true for bad protocol on configured web/SSH host and ports' do + web_url = "javascript://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git%0aalert(1)" + expect(described_class.blocked_url?(web_url)).to be true + + ssh_url = "javascript://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git%0aalert(1)" + expect(described_class.blocked_url?(ssh_url)).to be true + end + it 'returns true for localhost IPs' do + expect(described_class.blocked_url?('https://[0:0:0:0:0:0:0:0]/foo/foo.git')).to be true expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true - expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true - expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::]/foo/foo.git')).to be true end it 'returns true for loopback IP' do expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true end it 'returns true for alternative version of 127.0.0.1 (0177.1)' do expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true end + it 'returns true for alternative version of 127.0.0.1 (017700000001)' do + expect(described_class.blocked_url?('https://017700000001:65535/foo/foo.git')).to be true + end + it 'returns true for alternative version of 127.0.0.1 (0x7f.1)' do expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true end + it 'returns true for alternative version of 127.0.0.1 (0x7f.0.0.1)' do + expect(described_class.blocked_url?('https://0x7f.0.0.1:65535/foo/foo.git')).to be true + end + + it 'returns true for alternative version of 127.0.0.1 (0x7f000001)' do + expect(described_class.blocked_url?('https://0x7f000001:65535/foo/foo.git')).to be true + end + it 'returns true for alternative version of 127.0.0.1 (2130706433)' do expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true end @@ -55,6 +77,27 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true end + it 'returns true for alternative version of 127.0.0.1 (127.0.1)' do + expect(described_class.blocked_url?('https://127.0.1:65535/foo/foo.git')).to be true + end + + context 'with ipv6 mapped address' do + it 'returns true for localhost IPs' do + expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:0.0.0.0]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:0.0.0.0]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:0:0]/foo/foo.git')).to be true + end + + it 'returns true for loopback IPs' do + expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:127.0.0.1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:7f00:1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.2]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:127.0.0.2]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:7f00:2]/foo/foo.git')).to be true + end + end + it 'returns true for a non-alphanumeric hostname' do stub_resolv @@ -78,7 +121,22 @@ describe Gitlab::UrlBlocker do end context 'when allow_local_network is' do - let(:local_ips) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] } + let(:local_ips) do + [ + '192.168.1.2', + '[0:0:0:0:0:ffff:192.168.1.2]', + '[::ffff:c0a8:102]', + '10.0.0.2', + '[0:0:0:0:0:ffff:10.0.0.2]', + '[::ffff:a00:2]', + '172.16.0.2', + '[0:0:0:0:0:ffff:172.16.0.2]', + '[::ffff:ac10:20]', + '[feef::1]', + '[fee2::]', + '[fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa]' + ] + end let(:fake_domain) { 'www.fakedomain.fake' } context 'true (default)' do @@ -109,10 +167,14 @@ describe Gitlab::UrlBlocker do expect(described_class).not_to be_blocked_url('http://169.254.168.100') end - # This is blocked due to the hostname check: https://gitlab.com/gitlab-org/gitlab-ce/issues/50227 - it 'blocks IPv6 link-local endpoints' do - expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]') - expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]') + it 'allows IPv6 link-local endpoints' do + expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]') + expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]') + expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]') + expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]') + expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]') + expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]') + expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]') end end @@ -135,14 +197,20 @@ describe Gitlab::UrlBlocker do end it 'blocks IPv6 link-local endpoints' do + expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', allow_local_network: false) expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a9fe]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', allow_local_network: false) expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]', allow_local_network: false) - expect(described_class).to be_blocked_url('http://[FE80::C800:EFF:FE74:8]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a864]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false) end end def stub_domain_resolv(domain, ip) - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)]) + address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false) + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address]) + allow(address).to receive(:ipv6_v4mapped?).and_return(false) end def unstub_domain_resolv @@ -183,6 +251,36 @@ describe Gitlab::UrlBlocker do end end + describe '#validate_hostname!' do + let(:ip_addresses) do + [ + '2001:db8:1f70::999:de8:7648:6e8', + 'FE80::C800:EFF:FE74:8', + '::ffff:127.0.0.1', + '::ffff:169.254.168.100', + '::ffff:7f00:1', + '0:0:0:0:0:ffff:0.0.0.0', + 'localhost', + '127.0.0.1', + '127.000.000.001', + '0x7f000001', + '0x7f.0.0.1', + '0x7f.0.0.1', + '017700000001', + '0177.1', + '2130706433', + '::', + '::1' + ] + end + + it 'does not raise error for valid Ip addresses' do + ip_addresses.each do |ip| + expect { described_class.send(:validate_hostname!, ip) }.not_to raise_error + end + end + end + # Resolv does not support resolving UTF-8 domain names # See https://bugs.ruby-lang.org/issues/4270 def stub_resolv diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ff1a5aa2536..150c00e4bfe 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -522,7 +522,7 @@ describe Notify do let(:project_snippet) { create(:project_snippet, project: project) } let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) } - subject { described_class.note_snippet_email(project_snippet_note.author_id, project_snippet_note.id) } + subject { described_class.note_project_snippet_email(project_snippet_note.author_id, project_snippet_note.id) } it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { project_snippet } diff --git a/spec/migrations/cleanup_environments_external_url_spec.rb b/spec/migrations/cleanup_environments_external_url_spec.rb new file mode 100644 index 00000000000..07ddaf3d38f --- /dev/null +++ b/spec/migrations/cleanup_environments_external_url_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20181108091549_cleanup_environments_external_url.rb') + +describe CleanupEnvironmentsExternalUrl, :migration do + let(:environments) { table(:environments) } + let(:invalid_entries) { environments.where(environments.arel_table[:external_url].matches('javascript://%')) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + before do + namespace = namespaces.create(name: 'foo', path: 'foo') + project = projects.create!(namespace_id: namespace.id) + + environments.create!(id: 1, project_id: project.id, name: 'poisoned', slug: 'poisoned', external_url: 'javascript://alert("1")') + end + + it 'clears every environment with a javascript external_url' do + expect do + subject.up + end.to change { invalid_entries.count }.from(1).to(0) + end + + it 'do not removes environments' do + expect do + subject.up + end.not_to change { environments.count } + end +end diff --git a/spec/migrations/migrate_forbidden_redirect_uris_spec.rb b/spec/migrations/migrate_forbidden_redirect_uris_spec.rb new file mode 100644 index 00000000000..0bc13a3974a --- /dev/null +++ b/spec/migrations/migrate_forbidden_redirect_uris_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181026091631_migrate_forbidden_redirect_uris.rb') + +describe MigrateForbiddenRedirectUris, :migration do + let(:oauth_application) { table(:oauth_applications) } + let(:oauth_access_grant) { table(:oauth_access_grants) } + + let!(:control_app) { oauth_application.create(random_params) } + let!(:control_access_grant) { oauth_application.create(random_params) } + let!(:forbidden_js_app) { oauth_application.create(random_params.merge(redirect_uri: 'javascript://alert()')) } + let!(:forbidden_vb_app) { oauth_application.create(random_params.merge(redirect_uri: 'VBSCRIPT://alert()')) } + let!(:forbidden_access_grant) { oauth_application.create(random_params.merge(redirect_uri: 'vbscript://alert()')) } + + context 'oauth application' do + it 'migrates forbidden javascript URI' do + expect { migrate! }.to change { forbidden_js_app.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten') + end + + it 'migrates forbidden VBScript URI' do + expect { migrate! }.to change { forbidden_vb_app.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten') + end + + it 'does not migrate a valid URI' do + expect { migrate! }.not_to change { control_app.reload.redirect_uri } + end + end + + context 'access grant' do + it 'migrates forbidden VBScript URI' do + expect { migrate! }.to change { forbidden_access_grant.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten') + end + + it 'does not migrate a valid URI' do + expect { migrate! }.not_to change { control_access_grant.reload.redirect_uri } + end + end + + def random_params + { + name: 'test', + secret: 'test', + uid: Doorkeeper::OAuth::Helpers::UniqueToken.generate, + redirect_uri: 'http://valid.com' + } + end +end diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index 59c861a74db..859287bb0c8 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -436,32 +436,47 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data_store) { :redis } context 'when data exists' do - let(:data) { 'Sample data in redis' } - before do build_trace_chunk.send(:unsafe_set_data!, data) end - it 'persists the data' do - expect(build_trace_chunk.redis?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + context 'when data size reached CHUNK_SIZE' do + let(:data) { 'a' * described_class::CHUNK_SIZE } - subject + it 'persists the data' do + expect(build_trace_chunk.redis?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + + subject - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' end - it_behaves_like 'Atomic operation' + context 'when data size has not reached CHUNK_SIZE' do + let(:data) { 'Sample data in redis' } + + it 'does not persist the data and the orignal data is intact' do + expect { subject }.to raise_error(described_class::FailedToPersistDataError) + + expect(build_trace_chunk.redis?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + end + end end context 'when data does not exist' do it 'does not persist' do - expect { subject }.to raise_error('Can not persist empty data') + expect { subject }.to raise_error(described_class::FailedToPersistDataError) end end end @@ -470,32 +485,47 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data_store) { :database } context 'when data exists' do - let(:data) { 'Sample data in database' } - before do build_trace_chunk.send(:unsafe_set_data!, data) end - it 'persists the data' do - expect(build_trace_chunk.database?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) - expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + context 'when data size reached CHUNK_SIZE' do + let(:data) { 'a' * described_class::CHUNK_SIZE } - subject + it 'persists the data' do + expect(build_trace_chunk.database?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + subject + + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' end - it_behaves_like 'Atomic operation' + context 'when data size has not reached CHUNK_SIZE' do + let(:data) { 'Sample data in database' } + + it 'does not persist the data and the orignal data is intact' do + expect { subject }.to raise_error(described_class::FailedToPersistDataError) + + expect(build_trace_chunk.database?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + end + end end context 'when data does not exist' do it 'does not persist' do - expect { subject }.to raise_error('Can not persist empty data') + expect { subject }.to raise_error(described_class::FailedToPersistDataError) end end end @@ -504,27 +534,37 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data_store) { :fog } context 'when data exists' do - let(:data) { 'Sample data in fog' } - before do build_trace_chunk.send(:unsafe_set_data!, data) end - it 'does not change data store' do - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + context 'when data size reached CHUNK_SIZE' do + let(:data) { 'a' * described_class::CHUNK_SIZE } - subject + it 'does not change data store' do + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + subject + + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' end - it_behaves_like 'Atomic operation' + context 'when data size has not reached CHUNK_SIZE' do + let(:data) { 'Sample data in fog' } + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + end end end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index f9be61e4768..bcdfe3cf1eb 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -517,7 +517,7 @@ describe Note do describe '#to_ability_name' do it 'returns snippet for a project snippet note' do - expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet') + expect(build(:note_on_project_snippet).to_ability_name).to eq('project_snippet') end it 'returns personal_snippet for a personal snippet note' do diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb new file mode 100644 index 00000000000..6c904710fb5 --- /dev/null +++ b/spec/models/pool_repository_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe PoolRepository do + describe 'associations' do + it { is_expected.to belong_to(:shard) } + it { is_expected.to have_many(:member_projects) } + end + + describe 'validations' do + let!(:pool_repository) { create(:pool_repository) } + + it { is_expected.to validate_presence_of(:shard) } + end + + describe '#disk_path' do + it 'sets the hashed disk_path' do + pool = create(:pool_repository) + + elements = File.split(pool.disk_path) + + expect(elements).to all( match(/\d{2,}/) ) + end + end +end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index f2cb927df37..b6cf4c72450 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -13,6 +13,23 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do it { is_expected.to belong_to :project } end + context 'redirects' do + it 'does not follow redirects' do + redirect_to = 'https://redirected.example.com' + redirect_req_stub = stub_prometheus_request(prometheus_query_url('1'), status: 302, headers: { location: redirect_to }) + redirected_req_stub = stub_prometheus_request(redirect_to, body: { 'status': 'success' }) + + result = service.test + + # result = { success: false, result: error } + expect(result[:success]).to be_falsy + expect(result[:result]).to be_instance_of(Gitlab::PrometheusClient::Error) + + expect(redirect_req_stub).to have_been_requested + expect(redirected_req_stub).not_to have_been_requested + end + end + describe 'Validations' do context 'when manual_configuration is enabled' do before do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ca7d13ea5a4..e98c69e636a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -218,76 +218,93 @@ describe Project do end end - it 'does not allow an invalid URI as import_url' do - project = build(:project, import_url: 'invalid://') + describe 'import_url' do + it 'does not allow an invalid URI as import_url' do + project = build(:project, import_url: 'invalid://') - expect(project).not_to be_valid - end + expect(project).not_to be_valid + end - it 'does allow a SSH URI as import_url for persisted projects' do - project = create(:project) - project.import_url = 'ssh://test@gitlab.com/project.git' + it 'does allow a SSH URI as import_url for persisted projects' do + project = create(:project) + project.import_url = 'ssh://test@gitlab.com/project.git' - expect(project).to be_valid - end + expect(project).to be_valid + end - it 'does not allow a SSH URI as import_url for new projects' do - project = build(:project, import_url: 'ssh://test@gitlab.com/project.git') + it 'does not allow a SSH URI as import_url for new projects' do + project = build(:project, import_url: 'ssh://test@gitlab.com/project.git') - expect(project).not_to be_valid - end + expect(project).not_to be_valid + end - it 'does allow a valid URI as import_url' do - project = build(:project, import_url: 'http://gitlab.com/project.git') + it 'does allow a valid URI as import_url' do + project = build(:project, import_url: 'http://gitlab.com/project.git') - expect(project).to be_valid - end + expect(project).to be_valid + end - it 'allows an empty URI' do - project = build(:project, import_url: '') + it 'allows an empty URI' do + project = build(:project, import_url: '') - expect(project).to be_valid - end + expect(project).to be_valid + end - it 'does not produce import data on an empty URI' do - project = build(:project, import_url: '') + it 'does not produce import data on an empty URI' do + project = build(:project, import_url: '') - expect(project.import_data).to be_nil - end + expect(project.import_data).to be_nil + end - it 'does not produce import data on an invalid URI' do - project = build(:project, import_url: 'test://') + it 'does not produce import data on an invalid URI' do + project = build(:project, import_url: 'test://') - expect(project.import_data).to be_nil - end + expect(project.import_data).to be_nil + end - it "does not allow import_url pointing to localhost" do - project = build(:project, import_url: 'http://localhost:9000/t.git') + it "does not allow import_url pointing to localhost" do + project = build(:project, import_url: 'http://localhost:9000/t.git') - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed') - end + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed') + end - it "does not allow import_url with invalid ports for new projects" do - project = build(:project, import_url: 'http://github.com:25/t.git') + it "does not allow import_url with invalid ports for new projects" do + project = build(:project, import_url: 'http://github.com:25/t.git') - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443') - end + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443') + end - it "does not allow import_url with invalid ports for persisted projects" do - project = create(:project) - project.import_url = 'http://github.com:25/t.git' + it "does not allow import_url with invalid ports for persisted projects" do + project = create(:project) + project.import_url = 'http://github.com:25/t.git' - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443') - end + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443') + end + + it "does not allow import_url with invalid user" do + project = build(:project, import_url: 'http://$user:password@github.com/t.git') + + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Username needs to start with an alphanumeric character') + end - it "does not allow import_url with invalid user" do - project = build(:project, import_url: 'http://$user:password@github.com/t.git') + include_context 'invalid urls' - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Username needs to start with an alphanumeric character') + it 'does not allow urls with CR or LF characters' do + project = build(:project) + + aggregate_failures do + urls_with_CRLF.each do |url| + project.import_url = url + + expect(project).not_to be_valid + expect(project.errors.full_messages.first).to match(/is blocked: URI is invalid/) + end + end + end end describe 'project pending deletion' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 187283b284b..f09b4b67061 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1488,6 +1488,7 @@ describe Repository do :size, :commit_count, :rendered_readme, + :readme_path, :contribution_guide, :changelog, :license_blob, @@ -1874,6 +1875,42 @@ describe Repository do end end + describe '#readme_path', :use_clean_rails_memory_store_caching do + context 'with a non-existing repository' do + let(:project) { create(:project) } + + it 'returns nil' do + expect(repository.readme_path).to be_nil + end + end + + context 'with an existing repository' do + context 'when no README exists' do + let(:project) { create(:project, :empty_repo) } + + it 'returns nil' do + expect(repository.readme_path).to be_nil + end + end + + context 'when a README exists' do + let(:project) { create(:project, :repository) } + + it 'returns the README' do + expect(repository.readme_path).to eq("README.md") + end + + it 'caches the response' do + expect(repository).to receive(:readme).and_call_original.once + + 2.times do + expect(repository.readme_path).to eq("README.md") + end + end + end + end + end + describe '#expire_statistics_caches' do it 'expires the caches' do expect(repository).to receive(:expire_method_caches) @@ -2042,9 +2079,10 @@ describe Repository do describe '#refresh_method_caches' do it 'refreshes the caches of the given types' do expect(repository).to receive(:expire_method_caches) - .with(%i(rendered_readme license_blob license_key license)) + .with(%i(rendered_readme readme_path license_blob license_key license)) expect(repository).to receive(:rendered_readme) + expect(repository).to receive(:readme_path) expect(repository).to receive(:license_blob) expect(repository).to receive(:license_key) expect(repository).to receive(:license) diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index e8096358f7d..7e25c53e77c 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -10,11 +10,50 @@ describe NotePolicy, mdoels: true do return @policies if @policies noteable ||= issue - note = create(:note, noteable: noteable, author: user, project: project) + note = if noteable.is_a?(Commit) + create(:note_on_commit, commit_id: noteable.id, author: user, project: project) + else + create(:note, noteable: noteable, author: user, project: project) + end @policies = described_class.new(user, note) end + shared_examples_for 'a discussion with a private noteable' do + let(:noteable) { issue } + let(:policy) { policies(noteable) } + + context 'when the note author can no longer see the noteable' do + it 'can not edit nor read the note' do + expect(policy).to be_disallowed(:admin_note) + expect(policy).to be_disallowed(:resolve_note) + expect(policy).to be_disallowed(:read_note) + end + end + + context 'when the note author can still see the noteable' do + before do + project.add_developer(user) + end + + it 'can edit the note' do + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) + end + end + end + + context 'when the project is private' do + let(:project) { create(:project, :private, :repository) } + + context 'when the noteable is a commit' do + it_behaves_like 'a discussion with a private noteable' do + let(:noteable) { project.repository.head_commit } + end + end + end + context 'when the project is public' do context 'when the note author is not a project member' do it 'can edit a note' do @@ -24,14 +63,48 @@ describe NotePolicy, mdoels: true do end end - context 'when the noteable is a snippet' do + context 'when the noteable is a project snippet' do + it 'can edit note' do + policies = policies(create(:project_snippet, :public, project: project)) + + expect(policies).to be_allowed(:admin_note) + expect(policies).to be_allowed(:resolve_note) + expect(policies).to be_allowed(:read_note) + end + + context 'when it is private' do + it_behaves_like 'a discussion with a private noteable' do + let(:noteable) { create(:project_snippet, :private, project: project) } + end + end + end + + context 'when the noteable is a personal snippet' do it 'can edit note' do - policies = policies(create(:project_snippet, project: project)) + policies = policies(create(:personal_snippet, :public)) expect(policies).to be_allowed(:admin_note) expect(policies).to be_allowed(:resolve_note) expect(policies).to be_allowed(:read_note) end + + context 'when it is private' do + it 'can not edit nor read the note' do + policies = policies(create(:personal_snippet, :private)) + + expect(policies).to be_disallowed(:admin_note) + expect(policies).to be_disallowed(:resolve_note) + expect(policies).to be_disallowed(:read_note) + end + end + end + + context 'when a discussion is confidential' do + before do + issue.update_attribute(:confidential, true) + end + + it_behaves_like 'a discussion with a private noteable' end context 'when a discussion is locked' do diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb index 270e12bf201..6154be5c425 100644 --- a/spec/requests/api/applications_spec.rb +++ b/spec/requests/api/applications_spec.rb @@ -25,7 +25,7 @@ describe API::Applications, :api do it 'does not allow creating an application with the wrong redirect_uri format' do expect do - post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: '' + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://', scopes: '' end.not_to change { Doorkeeper::Application.count } expect(response).to have_gitlab_http_status(400) @@ -33,6 +33,16 @@ describe API::Applications, :api do expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.') end + it 'does not allow creating an application with a forbidden URI format' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'javascript://alert()', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to be_a Hash + expect(json_response['message']['redirect_uri'][0]).to eq('is forbidden by the server.') + end + it 'does not allow creating an application without a name' do expect do post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: '' diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e4e0ca285e0..27bcde77860 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -359,6 +359,8 @@ describe API::MergeRequests do expect(json_response['should_close_merge_request']).to be_falsy expect(json_response['force_close_merge_request']).to be_falsy expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size) + expect(json_response['merge_error']).to eq(merge_request.merge_error) + expect(json_response).not_to include('rebase_in_progress') end it 'exposes description and title html when render_html is true' do @@ -369,6 +371,14 @@ describe API::MergeRequests do expect(json_response).to include('title_html', 'description_html') end + it 'exposes rebase_in_progress when include_rebase_in_progress is true' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), include_rebase_in_progress: true + + expect(response).to have_gitlab_http_status(200) + + expect(json_response).to include('rebase_in_progress') + end + context 'merge_request_metrics' do before do merge_request.metrics.update!(merged_by: user, @@ -1181,6 +1191,26 @@ describe API::MergeRequests do end end + describe 'PUT :id/merge_requests/:merge_request_iid/rebase' do + it 'enqueues a rebase of the merge request against the target branch' do + Sidekiq::Testing.fake! do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user) + end + + expect(response).to have_gitlab_http_status(202) + expect(RebaseWorker.jobs.size).to eq(1) + end + + it 'returns 403 if the user cannot push to the branch' do + guest = create(:user) + project.add_guest(guest) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", guest) + + expect(response).to have_gitlab_http_status(403) + end + end + describe 'Time tracking' do let(:issuable) { merge_request } diff --git a/spec/rubocop/cop/migration/add_reference_spec.rb b/spec/rubocop/cop/migration/add_reference_spec.rb index 8f795bb561e..c348fc0efac 100644 --- a/spec/rubocop/cop/migration/add_reference_spec.rb +++ b/spec/rubocop/cop/migration/add_reference_spec.rb @@ -29,7 +29,7 @@ describe RuboCop::Cop::Migration::AddReference do expect_offense(<<~RUBY) call do add_reference(:projects, :users) - ^^^^^^^^^^^^^ `add_reference` requires `index: true` + ^^^^^^^^^^^^^ `add_reference` requires `index: true` or `index: { options... }` end RUBY end @@ -38,7 +38,7 @@ describe RuboCop::Cop::Migration::AddReference do expect_offense(<<~RUBY) def up add_reference(:projects, :users, index: false) - ^^^^^^^^^^^^^ `add_reference` requires `index: true` + ^^^^^^^^^^^^^ `add_reference` requires `index: true` or `index: { options... }` end RUBY end @@ -50,5 +50,13 @@ describe RuboCop::Cop::Migration::AddReference do end RUBY end + + it 'does not register an offense when the index is unique' do + expect_no_offenses(<<~RUBY) + def up + add_reference(:projects, :users, index: { unique: true } ) + end + RUBY + end end end diff --git a/spec/support/controllers/sessionless_auth_controller_shared_examples.rb b/spec/support/controllers/sessionless_auth_controller_shared_examples.rb new file mode 100644 index 00000000000..7e4958f177a --- /dev/null +++ b/spec/support/controllers/sessionless_auth_controller_shared_examples.rb @@ -0,0 +1,92 @@ +shared_examples 'authenticates sessionless user' do |path, format, params| + params ||= {} + + before do + stub_authentication_activity_metrics(debug: false) + end + + let(:user) { create(:user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:default_params) { { format: format }.merge(params.except(:public) || {}) } + + context "when the 'personal_access_token' param is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + get path, default_params.merge(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(200) + expect(controller.current_user).to eq(user) + end + + it 'does not log the user in if page is public', if: params[:public] do + get path, default_params + + expect(response).to have_gitlab_http_status(200) + expect(controller.current_user).to be_nil + end + end + + context 'when the personal access token has no api scope', unless: params[:public] do + it 'does not log the user in' do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + + personal_access_token.update(scopes: [:read_user]) + + get path, default_params.merge(private_token: personal_access_token.token) + + expect(response).not_to have_gitlab_http_status(200) + end + end + + context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + @request.headers['PRIVATE-TOKEN'] = personal_access_token.token + get path, default_params + + expect(response).to have_gitlab_http_status(200) + end + end + + context "when the 'feed_token' param is populated with the feed token", if: format == :rss do + it "logs the user in" do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + get path, default_params.merge(feed_token: user.feed_token) + + expect(response).to have_gitlab_http_status 200 + end + end + + context "when the 'feed_token' param is populated with an invalid feed token", if: format == :rss, unless: params[:public] do + it "logs the user" do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + + get path, default_params.merge(feed_token: 'token') + + expect(response.status).not_to eq 200 + end + end + + it "doesn't log the user in otherwise", unless: params[:public] do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + + get path, default_params.merge(private_token: 'token') + + expect(response.status).not_to eq(200) + end +end diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb index 4212be2cc88..ce1f9fce10d 100644 --- a/spec/support/helpers/prometheus_helpers.rb +++ b/spec/support/helpers/prometheus_helpers.rb @@ -49,11 +49,11 @@ module PrometheusHelpers "https://prometheus.example.com/api/v1/series?#{query}" end - def stub_prometheus_request(url, body: {}, status: 200) + def stub_prometheus_request(url, body: {}, status: 200, headers: {}) WebMock.stub_request(:get, url) .to_return({ status: status, - headers: { 'Content-Type' => 'application/json' }, + headers: { 'Content-Type' => 'application/json' }.merge(headers), body: body.to_json }) end diff --git a/spec/support/shared_contexts/url_shared_context.rb b/spec/support/shared_contexts/url_shared_context.rb new file mode 100644 index 00000000000..1b1f67daac3 --- /dev/null +++ b/spec/support/shared_contexts/url_shared_context.rb @@ -0,0 +1,17 @@ +shared_context 'invalid urls' do + let(:urls_with_CRLF) do + ["http://127.0.0.1:333/pa\rth", + "http://127.0.0.1:333/pa\nth", + "http://127.0a.0.1:333/pa\r\nth", + "http://127.0.0.1:333/path?param=foo\r\nbar", + "http://127.0.0.1:333/path?param=foo\rbar", + "http://127.0.0.1:333/path?param=foo\nbar", + "http://127.0.0.1:333/pa%0dth", + "http://127.0.0.1:333/pa%0ath", + "http://127.0a.0.1:333/pa%0d%0th", + "http://127.0.0.1:333/pa%0D%0Ath", + "http://127.0.0.1:333/path?param=foo%0Abar", + "http://127.0.0.1:333/path?param=foo%0Dbar", + "http://127.0.0.1:333/path?param=foo%0D%0Abar"] + end +end diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index ab6100509a6..082d09d3f16 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe UrlValidator do @@ -6,6 +8,30 @@ describe UrlValidator do include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS + describe 'validations' do + include_context 'invalid urls' + + let(:validator) { described_class.new(attributes: [:link_url]) } + + it 'returns error when url is nil' do + expect(validator.validate_each(badge, :link_url, nil)).to be_nil + expect(badge.errors.first[1]).to eq 'must be a valid URL' + end + + it 'returns error when url is empty' do + expect(validator.validate_each(badge, :link_url, '')).to be_nil + expect(badge.errors.first[1]).to eq 'must be a valid URL' + end + + it 'does not allow urls with CR or LF characters' do + aggregate_failures do + urls_with_CRLF.each do |url| + expect(validator.validate_each(badge, :link_url, url)[0]).to eq 'is blocked: URI is invalid' + end + end + end + end + context 'by default' do let(:validator) { described_class.new(attributes: [:link_url]) } |