New upstream version 11.3.11+dfsg
This commit is contained in:
parent
b5b5f601e2
commit
0c554a36ea
82 changed files with 1429 additions and 466 deletions
39
CHANGELOG.md
39
CHANGELOG.md
|
@ -2,6 +2,45 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 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)
|
## 11.3.10 (2018-11-18)
|
||||||
|
|
||||||
### Security (1 change)
|
### Security (1 change)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1.1.0
|
1.1.1
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
6.1.0
|
6.1.2
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
11.3.10
|
11.3.11
|
||||||
|
|
|
@ -25,6 +25,9 @@ export default function renderMermaid($els) {
|
||||||
},
|
},
|
||||||
// mermaidAPI options
|
// mermaidAPI options
|
||||||
theme: 'neutral',
|
theme: 'neutral',
|
||||||
|
flowchart: {
|
||||||
|
htmlLabels: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
$els.each((i, el) => {
|
$els.each((i, el) => {
|
||||||
|
|
|
@ -10,8 +10,8 @@ class ApplicationController < ActionController::Base
|
||||||
include WorkhorseHelper
|
include WorkhorseHelper
|
||||||
include EnforcesTwoFactorAuthentication
|
include EnforcesTwoFactorAuthentication
|
||||||
include WithPerformanceBar
|
include WithPerformanceBar
|
||||||
|
include SessionlessAuthentication
|
||||||
|
|
||||||
before_action :authenticate_sessionless_user!
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :enforce_terms!, if: :should_enforce_terms?
|
before_action :enforce_terms!, if: :should_enforce_terms?
|
||||||
before_action :validate_user_service_ticket!
|
before_action :validate_user_service_ticket!
|
||||||
|
@ -140,13 +140,6 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
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)
|
def log_exception(exception)
|
||||||
Raven.capture_exception(exception) if sentry_enabled?
|
Raven.capture_exception(exception) if sentry_enabled?
|
||||||
|
|
||||||
|
@ -412,25 +405,11 @@ class ApplicationController < ActionController::Base
|
||||||
Gitlab::I18n.with_user_locale(current_user, &block)
|
Gitlab::I18n.with_user_locale(current_user, &block)
|
||||||
end
|
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
|
def set_page_title_header
|
||||||
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
|
# 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'))
|
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def sessionless_user?
|
|
||||||
current_user && !session.keys.include?('warden.user.user.key')
|
|
||||||
end
|
|
||||||
|
|
||||||
def peek_request?
|
def peek_request?
|
||||||
request.path.start_with?('/-/peek')
|
request.path.start_with?('/-/peek')
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ module NotesActions
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
prepend_before_action :normalize_create_params, only: [:create]
|
||||||
before_action :set_polling_interval_header, only: [:index]
|
before_action :set_polling_interval_header, only: [:index]
|
||||||
before_action :require_noteable!, only: [:index, :create]
|
before_action :require_noteable!, only: [:index, :create]
|
||||||
before_action :authorize_admin_note!, only: [:update, :destroy]
|
before_action :authorize_admin_note!, only: [:update, :destroy]
|
||||||
|
@ -216,6 +217,15 @@ module NotesActions
|
||||||
ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
|
ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
|
||||||
end
|
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
|
def note_project
|
||||||
strong_memoize(:note_project) do
|
strong_memoize(:note_project) do
|
||||||
next nil unless project
|
next nil unless project
|
||||||
|
|
28
app/controllers/concerns/sessionless_authentication.rb
Normal file
28
app/controllers/concerns/sessionless_authentication.rb
Normal file
|
@ -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
|
|
@ -2,6 +2,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
|
||||||
include ParamsBackwardCompatibility
|
include ParamsBackwardCompatibility
|
||||||
include RendersMemberAccess
|
include RendersMemberAccess
|
||||||
|
|
||||||
|
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
|
||||||
before_action :set_non_archived_param
|
before_action :set_non_archived_param
|
||||||
before_action :default_sorting
|
before_action :default_sorting
|
||||||
skip_cross_project_access_check :index, :starred
|
skip_cross_project_access_check :index, :starred
|
||||||
|
|
|
@ -2,6 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
||||||
include ActionView::Helpers::NumberHelper
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
before_action :authorize_read_project!, only: :index
|
before_action :authorize_read_project!, only: :index
|
||||||
|
before_action :authorize_read_group!, only: :index
|
||||||
before_action :find_todos, only: [:index, :destroy_all]
|
before_action :find_todos, only: [:index, :destroy_all]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -58,6 +59,15 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
||||||
end
|
end
|
||||||
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
|
def find_todos
|
||||||
@todos ||= TodosFinder.new(current_user, todo_params).execute
|
@todos ||= TodosFinder.new(current_user, todo_params).execute
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,9 @@ class DashboardController < Dashboard::ApplicationController
|
||||||
:label_name
|
:label_name
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
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 :event_filter, only: :activity
|
||||||
before_action :projects, only: [:issues, :merge_requests]
|
before_action :projects, only: [:issues, :merge_requests]
|
||||||
before_action :set_show_full_reference, only: [:issues, :merge_requests]
|
before_action :set_show_full_reference, only: [:issues, :merge_requests]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
class GraphqlController < ApplicationController
|
class GraphqlController < ApplicationController
|
||||||
# Unauthenticated users have access to the API for public data
|
# Unauthenticated users have access to the API for public data
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
prepend_before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
|
||||||
|
|
||||||
before_action :check_graphql_feature_flag!
|
before_action :check_graphql_feature_flag!
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,9 @@ class GroupsController < Groups::ApplicationController
|
||||||
|
|
||||||
respond_to :html
|
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 :authenticate_user!, only: [:new, :create]
|
||||||
before_action :group, except: [:index, :new, :create]
|
before_action :group, except: [:index, :new, :create]
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
||||||
before_action :verify_user_oauth_applications_enabled
|
before_action :verify_user_oauth_applications_enabled
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :add_gon_variables
|
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?
|
helper_method :can?
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ class Projects::CommitsController < Projects::ApplicationController
|
||||||
include ExtractsPath
|
include ExtractsPath
|
||||||
include RendersCommits
|
include RendersCommits
|
||||||
|
|
||||||
|
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||||
before_action :whitelist_query_limiting, except: :commits_root
|
before_action :whitelist_query_limiting, except: :commits_root
|
||||||
before_action :require_non_empty_project
|
before_action :require_non_empty_project
|
||||||
before_action :assign_ref_vars, except: :commits_root
|
before_action :assign_ref_vars, except: :commits_root
|
||||||
|
|
|
@ -7,7 +7,10 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
include IssuesCalendar
|
include IssuesCalendar
|
||||||
include SpammableActions
|
include SpammableActions
|
||||||
|
|
||||||
prepend_before_action :authenticate_user!, only: [:new]
|
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 :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
|
||||||
before_action :check_issues_available!
|
before_action :check_issues_available!
|
||||||
|
@ -213,16 +216,18 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
] + [{ label_ids: [], assignee_ids: [] }]
|
] + [{ label_ids: [], assignee_ids: [] }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_user!
|
def authenticate_new_issue!
|
||||||
return if current_user
|
return if current_user
|
||||||
|
|
||||||
notice = "Please sign in to create the new issue."
|
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?
|
if request.get? && !request.xhr?
|
||||||
store_location_for :user, request.fullpath
|
store_location_for :user, request.fullpath
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to new_user_session_path, notice: notice
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def serializer
|
def serializer
|
||||||
|
|
|
@ -9,7 +9,10 @@ class Projects::MilestonesController < Projects::ApplicationController
|
||||||
before_action :authorize_read_milestone!
|
before_action :authorize_read_milestone!
|
||||||
|
|
||||||
# Allow admin 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
|
respond_to :html
|
||||||
|
|
||||||
|
@ -76,7 +79,7 @@ class Projects::MilestonesController < Projects::ApplicationController
|
||||||
|
|
||||||
def promote
|
def promote
|
||||||
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
|
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|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
|
@ -112,6 +115,12 @@ class Projects::MilestonesController < Projects::ApplicationController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def project_group
|
||||||
|
strong_memoize(:project_group) do
|
||||||
|
project.group
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def milestones
|
def milestones
|
||||||
strong_memoize(:milestones) do
|
strong_memoize(:milestones) do
|
||||||
MilestonesFinder.new(search_params).execute
|
MilestonesFinder.new(search_params).execute
|
||||||
|
@ -126,13 +135,17 @@ class Projects::MilestonesController < Projects::ApplicationController
|
||||||
return render_404 unless can?(current_user, :admin_milestone, @project)
|
return render_404 unless can?(current_user, :admin_milestone, @project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def authorize_promote_milestone!
|
||||||
|
return render_404 unless can?(current_user, :admin_milestone, project_group)
|
||||||
|
end
|
||||||
|
|
||||||
def milestone_params
|
def milestone_params
|
||||||
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
|
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_params
|
def search_params
|
||||||
if request.format.json? && @project.group && can?(current_user, :read_group, @project.group)
|
if request.format.json? && project_group && can?(current_user, :read_group, project_group)
|
||||||
groups = @project.group.self_and_ancestors_ids
|
groups = project_group.self_and_ancestors_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
|
params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
class Projects::TagsController < Projects::ApplicationController
|
class Projects::TagsController < Projects::ApplicationController
|
||||||
include SortingHelper
|
include SortingHelper
|
||||||
|
|
||||||
|
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
|
||||||
|
|
||||||
# Authorize
|
# Authorize
|
||||||
before_action :require_non_empty_project
|
before_action :require_non_empty_project
|
||||||
before_action :authorize_download_code!
|
before_action :authorize_download_code!
|
||||||
|
|
|
@ -5,6 +5,8 @@ class ProjectsController < Projects::ApplicationController
|
||||||
include PreviewMarkdown
|
include PreviewMarkdown
|
||||||
include SendFileUpload
|
include SendFileUpload
|
||||||
|
|
||||||
|
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||||
|
|
||||||
before_action :whitelist_query_limiting, only: [:create]
|
before_action :whitelist_query_limiting, only: [:create]
|
||||||
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
|
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
|
||||||
before_action :redirect_git_extension, only: [:show]
|
before_action :redirect_git_extension, only: [:show]
|
||||||
|
|
|
@ -12,6 +12,7 @@ class UsersController < ApplicationController
|
||||||
calendar_activities: true
|
calendar_activities: true
|
||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||||
before_action :user, except: [:exists]
|
before_action :user, except: [:exists]
|
||||||
before_action :authorize_read_user_profile!,
|
before_action :authorize_read_user_profile!,
|
||||||
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
|
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module MilestonesHelper
|
module MilestonesHelper
|
||||||
include EntityDateHelper
|
include EntityDateHelper
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
def milestones_filter_path(opts = {})
|
def milestones_filter_path(opts = {})
|
||||||
if @project
|
if @project
|
||||||
|
@ -241,4 +242,16 @@ module MilestonesHelper
|
||||||
dashboard_milestone_path(milestone.safe_title, title: milestone.title)
|
dashboard_milestone_path(milestone.safe_title, title: milestone.title)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -26,7 +26,7 @@ module Emails
|
||||||
mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id))
|
mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id))
|
||||||
end
|
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)
|
setup_note_mail(note_id, recipient_id)
|
||||||
|
|
||||||
@snippet = @note.noteable
|
@snippet = @note.noteable
|
||||||
|
|
|
@ -15,7 +15,7 @@ module CacheMarkdownField
|
||||||
# Increment this number every time the renderer changes its output
|
# Increment this number every time the renderer changes its output
|
||||||
CACHE_REDCARPET_VERSION = 3
|
CACHE_REDCARPET_VERSION = 3
|
||||||
CACHE_COMMONMARK_VERSION_START = 10
|
CACHE_COMMONMARK_VERSION_START = 10
|
||||||
CACHE_COMMONMARK_VERSION = 11
|
CACHE_COMMONMARK_VERSION = 12
|
||||||
|
|
||||||
# changes to these attributes cause the cache to be invalidates
|
# changes to these attributes cause the cache to be invalidates
|
||||||
INVALIDATED_BY = %w[author project].freeze
|
INVALIDATED_BY = %w[author project].freeze
|
||||||
|
|
|
@ -310,7 +310,7 @@ class Note < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_ability_name
|
def to_ability_name
|
||||||
for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
|
for_snippet? ? noteable.class.name.underscore : noteable_type.underscore
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_be_discussion_note?
|
def can_be_discussion_note?
|
||||||
|
|
|
@ -72,7 +72,7 @@ class PrometheusService < MonitoringService
|
||||||
end
|
end
|
||||||
|
|
||||||
def prometheus_client
|
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
|
end
|
||||||
|
|
||||||
def prometheus_installed?
|
def prometheus_installed?
|
||||||
|
|
|
@ -2,4 +2,6 @@
|
||||||
|
|
||||||
class CommitPolicy < BasePolicy
|
class CommitPolicy < BasePolicy
|
||||||
delegate { @subject.project }
|
delegate { @subject.project }
|
||||||
|
|
||||||
|
rule { can?(:download_code) }.enable :read_commit
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,17 @@ class NotePolicy < BasePolicy
|
||||||
|
|
||||||
condition(:editable, scope: :subject) { @subject.editable? }
|
condition(:editable, scope: :subject) { @subject.editable? }
|
||||||
|
|
||||||
|
condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") }
|
||||||
|
|
||||||
rule { ~editable }.prevent :admin_note
|
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
|
rule { is_author }.policy do
|
||||||
enable :read_note
|
enable :read_note
|
||||||
enable :admin_note
|
enable :admin_note
|
||||||
|
|
|
@ -41,12 +41,13 @@ class UrlValidator < ActiveModel::EachValidator
|
||||||
def validate_each(record, attribute, value)
|
def validate_each(record, attribute, value)
|
||||||
@record = record
|
@record = record
|
||||||
|
|
||||||
if value.present?
|
unless value.present?
|
||||||
value.strip!
|
|
||||||
else
|
|
||||||
record.errors.add(attribute, 'must be a valid URL')
|
record.errors.add(attribute, 'must be a valid URL')
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
value = strip_value!(record, attribute, value)
|
||||||
|
|
||||||
Gitlab::UrlBlocker.validate!(value, blocker_args)
|
Gitlab::UrlBlocker.validate!(value, blocker_args)
|
||||||
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
||||||
record.errors.add(attribute, "is blocked: #{e.message}")
|
record.errors.add(attribute, "is blocked: #{e.message}")
|
||||||
|
@ -54,6 +55,13 @@ class UrlValidator < ActiveModel::EachValidator
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def strip_value!(record, attribute, value)
|
||||||
|
new_value = value.strip
|
||||||
|
return value if new_value == value
|
||||||
|
|
||||||
|
record.public_send("#{attribute}=", new_value) # rubocop:disable GitlabSecurity/PublicSend
|
||||||
|
end
|
||||||
|
|
||||||
def default_options
|
def default_options
|
||||||
# By default the validator doesn't block any url based on the ip address
|
# By default the validator doesn't block any url based on the ip address
|
||||||
{
|
{
|
||||||
|
|
12
app/views/devise/mailer/email_changed.html.haml
Normal file
12
app/views/devise/mailer/email_changed.html.haml
Normal file
|
@ -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.
|
10
app/views/devise/mailer/email_changed.text.erb
Normal file
10
app/views/devise/mailer/email_changed.text.erb
Normal file
|
@ -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.
|
|
@ -4,62 +4,50 @@
|
||||||
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
|
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
|
||||||
|
|
||||||
- link = commit_path(project, commit, merge_request: merge_request)
|
- link = commit_path(project, commit, merge_request: merge_request)
|
||||||
- cache_key = [project.full_path,
|
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
|
||||||
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
|
.avatar-cell.d-none.d-sm-block
|
||||||
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
|
= author_avatar(commit, size: 36, has_tooltip: false)
|
||||||
|
|
||||||
.avatar-cell.d-none.d-sm-block
|
.commit-detail.flex-list
|
||||||
= author_avatar(commit, size: 36, has_tooltip: false)
|
.commit-content.qa-commit-content
|
||||||
|
- if view_details && merge_request
|
||||||
.commit-detail.flex-list
|
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
|
||||||
.commit-content.qa-commit-content
|
- else
|
||||||
- if view_details && merge_request
|
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
|
||||||
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
|
%span.commit-row-message.d-block.d-sm-none
|
||||||
- else
|
·
|
||||||
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
|
= commit.short_id
|
||||||
%span.commit-row-message.d-block.d-sm-none
|
- if commit.status(ref)
|
||||||
·
|
.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)
|
|
||||||
|
|
||||||
.commiter
|
|
||||||
- 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))
|
|
||||||
|
|
||||||
.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)
|
= render_commit_status(commit, ref: ref)
|
||||||
|
- if commit.description?
|
||||||
|
%button.text-expander.js-toggle-button
|
||||||
|
= sprite_icon('ellipsis_h', size: 12)
|
||||||
|
|
||||||
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
|
.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 }
|
||||||
|
|
||||||
.commit-sha-group
|
- if commit.description?
|
||||||
.label.label-monospace
|
%pre.commit-row-description.js-toggle-content.append-bottom-8
|
||||||
= commit.short_id
|
= preserve(markdown_field(commit, :description))
|
||||||
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body")
|
|
||||||
= link_to_browse_code(project, 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)
|
||||||
|
|
||||||
|
.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)
|
||||||
|
|
|
@ -35,8 +35,8 @@
|
||||||
.col-sm-2
|
.col-sm-2
|
||||||
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
|
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
|
||||||
- if @project
|
- if @project
|
||||||
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
|
- if can_admin_project_milestones? and milestone.active?
|
||||||
- if @project.group
|
- 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'),
|
%button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|
|
@ -93,7 +93,9 @@ module Gitlab
|
||||||
# - Sentry DSN (:sentry_dsn)
|
# - Sentry DSN (:sentry_dsn)
|
||||||
# - Deploy keys (:key)
|
# - Deploy keys (:key)
|
||||||
# - File content from Web Editor (:content)
|
# - File content from Web Editor (:content)
|
||||||
config.filter_parameters += [/token$/, /password/, /secret/]
|
#
|
||||||
|
# NOTE: It is **IMPORTANT** to also update gitlab-workhorse's filter when adding parameters here!
|
||||||
|
config.filter_parameters += [/token$/, /password/, /secret/, /key$/]
|
||||||
config.filter_parameters += %i(
|
config.filter_parameters += %i(
|
||||||
certificate
|
certificate
|
||||||
encrypted_key
|
encrypted_key
|
||||||
|
|
|
@ -103,6 +103,9 @@ Devise.setup do |config|
|
||||||
# Send a notification email when the user's password is changed
|
# Send a notification email when the user's password is changed
|
||||||
config.send_password_change_notification = true
|
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
|
# ==> Configuration for :validatable
|
||||||
# Range for password length. Default is 6..128.
|
# Range for password length. Default is 6..128.
|
||||||
config.password_length = 8..128
|
config.password_length = 8..128
|
||||||
|
|
|
@ -48,6 +48,13 @@ Doorkeeper.configure do
|
||||||
#
|
#
|
||||||
force_ssl_in_redirect_uri false
|
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)
|
# 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
|
# Optional parameter confirmation: true (default false) if you want to enforce ownership of
|
||||||
# a registered application
|
# a registered application
|
||||||
|
|
|
@ -33,22 +33,22 @@ class Rack::Attack
|
||||||
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
|
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
|
||||||
Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
|
Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
|
||||||
req.api_request? &&
|
req.api_request? &&
|
||||||
req.authenticated_user_id
|
req.authenticated_user_id([:api])
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
|
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
|
||||||
Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
|
Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
|
||||||
req.web_request? &&
|
req.web_request? &&
|
||||||
req.authenticated_user_id
|
req.authenticated_user_id([:api, :rss, :ics])
|
||||||
end
|
end
|
||||||
|
|
||||||
class Request
|
class Request
|
||||||
def unauthenticated?
|
def unauthenticated?
|
||||||
!authenticated_user_id
|
!authenticated_user_id([:api, :rss, :ics])
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticated_user_id
|
def authenticated_user_id(request_formats)
|
||||||
Gitlab::Auth::RequestAuthenticator.new(self).user&.id
|
Gitlab::Auth::RequestAuthenticator.new(self).user(request_formats)&.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_request?
|
def api_request?
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -11,7 +11,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20181014121030) do
|
ActiveRecord::Schema.define(version: 20181108091549) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
|
@ -63,6 +63,8 @@ Below is the table of events users can be notified of:
|
||||||
|------------------------------|-------------------------------------------------------------------|------------------------------|
|
|------------------------------|-------------------------------------------------------------------|------------------------------|
|
||||||
| New SSH key added | User | Security email, always sent. |
|
| New SSH key added | User | Security email, always sent. |
|
||||||
| New email 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)|
|
| New user created | User | Sent on user creation, except for omniauth (LDAP)|
|
||||||
| User added to project | User | Sent when user is added to project |
|
| 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 |
|
| Project access level changed | User | Sent when user project access level is changed |
|
||||||
|
|
|
@ -17,6 +17,9 @@ module Banzai
|
||||||
# This is a small extension to the CommonMark spec. If they start allowing
|
# This is a small extension to the CommonMark spec. If they start allowing
|
||||||
# spaces in urls, we could then remove this filter.
|
# 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
|
class SpacedLinkFilter < HTML::Pipeline::Filter
|
||||||
include ActionView::Helpers::TagHelper
|
include ActionView::Helpers::TagHelper
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,16 @@ module Banzai
|
||||||
def self.filters
|
def self.filters
|
||||||
@filters ||= FilterArray[
|
@filters ||= FilterArray[
|
||||||
Filter::PlantumlFilter,
|
Filter::PlantumlFilter,
|
||||||
|
|
||||||
|
# Must always be before the SanitizationFilter to prevent XSS attacks
|
||||||
|
Filter::SpacedLinkFilter,
|
||||||
|
|
||||||
Filter::SanitizationFilter,
|
Filter::SanitizationFilter,
|
||||||
Filter::SyntaxHighlightFilter,
|
Filter::SyntaxHighlightFilter,
|
||||||
|
|
||||||
Filter::MathFilter,
|
Filter::MathFilter,
|
||||||
Filter::ColorFilter,
|
Filter::ColorFilter,
|
||||||
Filter::MermaidFilter,
|
Filter::MermaidFilter,
|
||||||
Filter::SpacedLinkFilter,
|
|
||||||
Filter::VideoLinkFilter,
|
Filter::VideoLinkFilter,
|
||||||
Filter::ImageLazyLoadFilter,
|
Filter::ImageLazyLoadFilter,
|
||||||
Filter::ImageLinkFilter,
|
Filter::ImageLinkFilter,
|
||||||
|
|
|
@ -11,12 +11,18 @@ module Gitlab
|
||||||
@request = request
|
@request = request
|
||||||
end
|
end
|
||||||
|
|
||||||
def user
|
def user(request_formats)
|
||||||
find_sessionless_user || find_user_from_warden
|
request_formats.each do |format|
|
||||||
|
user = find_sessionless_user(format)
|
||||||
|
|
||||||
|
return user if user
|
||||||
|
end
|
||||||
|
|
||||||
|
find_user_from_warden
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_sessionless_user
|
def find_sessionless_user(request_format)
|
||||||
find_user_from_access_token || find_user_from_feed_token
|
find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format)
|
||||||
rescue Gitlab::Auth::AuthenticationError
|
rescue Gitlab::Auth::AuthenticationError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,8 +25,8 @@ module Gitlab
|
||||||
current_request.env['warden']&.authenticate if verified_request?
|
current_request.env['warden']&.authenticate if verified_request?
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_user_from_feed_token
|
def find_user_from_feed_token(request_format)
|
||||||
return unless rss_request? || ics_request?
|
return unless valid_rss_format?(request_format)
|
||||||
|
|
||||||
# NOTE: feed_token was renamed from rss_token but both needs to be supported because
|
# 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
|
# users might have already added the feed to their RSS reader before the rename
|
||||||
|
@ -36,6 +36,17 @@ module Gitlab
|
||||||
User.find_by_feed_token(token) || raise(UnauthorizedError)
|
User.find_by_feed_token(token) || raise(UnauthorizedError)
|
||||||
end
|
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
|
def find_user_from_access_token
|
||||||
return unless access_token
|
return unless access_token
|
||||||
|
|
||||||
|
@ -107,6 +118,26 @@ module Gitlab
|
||||||
@current_request ||= ensure_action_dispatch_request(request)
|
@current_request ||= ensure_action_dispatch_request(request)
|
||||||
end
|
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?
|
def rss_request?
|
||||||
current_request.path.ends_with?('.atom') || current_request.format.atom?
|
current_request.path.ends_with?('.atom') || current_request.format.atom?
|
||||||
end
|
end
|
||||||
|
@ -114,6 +145,10 @@ module Gitlab
|
||||||
def ics_request?
|
def ics_request?
|
||||||
current_request.path.ends_with?('.ics') || current_request.format.ics?
|
current_request.path.ends_with?('.ics') || current_request.format.ics?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def api_request?
|
||||||
|
current_request.path.starts_with?("/api/")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
require 'resolv'
|
require 'resolv'
|
||||||
|
require 'ipaddress'
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
class UrlBlocker
|
class UrlBlocker
|
||||||
|
@ -8,11 +9,8 @@ module Gitlab
|
||||||
def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: [])
|
def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: [])
|
||||||
return true if url.nil?
|
return true if url.nil?
|
||||||
|
|
||||||
begin
|
# Param url can be a string, URI or Addressable::URI
|
||||||
uri = Addressable::URI.parse(url)
|
uri = parse_url(url)
|
||||||
rescue Addressable::URI::InvalidURIError
|
|
||||||
raise BlockedUrlError, "URI is invalid"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Allow imports from the GitLab instance itself but only from the configured ports
|
# Allow imports from the GitLab instance itself but only from the configured ports
|
||||||
return true if internal?(uri)
|
return true if internal?(uri)
|
||||||
|
@ -24,7 +22,9 @@ module Gitlab
|
||||||
validate_hostname!(uri.hostname)
|
validate_hostname!(uri.hostname)
|
||||||
|
|
||||||
begin
|
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
|
rescue SocketError
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
@ -47,6 +47,18 @@ module Gitlab
|
||||||
|
|
||||||
private
|
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)
|
def validate_port!(port, ports)
|
||||||
return if port.blank?
|
return if port.blank?
|
||||||
# Only ports under 1024 are restricted
|
# Only ports under 1024 are restricted
|
||||||
|
@ -71,13 +83,14 @@ module Gitlab
|
||||||
|
|
||||||
def validate_hostname!(value)
|
def validate_hostname!(value)
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
|
return if IPAddress.valid?(value)
|
||||||
return if value =~ /\A\p{Alnum}/
|
return if value =~ /\A\p{Alnum}/
|
||||||
|
|
||||||
raise BlockedUrlError, "Hostname needs to start with an alphanumeric character"
|
raise BlockedUrlError, "Hostname or IP address invalid"
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_localhost!(addrs_info)
|
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))
|
local_ips.concat(Socket.ip_address_list.map(&:ip_address))
|
||||||
|
|
||||||
return if (local_ips & addrs_info.map(&:ip_address)).empty?
|
return if (local_ips & addrs_info.map(&:ip_address)).empty?
|
||||||
|
@ -92,7 +105,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_local_network!(addrs_info)
|
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"
|
raise BlockedUrlError, "Requests to the local network are not allowed"
|
||||||
end
|
end
|
||||||
|
@ -109,12 +122,14 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def internal_web?(uri)
|
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)
|
(uri.port.blank? || uri.port == config.gitlab.port)
|
||||||
end
|
end
|
||||||
|
|
||||||
def internal_shell?(uri)
|
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)
|
(uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -107,59 +107,6 @@ describe ApplicationController do
|
||||||
end
|
end
|
||||||
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
|
describe 'session expiration' do
|
||||||
controller(described_class) do
|
controller(described_class) do
|
||||||
# The anonymous controller will report 401 and fail to run any actions.
|
# The anonymous controller will report 401 and fail to run any actions.
|
||||||
|
@ -248,74 +195,6 @@ describe ApplicationController do
|
||||||
end
|
end
|
||||||
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
|
describe '#route_not_found' do
|
||||||
it 'renders 404 if authenticated' do
|
it 'renders 404 if authenticated' do
|
||||||
allow(controller).to receive(:current_user).and_return(user)
|
allow(controller).to receive(:current_user).and_return(user)
|
||||||
|
@ -581,36 +460,6 @@ describe ApplicationController do
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(200)
|
expect(response).to have_gitlab_http_status(200)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
5
spec/controllers/dashboard/projects_controller_spec.rb
Normal file
5
spec/controllers/dashboard/projects_controller_spec.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Dashboard::ProjectsController do
|
||||||
|
it_behaves_like 'authenticates sessionless user', :index, :atom
|
||||||
|
end
|
|
@ -42,6 +42,16 @@ describe Dashboard::TodosController do
|
||||||
end
|
end
|
||||||
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
|
context 'when using pagination' do
|
||||||
let(:last_page) { user.todos.page.total_pages }
|
let(:last_page) { user.todos.page.total_pages }
|
||||||
let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
|
let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe DashboardController do
|
describe DashboardController do
|
||||||
let(:user) { create(:user) }
|
context 'signed in' do
|
||||||
let(:project) { create(:project) }
|
let(:user) { create(:user) }
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
sign_in(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 merge requests' do
|
||||||
|
it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
|
||||||
|
it_behaves_like 'issuables requiring filter', :merge_requests
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET issues' do
|
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
|
||||||
it_behaves_like 'issuables list meta-data', :issue, :issues
|
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,15 +52,58 @@ describe GraphqlController do
|
||||||
end
|
end
|
||||||
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
|
# 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 = <<~QUERY
|
||||||
query Echo($text: String) {
|
query Echo($text: String) {
|
||||||
echo(text: $text)
|
echo(text: $text)
|
||||||
}
|
}
|
||||||
QUERY
|
QUERY
|
||||||
|
|
||||||
post :execute, query: query, operationName: 'Echo', variables: variables
|
post :execute, query: query, operationName: 'Echo', variables: variables, private_token: private_token
|
||||||
end
|
end
|
||||||
|
|
||||||
def query_response
|
def query_response
|
||||||
|
|
|
@ -581,4 +581,24 @@ describe GroupsController do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -23,6 +23,23 @@ describe Oauth::ApplicationsController do
|
||||||
expect(response).to have_gitlab_http_status(302)
|
expect(response).to have_gitlab_http_status(302)
|
||||||
expect(response).to redirect_to(profile_path)
|
expect(response).to redirect_to(profile_path)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,87 +5,115 @@ describe Projects::CommitsController do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET commits_root" do
|
context 'signed in' do
|
||||||
context "no ref is provided" do
|
before do
|
||||||
it 'should redirect to the default branch of the project' do
|
sign_in(user)
|
||||||
get(:commits_root,
|
end
|
||||||
namespace_id: project.namespace,
|
|
||||||
project_id: project)
|
|
||||||
|
|
||||||
expect(response).to redirect_to project_commits_path(project)
|
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)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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 "valid branch, valid file" do
|
||||||
|
let(:id) { 'master/README.md' }
|
||||||
|
|
||||||
|
it { is_expected.to respond_with(:success) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "valid branch, invalid file" do
|
||||||
|
let(:id) { 'master/invalid-path.rb' }
|
||||||
|
|
||||||
|
it { is_expected.to respond_with(:not_found) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "invalid branch, valid file" do
|
||||||
|
let(:id) { 'invalid-branch/README.md' }
|
||||||
|
|
||||||
|
it { is_expected.to respond_with(:not_found) }
|
||||||
|
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")
|
||||||
|
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
|
||||||
|
|
||||||
|
context "when the ref exists with the suffix" do
|
||||||
|
before do
|
||||||
|
commit = project.repository.commit('master')
|
||||||
|
|
||||||
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET show" do
|
context 'token authentication' do
|
||||||
render_views
|
context 'public project' do
|
||||||
|
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
|
||||||
|
before do
|
||||||
|
public_project = create(:project, :repository, :public)
|
||||||
|
|
||||||
context 'with file path' do
|
default_params.merge!(namespace_id: public_project.namespace, project_id: public_project, id: "master.atom")
|
||||||
before do
|
end
|
||||||
get(:show,
|
|
||||||
namespace_id: project.namespace,
|
|
||||||
project_id: project,
|
|
||||||
id: id)
|
|
||||||
end
|
|
||||||
|
|
||||||
context "valid branch, valid file" do
|
|
||||||
let(:id) { 'master/README.md' }
|
|
||||||
|
|
||||||
it { is_expected.to respond_with(:success) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context "valid branch, invalid file" do
|
|
||||||
let(:id) { 'master/invalid-path.rb' }
|
|
||||||
|
|
||||||
it { is_expected.to respond_with(:not_found) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context "invalid branch, valid file" do
|
|
||||||
let(:id) { 'invalid-branch/README.md' }
|
|
||||||
|
|
||||||
it { is_expected.to respond_with(:not_found) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the ref name ends in .atom" do
|
context 'private project' do
|
||||||
context "when the ref does not exist with the suffix" do
|
it_behaves_like 'authenticates sessionless user', :show, :atom, public: false do
|
||||||
before do
|
before do
|
||||||
get(:show,
|
private_project = create(:project, :repository, :private)
|
||||||
namespace_id: project.namespace,
|
private_project.add_maintainer(user)
|
||||||
project_id: project,
|
|
||||||
id: "master.atom")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "renders as atom" do
|
default_params.merge!(namespace_id: private_project.namespace, project_id: private_project, id: "master.atom")
|
||||||
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
|
|
||||||
|
|
||||||
context "when the ref exists with the suffix" do
|
|
||||||
before do
|
|
||||||
commit = project.repository.commit('master')
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
|
@ -1049,4 +1049,40 @@ describe Projects::IssuesController do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -143,11 +143,27 @@ describe Projects::MilestonesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#promote' do
|
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
|
context 'promotion succeeds' do
|
||||||
before do
|
before do
|
||||||
group = create(:group)
|
|
||||||
group.add_developer(user)
|
group.add_developer(user)
|
||||||
milestone.project.update(namespace: group)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows group milestone' do
|
it 'shows group milestone' do
|
||||||
|
@ -166,12 +182,17 @@ describe Projects::MilestonesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'promotion fails' do
|
context 'when user cannot admin group milestones' do
|
||||||
it 'shows project milestone' 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
|
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(response).to have_gitlab_http_status(404)
|
||||||
expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -244,14 +244,14 @@ describe Projects::NotesController do
|
||||||
|
|
||||||
def post_create(extra_params = {})
|
def post_create(extra_params = {})
|
||||||
post :create, {
|
post :create, {
|
||||||
note: { note: 'some other note' },
|
note: { note: 'some other note', noteable_id: merge_request.id },
|
||||||
namespace_id: project.namespace,
|
namespace_id: project.namespace,
|
||||||
project_id: project,
|
project_id: project,
|
||||||
target_type: 'merge_request',
|
target_type: 'merge_request',
|
||||||
target_id: merge_request.id,
|
target_id: merge_request.id,
|
||||||
note_project_id: forked_project.id,
|
note_project_id: forked_project.id,
|
||||||
in_reply_to_discussion_id: existing_comment.discussion_id
|
in_reply_to_discussion_id: existing_comment.discussion_id
|
||||||
}.merge(extra_params)
|
}.merge(extra_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the note_project_id is not correct' do
|
context 'when the note_project_id is not correct' do
|
||||||
|
@ -285,6 +285,31 @@ describe Projects::NotesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when target_id and noteable_id do not match' do
|
||||||
|
let(:locked_issue) { create(:issue, project: project) }
|
||||||
|
let(:issue) {create(:issue, project: project)}
|
||||||
|
|
||||||
|
before do
|
||||||
|
locked_issue.update_attribute(:discussion_locked, true)
|
||||||
|
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
|
context 'when the merge request discussion is locked' do
|
||||||
before do
|
before do
|
||||||
project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
|
project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
@ -337,35 +362,60 @@ describe Projects::NotesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT update' do
|
describe 'PUT update' do
|
||||||
let(:request_params) do
|
context "should update the note with a valid issue" do
|
||||||
{
|
let(:request_params) do
|
||||||
namespace_id: project.namespace,
|
{
|
||||||
project_id: project,
|
namespace_id: project.namespace,
|
||||||
id: note,
|
project_id: project,
|
||||||
format: :json,
|
id: note,
|
||||||
note: {
|
format: :json,
|
||||||
note: "New comment"
|
note: {
|
||||||
|
note: "New comment"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(note.author)
|
sign_in(note.author)
|
||||||
project.add_developer(note.author)
|
project.add_developer(note.author)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "updates the note" do
|
it "updates the note" do
|
||||||
expect { put :update, request_params }.to change { note.reload.note }
|
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) }
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE destroy' do
|
describe 'DELETE destroy' do
|
||||||
let(:request_params) do
|
let(:request_params) do
|
||||||
{
|
{
|
||||||
namespace_id: project.namespace,
|
namespace_id: project.namespace,
|
||||||
project_id: project,
|
project_id: project,
|
||||||
id: note,
|
id: note,
|
||||||
format: :js
|
format: :js
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -35,4 +35,26 @@ describe Projects::TagsController do
|
||||||
it { is_expected.to respond_with(:not_found) }
|
it { is_expected.to respond_with(:not_found) }
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -882,6 +882,28 @@ describe ProjectsController do
|
||||||
end
|
end
|
||||||
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)
|
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."
|
"Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
|
||||||
end
|
end
|
||||||
|
|
|
@ -395,6 +395,14 @@ describe UsersController do
|
||||||
end
|
end
|
||||||
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)
|
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."
|
"User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path."
|
||||||
end
|
end
|
||||||
|
|
|
@ -257,7 +257,7 @@ FactoryBot.define do
|
||||||
|
|
||||||
trait :with_runner_session do
|
trait :with_runner_session do
|
||||||
after(:build) do |build|
|
after(:build) do |build|
|
||||||
build.build_runner_session(url: 'ws://localhost')
|
build.build_runner_session(url: 'https://localhost')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,6 +40,18 @@ describe "User comments on issue", :js do
|
||||||
|
|
||||||
expect(page.find('pre code').text).to eq code_block_content
|
expect(page.find('pre code').text).to eq code_block_content
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context "when editing comments" do
|
context "when editing comments" do
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe 'Mermaid rendering', :js do
|
||||||
visit project_issue_path(project, issue)
|
visit project_issue_path(project, issue)
|
||||||
|
|
||||||
%w[A B C D].each do |label|
|
%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
|
end
|
||||||
end
|
end
|
||||||
|
|
32
spec/features/milestones/user_promotes_milestone_spec.rb
Normal file
32
spec/features/milestones/user_promotes_milestone_spec.rb
Normal file
|
@ -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
|
|
@ -4,10 +4,9 @@ describe 'User browses commits' do
|
||||||
include RepoHelpers
|
include RepoHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:project) { create(:project, :repository, namespace: user.namespace) }
|
let(:project) { create(:project, :public, :repository, namespace: user.namespace) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_maintainer(user)
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -127,6 +126,26 @@ describe 'User browses commits' do
|
||||||
.and have_selector('entry summary', text: commit.description[0..10].delete("\r\n"))
|
.and have_selector('entry summary', text: commit.description[0..10].delete("\r\n"))
|
||||||
end
|
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
|
context 'master branch' do
|
||||||
before do
|
before do
|
||||||
visit_commits_page
|
visit_commits_page
|
||||||
|
|
|
@ -104,5 +104,17 @@ describe Banzai::Pipeline::GfmPipeline do
|
||||||
|
|
||||||
expect(output).to include("src=\"test%20image.png\"")
|
expect(output).to include("src=\"test%20image.png\"")
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -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_sessionless_user).and_return(sessionless_user)
|
||||||
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_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
|
end
|
||||||
|
|
||||||
it 'returns session user if no sessionless user found' do
|
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)
|
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
|
end
|
||||||
|
|
||||||
it 'returns nil if no user found' do
|
it 'returns nil if no user found' do
|
||||||
expect(subject.user).to be_blank
|
expect(subject.user([:api])).to be_blank
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'bubbles up exceptions' do
|
it 'bubbles up exceptions' do
|
||||||
|
@ -42,26 +42,26 @@ describe Gitlab::Auth::RequestAuthenticator do
|
||||||
let!(:feed_token_user) { build(:user) }
|
let!(:feed_token_user) { build(:user) }
|
||||||
|
|
||||||
it 'returns access_token user first' do
|
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)
|
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
|
end
|
||||||
|
|
||||||
it 'returns feed_token user if no access_token user found' do
|
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)
|
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
|
end
|
||||||
|
|
||||||
it 'returns nil if no user found' do
|
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
|
end
|
||||||
|
|
||||||
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ describe Gitlab::Auth::UserAuthFinders do
|
||||||
'rack.input' => ''
|
'rack.input' => ''
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
let(:request) { Rack::Request.new(env)}
|
let(:request) { Rack::Request.new(env) }
|
||||||
|
|
||||||
def set_param(key, value)
|
def set_param(key, value)
|
||||||
request.update_param(key, value)
|
request.update_param(key, value)
|
||||||
|
@ -49,6 +49,7 @@ describe Gitlab::Auth::UserAuthFinders do
|
||||||
describe '#find_user_from_feed_token' do
|
describe '#find_user_from_feed_token' do
|
||||||
context 'when the request format is atom' do
|
context 'when the request format is atom' do
|
||||||
before do
|
before do
|
||||||
|
env['SCRIPT_NAME'] = 'url.atom'
|
||||||
env['HTTP_ACCEPT'] = 'application/atom+xml'
|
env['HTTP_ACCEPT'] = 'application/atom+xml'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -56,17 +57,17 @@ describe Gitlab::Auth::UserAuthFinders do
|
||||||
it 'returns user if valid feed_token' do
|
it 'returns user if valid feed_token' do
|
||||||
set_param(:feed_token, user.feed_token)
|
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
|
end
|
||||||
|
|
||||||
it 'returns nil if feed_token is blank' do
|
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
|
end
|
||||||
|
|
||||||
it 'returns exception if invalid feed_token' do
|
it 'returns exception if invalid feed_token' do
|
||||||
set_param(:feed_token, 'invalid_token')
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -74,34 +75,38 @@ describe Gitlab::Auth::UserAuthFinders do
|
||||||
it 'returns user if valid rssd_token' do
|
it 'returns user if valid rssd_token' do
|
||||||
set_param(:rss_token, user.feed_token)
|
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
|
end
|
||||||
|
|
||||||
it 'returns nil if rss_token is blank' do
|
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
|
end
|
||||||
|
|
||||||
it 'returns exception if invalid rss_token' do
|
it 'returns exception if invalid rss_token' do
|
||||||
set_param(:rss_token, 'invalid_token')
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the request format is not atom' do
|
context 'when the request format is not atom' do
|
||||||
it 'returns nil' do
|
it 'returns nil' do
|
||||||
|
env['SCRIPT_NAME'] = 'json'
|
||||||
|
|
||||||
set_param(:feed_token, user.feed_token)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the request format is empty' do
|
context 'when the request format is empty' do
|
||||||
it 'the method call does not modify the original value' do
|
it 'the method call does not modify the original value' do
|
||||||
|
env['SCRIPT_NAME'] = 'url.atom'
|
||||||
|
|
||||||
env.delete('action_dispatch.request.formats')
|
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
|
expect(env['action_dispatch.request.formats']).to be_nil
|
||||||
end
|
end
|
||||||
|
@ -111,8 +116,12 @@ describe Gitlab::Auth::UserAuthFinders do
|
||||||
describe '#find_user_from_access_token' do
|
describe '#find_user_from_access_token' do
|
||||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
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
|
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
|
end
|
||||||
|
|
||||||
context 'when validate_access_token! returns valid' do
|
context 'when validate_access_token! returns valid' do
|
||||||
|
@ -131,9 +140,59 @@ describe Gitlab::Auth::UserAuthFinders do
|
||||||
end
|
end
|
||||||
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
|
describe '#find_personal_access_token' do
|
||||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
env['SCRIPT_NAME'] = 'url.atom'
|
||||||
|
end
|
||||||
|
|
||||||
context 'passed as header' do
|
context 'passed as header' do
|
||||||
it 'returns token if valid personal_access_token' do
|
it 'returns token if valid personal_access_token' do
|
||||||
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
|
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
|
||||||
|
|
|
@ -10,8 +10,8 @@ describe Gitlab::UrlBlocker do
|
||||||
expect(described_class.blocked_url?(import_url)).to be false
|
expect(described_class.blocked_url?(import_url)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows imports from configured SSH host and port' do
|
it 'allows mirroring from configured SSH host and port' do
|
||||||
import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
|
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
|
expect(described_class.blocked_url?(import_url)).to be false
|
||||||
end
|
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
|
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
|
||||||
end
|
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
|
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://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://[::]/foo/foo.git')).to be true
|
||||||
expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for loopback IP' do
|
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.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
|
end
|
||||||
|
|
||||||
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
|
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
|
expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
|
||||||
end
|
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
|
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
|
expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true
|
||||||
end
|
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
|
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
|
expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true
|
||||||
end
|
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
|
expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true
|
||||||
end
|
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
|
it 'returns true for a non-alphanumeric hostname' do
|
||||||
stub_resolv
|
stub_resolv
|
||||||
|
|
||||||
|
@ -78,7 +121,22 @@ describe Gitlab::UrlBlocker do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when allow_local_network is' do
|
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' }
|
let(:fake_domain) { 'www.fakedomain.fake' }
|
||||||
|
|
||||||
context 'true (default)' do
|
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')
|
expect(described_class).not_to be_blocked_url('http://169.254.168.100')
|
||||||
end
|
end
|
||||||
|
|
||||||
# This is blocked due to the hostname check: https://gitlab.com/gitlab-org/gitlab-ce/issues/50227
|
it 'allows IPv6 link-local endpoints' do
|
||||||
it 'blocks 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).to be_blocked_url('http://[::ffff:169.254.169.254]')
|
expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]')
|
||||||
expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]')
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -135,14 +197,20 @@ describe Gitlab::UrlBlocker do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'blocks IPv6 link-local endpoints' do
|
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: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://[::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
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_domain_resolv(domain, ip)
|
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
|
end
|
||||||
|
|
||||||
def unstub_domain_resolv
|
def unstub_domain_resolv
|
||||||
|
@ -183,6 +251,36 @@ describe Gitlab::UrlBlocker do
|
||||||
end
|
end
|
||||||
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
|
# Resolv does not support resolving UTF-8 domain names
|
||||||
# See https://bugs.ruby-lang.org/issues/4270
|
# See https://bugs.ruby-lang.org/issues/4270
|
||||||
def stub_resolv
|
def stub_resolv
|
||||||
|
|
|
@ -522,7 +522,7 @@ describe Notify do
|
||||||
let(:project_snippet) { create(:project_snippet, project: project) }
|
let(:project_snippet) { create(:project_snippet, project: project) }
|
||||||
let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
|
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
|
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
|
||||||
let(:model) { project_snippet }
|
let(:model) { project_snippet }
|
||||||
|
|
28
spec/migrations/cleanup_environments_external_url_spec.rb
Normal file
28
spec/migrations/cleanup_environments_external_url_spec.rb
Normal file
|
@ -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
|
48
spec/migrations/migrate_forbidden_redirect_uris_spec.rb
Normal file
48
spec/migrations/migrate_forbidden_redirect_uris_spec.rb
Normal file
|
@ -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
|
|
@ -517,7 +517,7 @@ describe Note do
|
||||||
|
|
||||||
describe '#to_ability_name' do
|
describe '#to_ability_name' do
|
||||||
it 'returns snippet for a project snippet note' 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
|
end
|
||||||
|
|
||||||
it 'returns personal_snippet for a personal snippet note' do
|
it 'returns personal_snippet for a personal snippet note' do
|
||||||
|
|
|
@ -11,6 +11,23 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
|
||||||
it { is_expected.to belong_to :project }
|
it { is_expected.to belong_to :project }
|
||||||
end
|
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
|
describe 'Validations' do
|
||||||
context 'when manual_configuration is enabled' do
|
context 'when manual_configuration is enabled' do
|
||||||
before do
|
before do
|
||||||
|
|
|
@ -227,55 +227,72 @@ describe Project do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not allow an invalid URI as import_url' do
|
describe 'import_url' do
|
||||||
project2 = build(:project, import_url: 'invalid://')
|
it 'does not allow an invalid URI as import_url' do
|
||||||
|
project2 = build(:project, import_url: 'invalid://')
|
||||||
|
|
||||||
expect(project2).not_to be_valid
|
expect(project2).not_to be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does allow a valid URI as import_url' do
|
it 'does allow a valid URI as import_url' do
|
||||||
project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
|
project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
|
||||||
|
|
||||||
expect(project2).to be_valid
|
expect(project2).to be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows an empty URI' do
|
it 'allows an empty URI' do
|
||||||
project2 = build(:project, import_url: '')
|
project2 = build(:project, import_url: '')
|
||||||
|
|
||||||
expect(project2).to be_valid
|
expect(project2).to be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not produce import data on an empty URI' do
|
it 'does not produce import data on an empty URI' do
|
||||||
project2 = build(:project, import_url: '')
|
project2 = build(:project, import_url: '')
|
||||||
|
|
||||||
expect(project2.import_data).to be_nil
|
expect(project2.import_data).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not produce import data on an invalid URI' do
|
it 'does not produce import data on an invalid URI' do
|
||||||
project2 = build(:project, import_url: 'test://')
|
project2 = build(:project, import_url: 'test://')
|
||||||
|
|
||||||
expect(project2.import_data).to be_nil
|
expect(project2.import_data).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not allow import_url pointing to localhost" do
|
it "does not allow import_url pointing to localhost" do
|
||||||
project2 = build(:project, import_url: 'http://localhost:9000/t.git')
|
project2 = build(:project, import_url: 'http://localhost:9000/t.git')
|
||||||
|
|
||||||
expect(project2).to be_invalid
|
expect(project2).to be_invalid
|
||||||
expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed')
|
expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not allow import_url with invalid ports" do
|
it "does not allow import_url with invalid ports" do
|
||||||
project2 = build(:project, import_url: 'http://github.com:25/t.git')
|
project2 = build(:project, import_url: 'http://github.com:25/t.git')
|
||||||
|
|
||||||
expect(project2).to be_invalid
|
expect(project2).to be_invalid
|
||||||
expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
|
expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not allow import_url with invalid user" do
|
it "does not allow import_url with invalid user" do
|
||||||
project2 = build(:project, import_url: 'http://$user:password@github.com/t.git')
|
project2 = build(:project, import_url: 'http://$user:password@github.com/t.git')
|
||||||
|
|
||||||
expect(project2).to be_invalid
|
expect(project2).to be_invalid
|
||||||
expect(project2.errors[:import_url].first).to include('Username needs to start with an alphanumeric character')
|
expect(project2.errors[:import_url].first).to include('Username needs to start with an alphanumeric character')
|
||||||
|
end
|
||||||
|
|
||||||
|
include_context 'invalid urls'
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
describe 'project pending deletion' do
|
describe 'project pending deletion' do
|
||||||
|
|
|
@ -10,11 +10,50 @@ describe NotePolicy, mdoels: true do
|
||||||
return @policies if @policies
|
return @policies if @policies
|
||||||
|
|
||||||
noteable ||= issue
|
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)
|
@policies = described_class.new(user, note)
|
||||||
end
|
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 project is public' do
|
||||||
context 'when the note author is not a project member' do
|
context 'when the note author is not a project member' do
|
||||||
it 'can edit a note' do
|
it 'can edit a note' do
|
||||||
|
@ -24,14 +63,48 @@ describe NotePolicy, mdoels: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the noteable is a snippet' do
|
context 'when the noteable is a project snippet' do
|
||||||
it 'can edit note' do
|
it 'can edit note' do
|
||||||
policies = policies(create(:project_snippet, project: project))
|
policies = policies(create(:project_snippet, :public, project: project))
|
||||||
|
|
||||||
expect(policies).to be_allowed(:admin_note)
|
expect(policies).to be_allowed(:admin_note)
|
||||||
expect(policies).to be_allowed(:resolve_note)
|
expect(policies).to be_allowed(:resolve_note)
|
||||||
expect(policies).to be_allowed(:read_note)
|
expect(policies).to be_allowed(:read_note)
|
||||||
end
|
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(: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
|
end
|
||||||
|
|
||||||
context 'when a discussion is locked' do
|
context 'when a discussion is locked' do
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe API::Applications, :api do
|
||||||
|
|
||||||
it 'does not allow creating an application with the wrong redirect_uri format' do
|
it 'does not allow creating an application with the wrong redirect_uri format' do
|
||||||
expect 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 }
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
expect(response).to have_http_status 400
|
expect(response).to have_http_status 400
|
||||||
|
@ -32,6 +32,16 @@ describe API::Applications, :api do
|
||||||
expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.')
|
expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.')
|
||||||
end
|
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
|
it 'does not allow creating an application without a name' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: ''
|
post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: ''
|
||||||
|
|
|
@ -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
|
|
@ -49,11 +49,11 @@ module PrometheusHelpers
|
||||||
"https://prometheus.example.com/api/v1/series?#{query}"
|
"https://prometheus.example.com/api/v1/series?#{query}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_prometheus_request(url, body: {}, status: 200)
|
def stub_prometheus_request(url, body: {}, status: 200, headers: {})
|
||||||
WebMock.stub_request(:get, url)
|
WebMock.stub_request(:get, url)
|
||||||
.to_return({
|
.to_return({
|
||||||
status: status,
|
status: status,
|
||||||
headers: { 'Content-Type' => 'application/json' },
|
headers: { 'Content-Type' => 'application/json' }.merge(headers),
|
||||||
body: body.to_json
|
body: body.to_json
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
17
spec/support/shared_contexts/url_shared_context.rb
Normal file
17
spec/support/shared_contexts/url_shared_context.rb
Normal file
|
@ -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
|
|
@ -6,6 +6,30 @@ describe UrlValidator do
|
||||||
|
|
||||||
include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
|
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
|
context 'by default' do
|
||||||
let(:validator) { described_class.new(attributes: [:link_url]) }
|
let(:validator) { described_class.new(attributes: [:link_url]) }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue