Commit ff0bfe78 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream



See merge request !439
parents 8324dc3a be4754c3
...@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity: ...@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity:
# Checks for ambiguous operators in the first argument of a method invocation # Checks for ambiguous operators in the first argument of a method invocation
# without parentheses. # without parentheses.
Lint/AmbiguousOperator: Lint/AmbiguousOperator:
Enabled: false Enabled: true
# Checks for ambiguous regexp literals in the first argument of a method # Checks for ambiguous regexp literals in the first argument of a method
# invocation without parentheses. # invocation without parentheses.
......
...@@ -26,6 +26,7 @@ v 8.9.0 (unreleased) ...@@ -26,6 +26,7 @@ v 8.9.0 (unreleased)
- Fix issues filter when ordering by milestone - Fix issues filter when ordering by milestone
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore - Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature - Remove 'main language' feature
- Pipelines can be canceled only when there are running builds - Pipelines can be canceled only when there are running builds
...@@ -41,13 +42,12 @@ v 8.9.0 (unreleased) ...@@ -41,13 +42,12 @@ v 8.9.0 (unreleased)
- Put project Files and Commits tabs under Code tab - Put project Files and Commits tabs under Code tab
- Replace Colorize with Rainbow for coloring console output in Rake tasks. - Replace Colorize with Rainbow for coloring console output in Rake tasks.
v 8.8.4
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
v 8.8.4 (unreleased) v 8.8.4 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds - Ensure branch cleanup regardless of whether the GitHub import process succeeds
- Fix issue with arrow keys not working in search autocomplete dropdown - Fix issue with arrow keys not working in search autocomplete dropdown
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
- Upgrade to jQuery 2
v 8.8.3 v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
......
...@@ -46,12 +46,13 @@ gem 'akismet', '~> 2.0' ...@@ -46,12 +46,13 @@ gem 'akismet', '~> 2.0'
gem 'devise-two-factor', '~> 3.0.0' gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7' gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0' gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
# GitLab Pages # GitLab Pages
gem 'validates_hostname', '~> 1.0.0' gem 'validates_hostname', '~> 1.0.0'
# Browser detection # Browser detection
gem "browser", '~> 1.0.0' gem "browser", '~> 2.0.3'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
......
...@@ -92,7 +92,7 @@ GEM ...@@ -92,7 +92,7 @@ GEM
sass (~> 3.0) sass (~> 3.0)
slim (>= 1.3.6, < 4.0) slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4) terminal-table (~> 1.4)
browser (1.0.1) browser (2.0.3)
builder (3.2.2) builder (3.2.2)
bullet (5.0.0) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
...@@ -771,6 +771,7 @@ GEM ...@@ -771,6 +771,7 @@ GEM
simple_oauth (~> 0.1.4) simple_oauth (~> 0.1.4)
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
u2f (0.2.1)
uglifier (2.7.2) uglifier (2.7.2)
execjs (>= 0.3.0) execjs (>= 0.3.0)
json (>= 1.8.0) json (>= 1.8.0)
...@@ -841,7 +842,7 @@ DEPENDENCIES ...@@ -841,7 +842,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.2.0) brakeman (~> 3.2.0)
browser (~> 1.0.0) browser (~> 2.0.3)
bullet bullet
bundler-audit bundler-audit
byebug byebug
...@@ -996,6 +997,7 @@ DEPENDENCIES ...@@ -996,6 +997,7 @@ DEPENDENCIES
thin (~> 1.6.1) thin (~> 1.6.1)
tinder (~> 1.10.0) tinder (~> 1.10.0)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0) underscore-rails (~> 1.8.0)
unf (~> 0.1.4) unf (~> 0.1.4)
...@@ -1009,4 +1011,4 @@ DEPENDENCIES ...@@ -1009,4 +1011,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.12.4 1.12.5
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file. # the compiled file.
# #
#= require jquery #= require jquery2
#= require jquery-ui/autocomplete #= require jquery-ui/autocomplete
#= require jquery-ui/datepicker #= require jquery-ui/datepicker
#= require jquery-ui/draggable #= require jquery-ui/draggable
...@@ -57,9 +57,11 @@ ...@@ -57,9 +57,11 @@
#= require_directory ./commit #= require_directory ./commit
#= require_directory ./extensions #= require_directory ./extensions
#= require_directory ./lib #= require_directory ./lib
#= require_directory ./u2f
#= require_directory . #= require_directory .
#= require fuzzaldrin-plus #= require fuzzaldrin-plus
#= require cropper #= require cropper
#= require u2f
window.slugify = (text) -> window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......
...@@ -23,7 +23,7 @@ class Dispatcher ...@@ -23,7 +23,7 @@ class Dispatcher
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler() gl.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'dashboard:todos:index' when 'dashboard:todos:index'
...@@ -54,7 +54,7 @@ class Dispatcher ...@@ -54,7 +54,7 @@ class Dispatcher
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler() gl.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
new ZenMode() new ZenMode()
......
...@@ -21,7 +21,7 @@ class @DueDateSelect ...@@ -21,7 +21,7 @@ class @DueDateSelect
$dropdown.glDropdown( $dropdown.glDropdown(
hidden: -> hidden: ->
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
) )
addDueDate = (isDropdown) -> addDueDate = (isDropdown) ->
...@@ -42,12 +42,13 @@ class @DueDateSelect ...@@ -42,12 +42,13 @@ class @DueDateSelect
type: 'PUT' type: 'PUT'
url: issueUpdateURL url: issueUpdateURL
data: data data: data
dataType: 'json'
beforeSend: -> beforeSend: ->
$loading.fadeIn() $loading.fadeIn()
if isDropdown if isDropdown
$dropdown.trigger('loading.gl.dropdown') $dropdown.trigger('loading.gl.dropdown')
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
$valueContent.html(mediumDate) $valueContent.html(mediumDate)
$sidebarValue.html(mediumDate) $sidebarValue.html(mediumDate)
......
window.emojiAliases = -> gl.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
...@@ -83,7 +83,7 @@ class @MilestoneSelect ...@@ -83,7 +83,7 @@ class @MilestoneSelect
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.css('display', '')
clicked: (selected) -> clicked: (selected) ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
...@@ -118,7 +118,7 @@ class @MilestoneSelect ...@@ -118,7 +118,7 @@ class @MilestoneSelect
$dropdown.trigger('loaded.gl.dropdown') $dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut() $loading.fadeOut()
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
if data.milestone? if data.milestone?
data.milestone.namespace = _this.currentProject.namespace data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path data.milestone.path = _this.currentProject.path
......
...@@ -162,13 +162,14 @@ class @Notes ...@@ -162,13 +162,14 @@ class @Notes
renderNote: (note) -> renderNote: (note) ->
unless note.valid unless note.valid
if note.award if note.award
flash = new Flash('You have already used this award emoji!', 'alert') flash = new Flash('You have already awarded this emoji!', 'alert')
flash.pinTo('.header-content') flash.pinTo('.header-content')
return return
if note.award if note.award
awardsHandler.addAwardToEmojiBar(note.name) votesBlock = $('.js-awards-block').eq 0
awardsHandler.scrollToAwards() gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
gl.awardsHandler.scrollToAwards()
# render note if it not present in loaded list # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
......
# Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> authenticated -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FAuthenticate
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@challenges = u2fParams.challenges
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
authenticate: () =>
u2f.sign(@appId, @challenges, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderAuthenticated(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
"inProgress": '#js-authenticate-u2f-in-progress',
"error": '#js-authenticate-u2f-error',
"authenticated": '#js-authenticate-u2f-authenticated'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-login-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@authenticate()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderAuthenticated: (deviceResponse) =>
@renderTemplate('authenticated')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
class @U2FError
constructor: (@errorCode) ->
@httpsDisabled = (window.location.protocol isnt 'https:')
console.error("U2F Error Code: #{@errorCode}")
message: () =>
switch
when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
"U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
"This device has already been registered with us."
else
"There was a problem communicating with your device."
# Register U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> registered -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FRegister
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@registerRequests = u2fParams.register_requests
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
register: () =>
u2f.register(@appId, @registerRequests, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderRegistered(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
"inProgress": '#js-register-u2f-in-progress',
"error": '#js-register-u2f-error',
"registered": '#js-register-u2f-registered'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-setup-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@register()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderRegistered: (deviceResponse) =>
@renderTemplate('registered')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
# Helper class for U2F (universal 2nd factor) device registration and authentication.
class @U2FUtil
@isU2FSupported: ->
if @testMode
true
else
gon.u2f.browser_supports_u2f
@enableTestMode: ->
@testMode = true
<% if Rails.env.test? %>
U2FUtil.enableTestMode();
<% end %>
...@@ -149,7 +149,7 @@ class @UsersSelect ...@@ -149,7 +149,7 @@ class @UsersSelect
hidden: (e) -> hidden: (e) ->
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.css('display', '')
clicked: (user) -> clicked: (user) ->
page = $('body').data 'page' page = $('body').data 'page'
......
...@@ -36,7 +36,7 @@ class @WeightSelect ...@@ -36,7 +36,7 @@ class @WeightSelect
hidden: (e) -> hidden: (e) ->
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.css('display', '')
id: (obj, el) -> id: (obj, el) ->
$(el).data "id" $(el).data "id"
clicked: (selected) -> clicked: (selected) ->
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
padding: 0; padding: 0;
.timeline-entry { .timeline-entry {
padding: $gl-padding $gl-btn-padding; padding: $gl-padding $gl-btn-padding 11px;
border-color: $table-border-color; border-color: $table-border-color;
color: $gl-gray; color: $gl-gray;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
......
...@@ -95,6 +95,7 @@ ...@@ -95,6 +95,7 @@
.award-control { .award-control {
margin-right: 5px; margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
line-height: 20px; line-height: 20px;
...@@ -108,7 +109,8 @@ ...@@ -108,7 +109,8 @@
} }
&.is-loading { &.is-loading {
.award-control-icon-normal { .award-control-icon-normal,
.emoji-icon {
display: none; display: none;
} }
......
...@@ -69,6 +69,10 @@ ul.notes { ...@@ -69,6 +69,10 @@ ul.notes {
.note-edit-form { .note-edit-form {
display: block; display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
} }
} }
...@@ -116,10 +120,38 @@ ul.notes { ...@@ -116,10 +120,38 @@ ul.notes {
} }
} }
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
.award-control {
font-size: 13px;
padding: 2px 5px;
}
}
.note-header { .note-header {
padding-bottom: 3px; padding-bottom: 3px;
} }
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
}
} }
} }
......
...@@ -186,8 +186,8 @@ class ApplicationController < ActionController::Base ...@@ -186,8 +186,8 @@ class ApplicationController < ActionController::Base
end end
def check_2fa_requirement def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path redirect_to profile_two_factor_auth_path
end end
end end
...@@ -352,6 +352,10 @@ class ApplicationController < ActionController::Base ...@@ -352,6 +352,10 @@ class ApplicationController < ActionController::Base
session[:skip_tfa] && session[:skip_tfa] > Time.current session[:skip_tfa] && session[:skip_tfa] > Time.current
end end
def browser_supports_u2f?
browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
end
def redirect_to_home_page_url? def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page # If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections # Don't redirect to the default URL to prevent endless redirections
...@@ -365,6 +369,13 @@ class ApplicationController < ActionController::Base ...@@ -365,6 +369,13 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path current_user.nil? && root_path == request.path
end end
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
def u2f_app_id
request.base_url
end
private private
def set_default_sort def set_default_sort
......
...@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor ...@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
elsif user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
private
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor
end
end
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges])
# Remove any lingering user data from login
session.delete(:otp_user_id)
session.delete(:challenges)
sign_in(user)
else
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
end
render 'devise/sessions/two_factor' and return # Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_authentication(user)
key_handles = user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
if key_handles.present?
sign_requests = u2f.authentication_requests(key_handles)
challenges = sign_requests.map(&:challenge)
session[:challenges] = challenges
gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
end end
...@@ -9,13 +9,22 @@ module ToggleAwardEmoji ...@@ -9,13 +9,22 @@ module ToggleAwardEmoji
name = params.require(:name) name = params.require(:name)
awardable.toggle_award_emoji(name, current_user) awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(awardable, current_user) TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true } render json: { ok: true }
end end
private private
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
else
awardable
end
end
def awardable def awardable
raise NotImplementedError raise NotImplementedError
end end
......
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement skip_before_action :check_2fa_requirement
def new def show
unless current_user.otp_secret unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32) current_user.otp_secret = User.generate_otp_secret(32)
end end
...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed? current_user.save! if current_user.changed?
if two_factor_authentication_required? if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired? if two_factor_grace_period_expired?
flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.'
else else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}."
end end
end end
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
end end
def create def create
if current_user.validate_and_consume_otp!(params[:pin_code]) if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.two_factor_enabled = true current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes! @codes = current_user.generate_otp_backup_codes!
current_user.save! current_user.save!
...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else else
@error = 'Invalid pin code' @error = 'Invalid pin code'
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
render 'show'
end
end
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
render 'new' if @u2f_registration.persisted?
session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
render :show
end end
end end
...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def issuer_host def issuer_host
Gitlab.config.gitlab.host Gitlab.config.gitlab.host
end end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles)
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
...@@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
private private
def build def build
@build ||= project.builds.unscoped.find_by!(id: params[:build_id]) @build ||= project.builds.find_by!(id: params[:build_id])
end end
def artifacts_file def artifacts_file
......
...@@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController
private private
def build def build
@build ||= project.builds.unscoped.find_by!(id: params[:id]) @build ||= project.builds.find_by!(id: params[:id])
end end
def build_path(build) def build_path(build)
......
class Projects::NotesController < Projects::ApplicationController class Projects::NotesController < Projects::ApplicationController
include ToggleAwardEmoji
# Authorize # Authorize
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
...@@ -61,6 +63,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -61,6 +63,7 @@ class Projects::NotesController < Projects::ApplicationController
def note def note
@note ||= @project.notes.find(params[:id]) @note ||= @project.notes.find(params[:id])
end end
alias_method :awardable, :note
def note_to_html(note) def note_to_html(note)
render_to_string( render_to_string(
......
...@@ -31,8 +31,7 @@ class SessionsController < Devise::SessionsController ...@@ -31,8 +31,7 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil, resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil) reset_password_sent_at: nil)
end end
authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard" log_audit_event(current_user, with: authentication_method)
log_audit_event(current_user, with: authenticated_with)
end end
end end
...@@ -55,7 +54,7 @@ class SessionsController < Devise::SessionsController ...@@ -55,7 +54,7 @@ class SessionsController < Devise::SessionsController
end end
def user_params def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
end end
def find_user def find_user
...@@ -92,27 +91,6 @@ class SessionsController < Devise::SessionsController ...@@ -92,27 +91,6 @@ class SessionsController < Devise::SessionsController
find_user.try(:two_factor_enabled?) find_user.try(:two_factor_enabled?)
end end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) and return
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor and return
end
else
if user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
end
def gitlab_geo_login def gitlab_geo_login
return unless Gitlab::Geo.secondary? return unless Gitlab::Geo.secondary?
return if signed_in? return if signed_in?
...@@ -161,4 +139,14 @@ class SessionsController < Devise::SessionsController ...@@ -161,4 +139,14 @@ class SessionsController < Devise::SessionsController
def load_recaptcha def load_recaptcha
Gitlab::Recaptcha.load_configurations! Gitlab::Recaptcha.load_configurations!
end end
def authentication_method
if user_params[:otp_attempt]
"two-factor"
elsif user_params[:device_response]
"two-factor-via-u2f-device"
else
"standard"
end
end
end end
...@@ -70,7 +70,7 @@ module AuthHelper ...@@ -70,7 +70,7 @@ module AuthHelper
def two_factor_skippable? def two_factor_skippable?
current_application_settings.require_two_factor_authentication && current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled && !current_user.two_factor_enabled? &&
current_application_settings.two_factor_grace_period && current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired? !two_factor_grace_period_expired?
end end
......
...@@ -4,6 +4,7 @@ class Note < ActiveRecord::Base ...@@ -4,6 +4,7 @@ class Note < ActiveRecord::Base
include Participable include Participable
include Mentionable include Mentionable
include Elastic::NotesSearch include Elastic::NotesSearch
include Awardable
default_value_for :system, false default_value_for :system, false
......
...@@ -83,7 +83,7 @@ class IrkerService < Service ...@@ -83,7 +83,7 @@ class IrkerService < Service
self.channels = recipients.split(/\s+/).map do |recipient| self.channels = recipients.split(/\s+/).map do |recipient|
format_channel(recipient) format_channel(recipient)
end end
channels.reject! &:nil? channels.reject!(&:nil?)
end end
def format_channel(recipient) def format_channel(recipient)
......
# Registration information for U2F (universal 2nd factor) devices, like Yubikeys
class U2fRegistration < ActiveRecord::Base
belongs_to :user
def self.register(user, app_id, json_response, challenges)
u2f = U2F::U2F.new(app_id)
registration = self.new
begin
response = U2F::RegisterResponse.load_from_json(json_response)
registration_data = u2f.register!(challenges, response)
registration.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle,
public_key: registration_data.public_key,
counter: registration_data.counter,
user: user)
rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e
registration.errors.add(:base, e.message)
end
registration
end
def self.authenticate(user, app_id, json_response, challenges)
response = U2F::SignResponse.load_from_json(json_response)
registration = user.u2f_registrations.find_by_key_handle(response.key_handle)
u2f = U2F::U2F.new(app_id)
if registration
u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter)
registration.update(counter: response.counter)
true
end
rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error
false
end
end
...@@ -27,7 +27,6 @@ class User < ActiveRecord::Base ...@@ -27,7 +27,6 @@ class User < ActiveRecord::Base
devise :two_factor_authenticatable, devise :two_factor_authenticatable,
otp_secret_encryption_key: Gitlab::Application.config.secret_key_base otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
alias_attribute :two_factor_enabled, :otp_required_for_login
devise :two_factor_backupable, otp_number_of_backup_codes: 10 devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON serialize :otp_backup_codes, JSON
...@@ -51,6 +50,7 @@ class User < ActiveRecord::Base ...@@ -51,6 +50,7 @@ class User < ActiveRecord::Base
has_many :keys, dependent: :destroy has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy has_many :emails, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
# Groups # Groups
has_many :members, dependent: :destroy has_many :members, dependent: :destroy
...@@ -179,12 +179,20 @@ class User < ActiveRecord::Base ...@@ -179,12 +179,20 @@ class User < ActiveRecord::Base
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) } scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
scope :ldap, -> { joins(:identities).where('identities.provider LIKE ?', 'ldap%') } scope :ldap, -> { joins(:identities).where('identities.provider LIKE ?', 'ldap%') }
scope :with_two_factor, -> { where(two_factor_enabled: true) }
scope :without_two_factor, -> { where(two_factor_enabled: false) }
scope :with_provider, ->(provider) do scope :with_provider, ->(provider) do
joins(:identities).where(identities: { provider: provider }) joins(:identities).where(identities: { provider: provider })
end end
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
end
def self.without_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
where("u2f.id IS NULL AND otp_required_for_login = ?", false)
end
# #
# Class methods # Class methods
# #
...@@ -355,14 +363,29 @@ class User < ActiveRecord::Base ...@@ -355,14 +363,29 @@ class User < ActiveRecord::Base
end end
def disable_two_factor! def disable_two_factor!
transaction do
update_attributes( update_attributes(
two_factor_enabled: false, otp_required_for_login: false,
encrypted_otp_secret: nil, encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil, encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil, encrypted_otp_secret_salt: nil,
otp_grace_period_started_at: nil, otp_grace_period_started_at: nil,
otp_backup_codes: nil otp_backup_codes: nil
) )
self.u2f_registrations.destroy_all
end
end
def two_factor_enabled?
two_factor_otp_enabled? || two_factor_u2f_enabled?
end
def two_factor_otp_enabled?
self.otp_required_for_login?
end
def two_factor_u2f_enabled?
self.u2f_registrations.exists?
end end
def namespace_uniq def namespace_uniq
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
gl.awardMenuUrl = "#{emojis_path}" gl.awardMenuUrl = "#{emojis_path}"
.award-menu-holder.js-award-holder .award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } %button.btn.award-control.js-add-award{ type: "button" }
= icon('smile-o', class: "award-control-icon award-control-icon-normal") = icon('smile-o', class: "award-control-icon award-control-icon-normal")
= icon('spinner spin', class: "award-control-icon award-control-icon-loading") = icon('spinner spin', class: "award-control-icon award-control-icon-loading")
%span.award-control-text %span.award-control-text
......
%div %div
.login-box .login-box
.login-heading .login-heading
%h3 Two-factor Authentication %h3 Two-Factor Authentication
.login-body .login-body
- if @user.two_factor_otp_enabled?
%h5 Authenticate via Two-Factor App
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
= f.hidden_field :remember_me, value: params[resource_name][:remember_me] = f.hidden_field :remember_me, value: params[resource_name][:remember_me]
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true, autocomplete: 'off' = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20 .prepend-top-20
= f.submit "Verify code", class: "btn btn-save" = f.submit "Verify code", class: "btn btn-save"
- if @user.two_factor_u2f_enabled?
%hr
= render "u2f/authenticate"
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
%td Show/hide this dialog %td Show/hide this dialog
%tr %tr
%td.shortcut %td.shortcut
- if browser.mac? - if browser.platform.mac?
.key &#8984; shift p .key &#8984; shift p
- else - else
.key ctrl shift p .key ctrl shift p
......
...@@ -35,8 +35,6 @@ ...@@ -35,8 +35,6 @@
= csrf_meta_tags = csrf_meta_tags
= include_gon
- unless browser.safari? - unless browser.safari?
%meta{name: 'referrer', content: 'origin-when-cross-origin'} %meta{name: 'referrer', content: 'origin-when-cross-origin'}
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body{class: "#{user_application_theme}", 'data-page' => body_data_page} %body{class: "#{user_application_theme}", 'data-page' => body_data_page}
= Gon::Base.render_data
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top = yield :scripts_body_top
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body.ui_charcoal.login-page.application.navless %body.ui_charcoal.login-page.application.navless
= Gon::Base.render_data
= render "layouts/header/empty" = render "layouts/header/empty"
= render "layouts/broadcast" = render "layouts/broadcast"
.container.navless-container .container.navless-container
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body.ui_charcoal.login-page.application.navless %body.ui_charcoal.login-page.application.navless
= Gon::Base.render_data
= render "layouts/header/empty" = render "layouts/header/empty"
= render "layouts/broadcast" = render "layouts/broadcast"
.container.navless-container .container.navless-container
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body{class: "#{user_application_theme} application navless"} %body{class: "#{user_application_theme} application navless"}
= Gon::Base.render_data
= render "layouts/header/empty" = render "layouts/header/empty"
.container.navless-container .container.navless-container
= render "layouts/flash" = render "layouts/flash"
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
%p %p
Your private token is used to access application resources without authentication. Your private token is used to access application resources without authentication.
.col-lg-9 .col-lg-9
= form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f| = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f|
%p.cgray %p.cgray
- if current_user.private_token - if current_user.private_token
= label_tag "token", "Private token", class: "label-light" = label_tag "token", "Private token", class: "label-light"
...@@ -29,21 +29,22 @@ ...@@ -29,21 +29,22 @@
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Two-factor Authentication Two-Factor Authentication
%p %p
Increase your account's security by enabling two-factor authentication (2FA). Increase your account's security by enabling Two-Factor Authentication (2FA).
.col-lg-9 .col-lg-9
%p %p
Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
- if !current_user.two_factor_enabled? - if current_user.two_factor_enabled?
%p = link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info'
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. = link_to 'Disable', profile_two_factor_auth_path,
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. method: :delete,
.append-bottom-10 data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
= link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' class: 'btn btn-danger'
- else - else
= link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger', .append-bottom-10
data: { confirm: 'Are you sure?' } = link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success'
%hr %hr
- if button_based_providers.any? - if button_based_providers.any?
.row.prepend-top-default .row.prepend-top-default
......
- page_title 'Two-factor Authentication', 'Account'
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Two-factor Authentication (2FA)
%p
Increase your account's security by enabling two-factor authentication (2FA).
.col-lg-9
%p
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.row.append-bottom-10
.col-md-3
= raw @qr_code
.col-md-9
.account-well
%p.prepend-top-0.append-bottom-0
Can't scan the code?
%p.prepend-top-0.append-bottom-0
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
= current_user.email
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
%p.two-factor-new-manual-content
Time based: Yes
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
.alert.alert-danger
= @error
.form-group
= label_tag :pin_code, nil, class: "label-light"
= text_field_tag :pin_code, nil, class: "form-control", required: true
.prepend-top-default
= submit_tag 'Enable two-factor authentication', class: 'btn btn-success'
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
- page_title 'Two-Factor Authentication', 'Account'
- header_title "Two-Factor Authentication", profile_two_factor_auth_path
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Register Two-Factor Authentication App
%p
Use an app on your mobile device to enable two-factor authentication (2FA).
.col-lg-9
- if current_user.two_factor_otp_enabled?
= icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page."
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.row.append-bottom-10
.col-md-3
= raw @qr_code
.col-md-9
.account-well
%p.prepend-top-0.append-bottom-0
Can't scan the code?
%p.prepend-top-0.append-bottom-0
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
= current_user.email
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
%p.two-factor-new-manual-content
Time based: Yes
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
.alert.alert-danger
= @error
.form-group
= label_tag :pin_code, nil, class: "label-light"
= text_field_tag :pin_code, nil, class: "form-control", required: true
.prepend-top-default
= submit_tag 'Register with Two-Factor App', class: 'btn btn-success'
%hr
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Register Universal Two-Factor (U2F) Device
%p
Use a hardware device to add the second factor of authentication.
%p
As U2F devices are only supported by a few browsers, it's recommended that you set up a
two-factor authentication app as well as a U2F device so you'll always be able to log in
using an unsupported browser.
.col-lg-9
%p
- if @registration_key_handles.present?
= icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
= render "u2f/register"
- if two_factor_skippable?
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
$(".flash-alert").append(button);
...@@ -22,6 +22,9 @@ ...@@ -22,6 +22,9 @@
%span.note-role %span.note-role
= access = access
- if note_editable - if note_editable
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
= icon('smile-o')
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil') = icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
...@@ -30,9 +33,11 @@ ...@@ -30,9 +33,11 @@
.note-text .note-text
= preserve do = preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable - if note_editable
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) .note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.attachment.url - if note.attachment.url
.note-attachment .note-attachment
......
#js-authenticate-u2f
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
%script#js-authenticate-u2f-setup{ type: "text/template" }
%div
%p Insert your security key (if you haven't already), and press the button below.
%a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
%script#js-authenticate-u2f-error{ type: "text/template" }
%div
%p <%= error_message %>
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
%div
%p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
= form_tag(new_user_session_path, method: :post) do |f|
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Authenticate via U2F Device", class: "btn btn-success"
:javascript
var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f);
u2fAuthenticate.start();
#js-register-u2f
%script#js-register-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
%script#js-register-u2f-setup{ type: "text/template" }
.row.append-bottom-10
.col-md-3
%a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device
.col-md-9
%p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
%script#js-register-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
%span <%= error_message %>
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10
%p Your device was successfully set up! Click this button to register with the GitLab server.
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
= hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Register U2F Device", class: "btn btn-success"
:javascript
var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
u2fRegister.start();
...@@ -362,8 +362,9 @@ Rails.application.routes.draw do ...@@ -362,8 +362,9 @@ Rails.application.routes.draw do
resources :keys resources :keys
resources :emails, only: [:index, :create, :destroy] resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy] resource :avatar, only: [:destroy]
resource :two_factor_auth, only: [:new, :create, :destroy] do resource :two_factor_auth, only: [:show, :create, :destroy] do
member do member do
post :create_u2f
post :codes post :codes
patch :skip patch :skip
end end
...@@ -810,6 +811,7 @@ Rails.application.routes.draw do ...@@ -810,6 +811,7 @@ Rails.application.routes.draw do
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do member do
post :toggle_award_emoji
delete :delete_attachment delete :delete_attachment
end end
end end
......
class CreateU2fRegistrations < ActiveRecord::Migration
def change
create_table :u2f_registrations do |t|
t.text :certificate
t.string :key_handle, index: true
t.string :public_key
t.integer :counter
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
# 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: 20160530214349) do ActiveRecord::Schema.define(version: 20160530214349) 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"
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -1071,6 +1070,19 @@ ActiveRecord::Schema.define(version: 20160530214349) do ...@@ -1071,6 +1070,19 @@ ActiveRecord::Schema.define(version: 20160530214349) do
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
create_table "u2f_registrations", force: :cascade do |t|
t.text "certificate"
t.string "key_handle"
t.string "public_key"
t.integer "counter"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false t.string "encrypted_password", default: "", null: false
...@@ -1182,4 +1194,5 @@ ActiveRecord::Schema.define(version: 20160530214349) do ...@@ -1182,4 +1194,5 @@ ActiveRecord::Schema.define(version: 20160530214349) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_foreign_key "remote_mirrors", "projects" add_foreign_key "remote_mirrors", "projects"
add_foreign_key "u2f_registrations", "users"
end end
...@@ -8,12 +8,27 @@ your phone. ...@@ -8,12 +8,27 @@ your phone.
By enabling 2FA, the only way someone other than you can log into your account By enabling 2FA, the only way someone other than you can log into your account
is to know your username and password *and* have access to your phone. is to know your username and password *and* have access to your phone.
#### Note > **Note:**
When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you
lose your codes for GitLab.com, we can't disable or recover them. lose your codes for GitLab.com, we can't disable or recover them.
In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
the second factor of authentication. Once enabled, in addition to supplying your username and
password to login, you'll be prompted to activate your U2F device (usually by pressing
a button on it), and it will perform secure authentication on your behalf.
> **Note:** Support for U2F devices was added in version 8.8
The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
that you set up both methods of two-factor authentication, so you can still access your account
from other browsers.
> **Note:** GitLab officially only supports [Yubikey] U2F devices.
## Enabling 2FA ## Enabling 2FA
### Enable 2FA via mobile application
**In GitLab:** **In GitLab:**
1. Log in to your GitLab account. 1. Log in to your GitLab account.
...@@ -38,9 +53,26 @@ lose your codes for GitLab.com, we can't disable or recover them. ...@@ -38,9 +53,26 @@ lose your codes for GitLab.com, we can't disable or recover them.
1. Click **Submit**. 1. Click **Submit**.
If the pin you entered was correct, you'll see a message indicating that If the pin you entered was correct, you'll see a message indicating that
Two-factor Authentication has been enabled, and you'll be presented with a list Two-Factor Authentication has been enabled, and you'll be presented with a list
of recovery codes. of recovery codes.
### Enable 2FA via U2F device
**In GitLab:**
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
1. Click **Enable Two-Factor Authentication**.
1. Plug in your U2F device.
1. Click on **Setup New U2F Device**.
1. A light will start blinking on your device. Activate it by pressing its button.
You will see a message indicating that your device was successfully set up.
Click on **Register U2F Device** to complete the process.
![Two-Factor U2F Setup](2fa_u2f_register.png)
## Recovery Codes ## Recovery Codes
Should you ever lose access to your phone, you can use one of the ten provided Should you ever lose access to your phone, you can use one of the ten provided
...@@ -51,21 +83,39 @@ account. ...@@ -51,21 +83,39 @@ account.
If you lose the recovery codes or just want to generate new ones, you can do so If you lose the recovery codes or just want to generate new ones, you can do so
from the **Profile Settings** > **Account** page where you first enabled 2FA. from the **Profile Settings** > **Account** page where you first enabled 2FA.
> **Note:** Recovery codes are not generated for U2F devices.
## Logging in with 2FA Enabled ## Logging in with 2FA Enabled
Logging in with 2FA enabled is only slightly different than a normal login. Logging in with 2FA enabled is only slightly different than a normal login.
Enter your username and password credentials as you normally would, and you'll Enter your username and password credentials as you normally would, and you'll
be presented with a second prompt for an authentication code. Enter the pin from be presented with a second prompt, depending on which type of 2FA you've enabled.
your phone's application or a recovery code to log in.
### Log in via mobile application
Enter the pin from your phone's application or a recovery code to log in.
![Two-factor authentication on sign in](2fa_auth.png) ![Two-Factor Authentication on sign in via OTP](2fa_auth.png)
### Log in via U2F device
1. Click **Login via U2F Device**
1. A light will start blinking on your device. Activate it by pressing its button.
You will see a message indicating that your device responded to the authentication request.
Click on **Authenticate via U2F Device** to complete the process.
![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png)
## Disabling 2FA ## Disabling 2FA
1. Log in to your GitLab account. 1. Log in to your GitLab account.
1. Go to your **Profile Settings**. 1. Go to your **Profile Settings**.
1. Go to **Account**. 1. Go to **Account**.
1. Click **Disable Two-factor Authentication**. 1. Click **Disable**, under **Two-Factor Authentication**.
This will clear all your two-factor authentication registrations, including mobile
applications and U2F devices.
## Note to GitLab administrators ## Note to GitLab administrators
...@@ -74,3 +124,4 @@ You need to take special care to that 2FA keeps working after ...@@ -74,3 +124,4 @@ You need to take special care to that 2FA keeps working after
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
[FreeOTP]: https://fedorahosted.org/freeotp/ [FreeOTP]: https://fedorahosted.org/freeotp/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
...@@ -30,7 +30,7 @@ module API ...@@ -30,7 +30,7 @@ module API
expose :identities, using: Entities::Identity expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project expose :can_create_project?, as: :can_create_project
expose :two_factor_enabled expose :two_factor_enabled?, as: :two_factor_enabled
expose :external expose :external
end end
......
...@@ -57,7 +57,7 @@ module API ...@@ -57,7 +57,7 @@ module API
not_found! "File" unless blob not_found! "File" unless blob
content_type 'text/plain' content_type 'text/plain'
header *Gitlab::Workhorse.send_git_blob(repo, blob) header(*Gitlab::Workhorse.send_git_blob(repo, blob))
end end
# Get a raw blob contents by blob sha # Get a raw blob contents by blob sha
...@@ -83,7 +83,7 @@ module API ...@@ -83,7 +83,7 @@ module API
env['api.format'] = :txt env['api.format'] = :txt
content_type blob.mime_type content_type blob.mime_type
header *Gitlab::Workhorse.send_git_blob(repo, blob) header(*Gitlab::Workhorse.send_git_blob(repo, blob))
end end
# Get a an archive of the repository # Get a an archive of the repository
...@@ -98,7 +98,7 @@ module API ...@@ -98,7 +98,7 @@ module API
authorize! :download_code, user_project authorize! :download_code, user_project
begin begin
header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) header(*Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]))
rescue rescue
not_found!('File') not_found!('File')
end end
......
...@@ -17,9 +17,9 @@ module Gitlab ...@@ -17,9 +17,9 @@ module Gitlab
file.rewind file.rewind
cmd = [] cmd = []
cmd.push *%W(ssh-keygen) cmd.push('ssh-keygen')
cmd.push *%W(-E md5) if explicit_fingerprint_algorithm? cmd.push('-E', 'md5') if explicit_fingerprint_algorithm?
cmd.push *%W(-lf #{file.path}) cmd.push('-lf', file.path)
cmd_output, cmd_status = popen(cmd, '/tmp') cmd_output, cmd_status = popen(cmd, '/tmp')
end end
......
...@@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do ...@@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do
allow(subject).to receive(:current_user).and_return(user) allow(subject).to receive(:current_user).and_return(user)
end end
describe 'GET new' do describe 'GET show' do
let(:user) { create(:user) } let(:user) { create(:user) }
it 'generates otp_secret for user' do it 'generates otp_secret for user' do
expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once
get :new get :show
get :new # Second hit shouldn't re-generate it get :show # Second hit shouldn't re-generate it
end end
it 'assigns qr_code' do it 'assigns qr_code' do
code = double('qr code') code = double('qr code')
expect(subject).to receive(:build_qr_code).and_return(code) expect(subject).to receive(:build_qr_code).and_return(code)
get :new get :show
expect(assigns[:qr_code]).to eq code expect(assigns[:qr_code]).to eq code
end end
end end
...@@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do ...@@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do
expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true) expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
end end
it 'sets two_factor_enabled' do it 'enables 2fa for the user' do
go go
user.reload user.reload
...@@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do ...@@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do
expect(assigns[:qr_code]).to eq code expect(assigns[:qr_code]).to eq code
end end
it 'renders new' do it 'renders show' do
go go
expect(response).to render_template(:new) expect(response).to render_template(:show)
end end
end end
end end
......
require('spec_helper')
describe Projects::NotesController do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let(:note) { create(:note, noteable: issue, project: project) }
describe 'POST #toggle_award_emoji' do
before do
sign_in(user)
project.team << [user, :developer]
end
it "toggles the award emoji" do
expect do
post(:toggle_award_emoji, namespace_id: project.namespace.path,
project_id: project.path, id: note.id, name: "thumbsup")
end.to change { note.award_emoji.count }.by(1)
expect(response.status).to eq(200)
end
it "removes the already awarded emoji" do
post(:toggle_award_emoji, namespace_id: project.namespace.path,
project_id: project.path, id: note.id, name: "thumbsup")
expect do
post(:toggle_award_emoji, namespace_id: project.namespace.path,
project_id: project.path, id: note.id, name: "thumbsup")
end.to change { AwardEmoji.count }.by(-1)
expect(response.status).to eq(200)
end
end
end
...@@ -25,10 +25,15 @@ describe SessionsController do ...@@ -25,10 +25,15 @@ describe SessionsController do
expect(response).to set_flash.to /Signed in successfully/ expect(response).to set_flash.to /Signed in successfully/
expect(subject.current_user). to eq user expect(subject.current_user). to eq user
end end
it "creates an audit log record" do
expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq("standard")
end
end end
end end
context 'when using two-factor authentication' do context 'when using two-factor authentication via OTP' do
let(:user) { create(:user, :two_factor) } let(:user) { create(:user, :two_factor) }
def authenticate_2fa(user_params) def authenticate_2fa(user_params)
...@@ -117,6 +122,25 @@ describe SessionsController do ...@@ -117,6 +122,25 @@ describe SessionsController do
end end
end end
end end
it "creates an audit log record" do
expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq("two-factor")
end
end
context 'when using two-factor authentication via U2F device' do
let(:user) { create(:user, :two_factor) }
def authenticate_2fa_u2f(user_params)
post(:create, { user: user_params }, { otp_user_id: user.id })
end
it "creates an audit log record" do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
end
end end
end end
end end
FactoryGirl.define do
factory :u2f_registration do
certificate { FFaker::BaconIpsum.characters(728) }
key_handle { FFaker::BaconIpsum.characters(86) }
public_key { FFaker::BaconIpsum.characters(88) }
counter 0
end
end
...@@ -15,14 +15,26 @@ FactoryGirl.define do ...@@ -15,14 +15,26 @@ FactoryGirl.define do
end end
trait :two_factor do trait :two_factor do
two_factor_via_otp
end
trait :two_factor_via_otp do
before(:create) do |user| before(:create) do |user|
user.two_factor_enabled = true user.otp_required_for_login = true
user.otp_secret = User.generate_otp_secret(32) user.otp_secret = User.generate_otp_secret(32)
user.otp_grace_period_started_at = Time.now user.otp_grace_period_started_at = Time.now
user.generate_otp_backup_codes! user.generate_otp_backup_codes!
end end
end end
trait :two_factor_via_u2f do
transient { registrations_count 5 }
after(:create) do |user, evaluator|
create_list(:u2f_registration, evaluator.registrations_count, user: user)
end
end
factory :omniauth_user do factory :omniauth_user do
transient do transient do
extern_uid '123456' extern_uid '123456'
......
...@@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do ...@@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication filters' do describe 'Two-factor Authentication filters' do
it 'counts users who have enabled 2FA' do it 'counts users who have enabled 2FA' do
create(:user, two_factor_enabled: true) create(:user, :two_factor)
visit admin_users_path visit admin_users_path
...@@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do ...@@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do
end end
it 'filters by users who have enabled 2FA' do it 'filters by users who have enabled 2FA' do
user = create(:user, two_factor_enabled: true) user = create(:user, :two_factor)
visit admin_users_path visit admin_users_path
click_link '2FA Enabled' click_link '2FA Enabled'
...@@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do ...@@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do
end end
it 'counts users who have not enabled 2FA' do it 'counts users who have not enabled 2FA' do
create(:user, two_factor_enabled: false) create(:user)
visit admin_users_path visit admin_users_path
...@@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do ...@@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do
end end
it 'filters by users who have not enabled 2FA' do it 'filters by users who have not enabled 2FA' do
user = create(:user, two_factor_enabled: false) user = create(:user)
visit admin_users_path visit admin_users_path
click_link '2FA Disabled' click_link '2FA Disabled'
...@@ -173,7 +173,7 @@ describe "Admin::Users", feature: true do ...@@ -173,7 +173,7 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication status' do describe 'Two-factor Authentication status' do
it 'shows when enabled' do it 'shows when enabled' do
@user.update_attribute(:two_factor_enabled, true) @user.update_attribute(:otp_required_for_login, true)
visit admin_user_path(@user) visit admin_user_path(@user)
......
...@@ -7,6 +7,7 @@ describe "Builds" do ...@@ -7,6 +7,7 @@ describe "Builds" do
login_as(:user) login_as(:user)
@commit = FactoryGirl.create :ci_commit @commit = FactoryGirl.create :ci_commit
@build = FactoryGirl.create :ci_build, commit: @commit @build = FactoryGirl.create :ci_build, commit: @commit
@build2 = FactoryGirl.create :ci_build
@project = @commit.project @project = @commit.project
@project.team << [@user, :developer] @project.team << [@user, :developer]
end end
...@@ -66,13 +67,24 @@ describe "Builds" do ...@@ -66,13 +67,24 @@ describe "Builds" do
end end
describe "GET /:project/builds/:id" do describe "GET /:project/builds/:id" do
context "Build from project" do
before do before do
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
end end
it { expect(page.status_code).to eq(200) }
it { expect(page).to have_content @commit.sha[0..7] } it { expect(page).to have_content @commit.sha[0..7] }
it { expect(page).to have_content @commit.git_commit_message } it { expect(page).to have_content @commit.git_commit_message }
it { expect(page).to have_content @commit.git_author_name } it { expect(page).to have_content @commit.git_author_name }
end
context "Build from other project" do
before do
visit namespace_project_build_path(@project.namespace, @project, @build2)
end
it { expect(page.status_code).to eq(404) }
end
context "Download artifacts" do context "Download artifacts" do
before do before do
...@@ -103,51 +115,143 @@ describe "Builds" do ...@@ -103,51 +115,143 @@ describe "Builds" do
end end
describe "POST /:project/builds/:id/cancel" do describe "POST /:project/builds/:id/cancel" do
context "Build from project" do
before do before do
@build.run! @build.run!
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
click_link "Cancel" click_link "Cancel"
end end
it { expect(page.status_code).to eq(200) }
it { expect(page).to have_content 'canceled' } it { expect(page).to have_content 'canceled' }
it { expect(page).to have_content 'Retry' } it { expect(page).to have_content 'Retry' }
end end
context "Build from other project" do
before do
@build.run!
visit namespace_project_build_path(@project.namespace, @project, @build)
page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2))
end
it { expect(page.status_code).to eq(404) }
end
end
describe "POST /:project/builds/:id/retry" do describe "POST /:project/builds/:id/retry" do
context "Build from project" do
before do before do
@build.run! @build.run!
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
click_link "Cancel" click_link 'Cancel'
click_link 'Retry' click_link 'Retry'
end end
it { expect(page.status_code).to eq(200) }
it { expect(page).to have_content 'pending' } it { expect(page).to have_content 'pending' }
it { expect(page).to have_content 'Cancel' } it { expect(page).to have_content 'Cancel' }
end end
context "Build from other project" do
before do
@build.run!
visit namespace_project_build_path(@project.namespace, @project, @build)
click_link 'Cancel'
page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2))
end
it { expect(page.status_code).to eq(404) }
end
end
describe "GET /:project/builds/:id/download" do describe "GET /:project/builds/:id/download" do
context "Build from project" do
before do before do
@build.update_attributes(artifacts_file: artifacts_file) @build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
page.within('.artifacts') { click_link 'Download' } page.within('.artifacts') { click_link 'Download' }
end end
it { expect(page.status_code).to eq(200) }
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
end end
context "Build from other project" do
before do
@build2.update_attributes(artifacts_file: artifacts_file)
visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2)
end
it { expect(page.status_code).to eq(404) }
end
end
describe "GET /:project/builds/:id/raw" do describe "GET /:project/builds/:id/raw" do
context "Build from project" do
before do before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
@build.run! @build.run!
@build.trace = 'BUILD TRACE' @build.trace = 'BUILD TRACE'
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
page.within('.build-controls') { click_link 'Raw' }
end end
it 'sends the right headers' do it 'sends the right headers' do
page.within('.build-controls') { click_link 'Raw' } expect(page.status_code).to eq(200)
expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
end end
end end
context "Build from other project" do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
@build2.run!
@build2.trace = 'BUILD TRACE'
visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
puts page.status_code
puts current_url
end
it 'sends the right headers' do
expect(page.status_code).to eq(404)
end
end
end
describe "GET /:project/builds/:id/trace.json" do
context "Build from project" do
before do
visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json)
end
it { expect(page.status_code).to eq(200) }
end
context "Build from other project" do
before do
visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json)
end
it { expect(page.status_code).to eq(404) }
end
end
describe "GET /:project/builds/:id/status" do
context "Build from project" do
before do
visit status_namespace_project_build_path(@project.namespace, @project, @build)
end
it { expect(page.status_code).to eq(200) }
end
context "Build from other project" do
before do
visit status_namespace_project_build_path(@project.namespace, @project, @build2)
end
it { expect(page.status_code).to eq(404) }
end
end
end end
...@@ -365,13 +365,9 @@ describe 'Issues', feature: true do ...@@ -365,13 +365,9 @@ describe 'Issues', feature: true do
page.within('.assignee') do page.within('.assignee') do
expect(page).to have_content "#{@user.name}" expect(page).to have_content "#{@user.name}"
end
find('.block.assignee .edit-link').click click_link 'Edit'
sleep 2 # wait for ajax stuff to complete click_link 'Unassigned'
first('.dropdown-menu-user-link').click
sleep 2
page.within('.assignee') do
expect(page).to have_content 'No assignee' expect(page).to have_content 'No assignee'
end end
......
...@@ -33,11 +33,11 @@ feature 'Login', feature: true do ...@@ -33,11 +33,11 @@ feature 'Login', feature: true do
before do before do
login_with(user, remember: true) login_with(user, remember: true)
expect(page).to have_content('Two-factor Authentication') expect(page).to have_content('Two-Factor Authentication')
end end
def enter_code(code) def enter_code(code)
fill_in 'Two-factor Authentication code', with: code fill_in 'Two-Factor Authentication code', with: code
click_button 'Verify code' click_button 'Verify code'
end end
...@@ -143,12 +143,12 @@ feature 'Login', feature: true do ...@@ -143,12 +143,12 @@ feature 'Login', feature: true do
context 'within the grace period' do context 'within the grace period' do
it 'redirects to two-factor configuration page' do it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content('You must enable Two-factor Authentication for your account before') expect(page).to have_content('You must enable Two-Factor Authentication for your account before')
end end
it 'disallows skipping two-factor configuration' do it 'allows skipping two-factor configuration', js: true do
expect(current_path).to eq new_profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later' click_link 'Configure it later'
expect(current_path).to eq root_path expect(current_path).to eq root_path
...@@ -159,26 +159,26 @@ feature 'Login', feature: true do ...@@ -159,26 +159,26 @@ feature 'Login', feature: true do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) } let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content('You must enable Two-factor Authentication for your account.') expect(page).to have_content('You must enable Two-Factor Authentication for your account.')
end end
it 'disallows skipping two-factor configuration' do it 'disallows skipping two-factor configuration', js: true do
expect(current_path).to eq new_profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later') expect(page).not_to have_link('Configure it later')
end end
end end
end end
context 'without grace pariod defined' do context 'without grace period defined' do
before(:each) do before(:each) do
stub_application_setting(two_factor_grace_period: 0) stub_application_setting(two_factor_grace_period: 0)
login_with(user) login_with(user)
end end
it 'redirects to two-factor configuration page' do it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content('You must enable Two-factor Authentication for your account.') expect(page).to have_content('You must enable Two-Factor Authentication for your account.')
end end
end end
end end
......
require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
def register_u2f_device(u2f_device = nil)
u2f_device ||= FakeU2fDevice.new(page)
u2f_device.respond_to_u2f_registration
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
click_on 'Register U2F Device'
u2f_device
end
describe "registration" do
let(:user) { create(:user) }
before { login_as(user) }
describe 'when 2FA via OTP is disabled' do
it 'allows registering a new device' do
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
end
it 'allows registering more than one device' do
visit profile_account_path
# First device
click_on 'Enable Two-Factor Authentication'
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
# Second device
click_on 'Manage Two-Factor Authentication'
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
click_on 'Manage Two-Factor Authentication'
expect(page.body).to match('You have 2 U2F devices registered')
end
end
describe 'when 2FA via OTP is enabled' do
before { user.update_attributes(otp_required_for_login: true) }
it 'allows registering a new device' do
visit profile_account_path
click_on 'Manage Two-Factor Authentication'
expect(page.body).to match("You've already enabled two-factor authentication using mobile")
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
end
it 'allows registering more than one device' do
visit profile_account_path
# First device
click_on 'Manage Two-Factor Authentication'
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
# Second device
click_on 'Manage Two-Factor Authentication'
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
click_on 'Manage Two-Factor Authentication'
expect(page.body).to match('You have 2 U2F devices registered')
end
end
it 'allows the same device to be registered for multiple users' do
# First user
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
u2f_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
logout
# Second user
login_as(:user)
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
register_u2f_device(u2f_device)
expect(page.body).to match('Your U2F device was registered')
expect(U2fRegistration.count).to eq(2)
end
context "when there are form errors" do
it "doesn't register the device if there are errors" do
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
# Have the "u2f device" respond with bad data
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
click_on 'Register U2F Device'
expect(U2fRegistration.count).to eq(0)
expect(page.body).to match("The form contains the following error")
expect(page.body).to match("did not send a valid JSON response")
end
it "allows retrying registration" do
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
# Failed registration
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
click_on 'Register U2F Device'
expect(page.body).to match("The form contains the following error")
# Successful registration
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
expect(U2fRegistration.count).to eq(1)
end
end
end
describe "authentication" do
let(:user) { create(:user) }
before do
# Register and logout
login_as(user)
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
@u2f_device = register_u2f_device
logout
end
describe "when 2FA via OTP is disabled" do
it "allows logging in with the U2F device" do
login_with(user)
@u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
expect(page.body).to match('Signed in successfully')
end
end
describe "when 2FA via OTP is enabled" do
it "allows logging in with the U2F device" do
user.update_attributes(otp_required_for_login: true)
login_with(user)
@u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
expect(page.body).to match('Signed in successfully')
end
end
describe "when a given U2F device has already been registered by another user" do
describe "but not the current user" do
it "does not allow logging in with that particular device" do
# Register current user with the different U2F device
current_user = login_as(:user)
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
register_u2f_device
logout
# Try authenticating user with the old U2F device
login_as(current_user)
@u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
expect(page.body).to match('Authentication via U2F device failed')
end
end
describe "and also the current user" do
it "allows logging in with that particular device" do
# Register current user with the same U2F device
current_user = login_as(:user)
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
register_u2f_device(@u2f_device)
logout
# Try authenticating user with the same U2F device
login_as(current_user)
@u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
expect(page.body).to match('Signed in successfully')
end
end
end
describe "when a given U2F device has not been registered" do
it "does not allow logging in with that particular device" do
unregistered_device = FakeU2fDevice.new(page)
login_as(user)
unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
expect(page.body).to match('Authentication via U2F device failed')
end
end
end
describe "when two-factor authentication is disabled" do
let(:user) { create(:user) }
before do
login_as(user)
visit profile_account_path
click_on 'Enable Two-Factor Authentication'
register_u2f_device
end
it "deletes u2f registrations" do
expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0)
end
end
end
#= require awards_handler
#= require jquery
#= require jquery.cookie
#= require ./fixtures/emoji_menu
awardsHandler = null
window.gl or= {}
gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' }
gl.awardMenuUrl = '/emojis'
lazyAssert = (done, assertFn) ->
setTimeout -> # Maybe jasmine.clock here?
assertFn()
done()
, 333
describe 'AwardsHandler', ->
fixture.preload 'awards_handler.html'
beforeEach ->
fixture.load 'awards_handler.html'
awardsHandler = new AwardsHandler
spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb()
spyOn(jQuery, 'get').and.callFake (req, cb) ->
expect(req).toBe '/emojis'
cb window.emojiMenu
describe '::showEmojiMenu', ->
it 'should show emoji menu when Add emoji button clicked', (done) ->
$('.js-add-award').eq(0).click()
lazyAssert done, ->
$emojiMenu = $ '.emoji-menu'
expect($emojiMenu.length).toBe 1
expect($emojiMenu.hasClass('is-visible')).toBe yes
expect($emojiMenu.find('#emoji_search').length).toBe 1
expect($('.js-awards-block.current').length).toBe 1
it 'should also show emoji menu for the smiley icon in notes', (done) ->
$('.note-action-button').click()
lazyAssert done, ->
$emojiMenu = $ '.emoji-menu'
expect($emojiMenu.length).toBe 1
it 'should remove emoji menu when body is clicked', (done) ->
$('.js-add-award').eq(0).click()
lazyAssert done, ->
$emojiMenu = $('.emoji-menu')
$('body').click()
expect($emojiMenu.length).toBe 1
expect($emojiMenu.hasClass('is-visible')).toBe no
expect($('.js-awards-block.current').length).toBe 0
describe '::addAwardToEmojiBar', ->
it 'should add emoji to votes block', ->
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
$emojiButton = $votesBlock.find '[data-emoji=heart]'
expect($emojiButton.length).toBe 1
expect($emojiButton.next('.js-counter').text()).toBe '1'
expect($votesBlock.hasClass('hidden')).toBe no
it 'should remove the emoji when we click again', ->
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
$emojiButton = $votesBlock.find '[data-emoji=heart]'
expect($emojiButton.length).toBe 0
it 'should decrement the emoji counter', ->
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
$emojiButton = $votesBlock.find '[data-emoji=heart]'
$emojiButton.next('.js-counter').text 5
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
expect($emojiButton.length).toBe 1
expect($emojiButton.next('.js-counter').text()).toBe '4'
describe '::getAwardUrl', ->
it 'should return the url for request', ->
expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'
describe '::addAward and ::checkMutuality', ->
it 'should handle :+1: and :-1: mutuality', ->
awardUrl = awardsHandler.getAwardUrl()
$votesBlock = $('.js-awards-block').eq 0
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent()
$thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent()
awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no
expect($thumbsUpEmoji.hasClass('active')).toBe yes
expect($thumbsDownEmoji.hasClass('active')).toBe no
$thumbsUpEmoji.tooltip()
$thumbsDownEmoji.tooltip()
awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes
expect($thumbsUpEmoji.hasClass('active')).toBe no
expect($thumbsDownEmoji.hasClass('active')).toBe yes
describe '::removeEmoji', ->
it 'should remove emoji', ->
awardUrl = awardsHandler.getAwardUrl()
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAward $votesBlock, awardUrl, 'fire', no
expect($votesBlock.find('[data-emoji=fire]').length).toBe 1
awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button')
expect($votesBlock.find('[data-emoji=fire]').length).toBe 0
describe 'search', ->
it 'should filter the emoji', ->
$('.js-add-award').eq(0).click()
expect($('[data-emoji=angel]').is(':visible')).toBe yes
expect($('[data-emoji=anger]').is(':visible')).toBe yes
$('#emoji_search').val('ali').trigger 'keyup'
expect($('[data-emoji=angel]').is(':visible')).toBe no
expect($('[data-emoji=anger]').is(':visible')).toBe no
expect($('[data-emoji=alien]').is(':visible')).toBe yes
expect($('h5.emoji-search').is(':visible')).toBe yes
describe 'emoji menu', ->
selector = '[data-emoji=sunglasses]'
openEmojiMenuAndAddEmoji = ->
$('.js-add-award').eq(0).click()
$menu = $ '.emoji-menu'
$block = $ '.js-awards-block'
$emoji = $menu.find ".emoji-menu-list-item #{selector}"
expect($emoji.length).toBe 1
expect($block.find(selector).length).toBe 0
$emoji.click()
expect($menu.hasClass('.is-visible')).toBe no
expect($block.find(selector).length).toBe 1
it 'should add selected emoji to awards block', ->
openEmojiMenuAndAddEmoji()
it 'should remove already selected emoji', ->
openEmojiMenuAndAddEmoji()
$('.js-add-award').eq(0).click()
$block = $ '.js-awards-block'
$emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}"
$emoji.click()
expect($block.find(selector).length).toBe 0
...@@ -14,17 +14,17 @@ describe 'Quick Submit behavior', -> ...@@ -14,17 +14,17 @@ describe 'Quick Submit behavior', ->
} }
it 'does not respond to other keyCodes', -> it 'does not respond to other keyCodes', ->
$('input').trigger(keydownEvent(keyCode: 32)) $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to Enter alone', -> it 'does not respond to Enter alone', ->
$('input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to repeated events', -> it 'does not respond to repeated events', ->
$('input').trigger(keydownEvent(repeat: true)) $('input.quick-submit-input').trigger(keydownEvent(repeat: true))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
...@@ -38,26 +38,26 @@ describe 'Quick Submit behavior', -> ...@@ -38,26 +38,26 @@ describe 'Quick Submit behavior', ->
# only run the tests that apply to the current platform # only run the tests that apply to the current platform
if navigator.userAgent.match(/Macintosh/) if navigator.userAgent.match(/Macintosh/)
it 'responds to Meta+Enter', -> it 'responds to Meta+Enter', ->
$('input').trigger(keydownEvent()) $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered() expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', -> it 'excludes other modifier keys', ->
$('input').trigger(keydownEvent(altKey: true)) $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
$('input').trigger(keydownEvent(ctrlKey: true)) $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true))
$('input').trigger(keydownEvent(shiftKey: true)) $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
else else
it 'responds to Ctrl+Enter', -> it 'responds to Ctrl+Enter', ->
$('input').trigger(keydownEvent()) $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered() expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', -> it 'excludes other modifier keys', ->
$('input').trigger(keydownEvent(altKey: true)) $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
$('input').trigger(keydownEvent(metaKey: true)) $('input.quick-submit-input').trigger(keydownEvent(metaKey: true))
$('input').trigger(keydownEvent(shiftKey: true)) $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
......
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
.description.js-task-list-container.is-task-list-enabled
.wiki
%p Qui exercitationem magnam optio quae fuga earum odio.
%textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
%small.edited-text
.content-block.content-block-small
.awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
%button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
.icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
%span.award-control-text.js-counter 0
%button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
.icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
%span.award-control-text.js-counter 0
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{:type => "button"}
%i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
%i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
%span.award-control-text Add
%section.issuable-discussion
#notes
%ul#notes-list.notes.main-notes-list.timeline
%li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
.timeline-entry-inner
.timeline-icon
%a{:href => "/u/agustin"}
%img.avatar.s40{:alt => "", :src => "#"}/
.timeline-content
.note-header
%a.author_link{:href => "/u/agustin"}
%span.author Brenna Stokes
.inline.note-headline-light
@agustin commented
%a{:href => "#note_348"}
%time 11 days ago
.note-actions
%span.note-role Reporter
%a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
%i.fa.fa-spinner.fa-spin
%i.fa.fa-smile-o
.js-task-list-container.note-body.is-task-list-enabled
.note-text
%p Suscipit sunt quia quisquam sed eveniet ipsam.
.note-awards
.awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{:type => "button"}
%i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
%i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
%span.award-control-text Add
%form.js-quick-submit{ action: '/foo' } %form.js-quick-submit{ action: '/foo' }
%input{ type: 'text' } %input{ type: 'text', class: 'quick-submit-input'}
%textarea %textarea
%input{ type: 'submit'} Submit %input{ type: 'submit'} Submit
......
This diff is collapsed.
= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" }
= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' }
#= require jquery-ui #= require jquery-ui/autocomplete
#= require new_branch_form #= require new_branch_form
describe 'Branch', -> describe 'Branch', ->
......
#= require u2f/authenticate
#= require u2f/util
#= require u2f/error
#= require u2f
#= require ./mock_u2f_device
describe 'U2FAuthenticate', ->
U2FUtil.enableTestMode()
fixture.load('u2f/authenticate')
beforeEach ->
@u2fDevice = new MockU2FDevice
@container = $("#js-authenticate-u2f")
@component = new U2FAuthenticate(@container, {}, "token")
@component.start()
it 'allows authenticating via a U2F device', ->
setupButton = @container.find("#js-login-u2f-device")
setupMessage = @container.find("p")
expect(setupMessage.text()).toContain('Insert your security key')
expect(setupButton.text()).toBe('Login Via U2F Device')
setupButton.trigger('click')
inProgressMessage = @container.find("p")
expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
@u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
authenticatedMessage = @container.find("p")
deviceResponse = @container.find('#js-device-response')
expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
describe "errors", ->
it "displays an error message", ->
setupButton = @container.find("#js-login-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
errorMessage = @container.find("p")
expect(errorMessage.text()).toContain("There was a problem communicating with your device")
it "allows retrying authentication after an error", ->
setupButton = @container.find("#js-login-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
retryButton = @container.find("#js-u2f-try-again")
retryButton.trigger('click')
setupButton = @container.find("#js-login-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
authenticatedMessage = @container.find("p")
expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
class @MockU2FDevice
constructor: () ->
window.u2f ||= {}
window.u2f.register = (appId, registerRequests, signRequests, callback) =>
@registerCallback = callback
window.u2f.sign = (appId, challenges, signRequests, callback) =>
@authenticateCallback = callback
respondToRegisterRequest: (params) =>
@registerCallback(params)
respondToAuthenticateRequest: (params) =>
@authenticateCallback(params)
#= require u2f/register
#= require u2f/util
#= require u2f/error
#= require u2f
#= require ./mock_u2f_device
describe 'U2FRegister', ->
U2FUtil.enableTestMode()
fixture.load('u2f/register')
beforeEach ->
@u2fDevice = new MockU2FDevice
@container = $("#js-register-u2f")
@component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token")
@component.start()
it 'allows registering a U2F device', ->
setupButton = @container.find("#js-setup-u2f-device")
expect(setupButton.text()).toBe('Setup New U2F Device')
setupButton.trigger('click')
inProgressMessage = @container.children("p")
expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
@u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
registeredMessage = @container.find('p')
deviceResponse = @container.find('#js-device-response')
expect(registeredMessage.text()).toContain("Your device was successfully set up!")
expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
describe "errors", ->
it "doesn't allow the same device to be registered twice (for the same user", ->
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({errorCode: 4})
errorMessage = @container.find("p")
expect(errorMessage.text()).toContain("already been registered with us")
it "displays an error message for other errors", ->
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({errorCode: "error!"})
errorMessage = @container.find("p")
expect(errorMessage.text()).toContain("There was a problem communicating with your device")
it "allows retrying registration after an error", ->
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({errorCode: "error!"})
retryButton = @container.find("#U2FTryAgain")
retryButton.trigger('click')
setupButton = @container.find("#js-setup-u2f-device")
setupButton.trigger('click')
@u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
registeredMessage = @container.find("p")
expect(registeredMessage.text()).toContain("Your device was successfully set up!")
...@@ -42,9 +42,7 @@ describe Gitlab::Badge::Build do ...@@ -42,9 +42,7 @@ describe Gitlab::Badge::Build do
end end
context 'build exists' do context 'build exists' do
let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } let!(:build) { create_build(project, sha, branch) }
let!(:build) { create(:ci_build, commit: ci_commit) }
context 'build success' do context 'build success' do
before { build.success! } before { build.success! }
...@@ -96,6 +94,28 @@ describe Gitlab::Badge::Build do ...@@ -96,6 +94,28 @@ describe Gitlab::Badge::Build do
end end
end end
context 'when outdated pipeline for given ref exists' do
before do
build = create_build(project, sha, branch)
build.success!
old_build = create_build(project, '11eeffdd', branch)
old_build.drop!
end
it 'does not take outdated pipeline into account' do
expect(badge.to_s).to eq 'build-success'
end
end
def create_build(project, sha, branch)
ci_commit = create(:ci_commit, project: project,
sha: sha,
ref: branch)
create(:ci_build, commit: ci_commit)
end
def status_node(data, status) def status_node(data, status)
xml = Nokogiri::XML.parse(data) xml = Nokogiri::XML.parse(data)
xml.at(%Q{text:contains("#{status}")}) xml.at(%Q{text:contains("#{status}")})
......
...@@ -38,12 +38,11 @@ describe Issue, "Awardable" do ...@@ -38,12 +38,11 @@ describe Issue, "Awardable" do
describe "#toggle_award_emoji" do describe "#toggle_award_emoji" do
it "adds an emoji if it isn't awarded yet" do it "adds an emoji if it isn't awarded yet" do
expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by 1 expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
end end
it "toggles already awarded emoji" do it "toggles already awarded emoji" do
expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by -1
end end
end end
end end
...@@ -9,6 +9,16 @@ describe Note, models: true do ...@@ -9,6 +9,16 @@ describe Note, models: true do
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
end end
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Mentionable) }
it { is_expected.to include_module(Awardable) }
it { is_expected.to include_module(Gitlab::CurrentSettings) }
end
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:note) } it { is_expected.to validate_presence_of(:note) }
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
......
...@@ -134,6 +134,66 @@ describe User, models: true do ...@@ -134,6 +134,66 @@ describe User, models: true do
end end
end end
describe "scopes" do
describe ".with_two_factor" do
it "returns users with 2fa enabled via OTP" do
user_with_2fa = create(:user, :two_factor_via_otp)
user_without_2fa = create(:user)
users_with_two_factor = User.with_two_factor.pluck(:id)
expect(users_with_two_factor).to include(user_with_2fa.id)
expect(users_with_two_factor).not_to include(user_without_2fa.id)
end
it "returns users with 2fa enabled via U2F" do
user_with_2fa = create(:user, :two_factor_via_u2f)
user_without_2fa = create(:user)
users_with_two_factor = User.with_two_factor.pluck(:id)
expect(users_with_two_factor).to include(user_with_2fa.id)
expect(users_with_two_factor).not_to include(user_without_2fa.id)
end
it "returns users with 2fa enabled via OTP and U2F" do
user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
user_without_2fa = create(:user)
users_with_two_factor = User.with_two_factor.pluck(:id)
expect(users_with_two_factor).to eq([user_with_2fa.id])
expect(users_with_two_factor).not_to include(user_without_2fa.id)
end
end
describe ".without_two_factor" do
it "excludes users with 2fa enabled via OTP" do
user_with_2fa = create(:user, :two_factor_via_otp)
user_without_2fa = create(:user)
users_without_two_factor = User.without_two_factor.pluck(:id)
expect(users_without_two_factor).to include(user_without_2fa.id)
expect(users_without_two_factor).not_to include(user_with_2fa.id)
end
it "excludes users with 2fa enabled via U2F" do
user_with_2fa = create(:user, :two_factor_via_u2f)
user_without_2fa = create(:user)
users_without_two_factor = User.without_two_factor.pluck(:id)
expect(users_without_two_factor).to include(user_without_2fa.id)
expect(users_without_two_factor).not_to include(user_with_2fa.id)
end
it "excludes users with 2fa enabled via OTP and U2F" do
user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
user_without_2fa = create(:user)
users_without_two_factor = User.without_two_factor.pluck(:id)
expect(users_without_two_factor).to include(user_without_2fa.id)
expect(users_without_two_factor).not_to include(user_with_2fa.id)
end
end
end
describe "Respond to" do describe "Respond to" do
it { is_expected.to respond_to(:is_admin?) } it { is_expected.to respond_to(:is_admin?) }
it { is_expected.to respond_to(:name) } it { is_expected.to respond_to(:name) }
......
class FakeU2fDevice
def initialize(page)
@page = page
end
def respond_to_u2f_registration
app_id = @page.evaluate_script('gon.u2f.app_id')
challenges = @page.evaluate_script('gon.u2f.challenges')
json_response = u2f_device(app_id).register_response(challenges[0])
@page.execute_script("
u2f.register = function(appId, registerRequests, signRequests, callback) {
callback(#{json_response});
};
")
end
def respond_to_u2f_authentication
app_id = @page.evaluate_script('gon.u2f.app_id')
challenges = @page.evaluate_script('gon.u2f.challenges')
json_response = u2f_device(app_id).sign_response(challenges[0])
@page.execute_script("
u2f.sign = function(appId, challenges, signRequests, callback) {
callback(#{json_response});
};
")
end
private
def u2f_device(app_id)
@u2f_device ||= U2F::FakeU2F.new(app_id)
end
end
# The MIT License (MIT)
#
# Copyright (c) 2014 GitHub, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# TaskList Behavior
#
#= provides tasklist:enabled
#= provides tasklist:disabled
#= provides tasklist:change
#= provides tasklist:changed
#
#
# Enables Task List update behavior.
#
# ### Example Markup
#
# <div class="js-task-list-container">
# <ul class="task-list">
# <li class="task-list-item">
# <input type="checkbox" class="js-task-list-item-checkbox" disabled />
# text
# </li>
# </ul>
# <form>
# <textarea class="js-task-list-field">- [ ] text</textarea>
# </form>
# </div>
#
# ### Specification
#
# TaskLists MUST be contained in a `(div).js-task-list-container`.
#
# TaskList Items SHOULD be an a list (`UL`/`OL`) element.
#
# Task list items MUST match `(input).task-list-item-checkbox` and MUST be
# `disabled` by default.
#
# TaskLists MUST have a `(textarea).js-task-list-field` form element whose
# `value` attribute is the source (Markdown) to be udpated. The source MUST
# follow the syntax guidelines.
#
# TaskList updates trigger `tasklist:change` events. If the change is
# successful, `tasklist:changed` is fired. The change can be canceled.
#
# jQuery is required.
#
# ### Methods
#
# `.taskList('enable')` or `.taskList()`
#
# Enables TaskList updates for the container.
#
# `.taskList('disable')`
#
# Disables TaskList updates for the container.
#
## ### Events
#
# `tasklist:enabled`
#
# Fired when the TaskList is enabled.
#
# * **Synchronicity** Sync
# * **Bubbles** Yes
# * **Cancelable** No
# * **Target** `.js-task-list-container`
#
# `tasklist:disabled`
#
# Fired when the TaskList is disabled.
#
# * **Synchronicity** Sync
# * **Bubbles** Yes
# * **Cancelable** No
# * **Target** `.js-task-list-container`
#
# `tasklist:change`
#
# Fired before the TaskList item change takes affect.
#
# * **Synchronicity** Sync
# * **Bubbles** Yes
# * **Cancelable** Yes
# * **Target** `.js-task-list-field`
#
# `tasklist:changed`
#
# Fired once the TaskList item change has taken affect.
#
# * **Synchronicity** Sync
# * **Bubbles** Yes
# * **Cancelable** No
# * **Target** `.js-task-list-field`
#
# ### NOTE
#
# Task list checkboxes are rendered as disabled by default because rendered
# user content is cached without regard for the viewer.
incomplete = "[ ]"
complete = "[x]"
# Escapes the String for regular expression matching.
escapePattern = (str) ->
str.
replace(/([\[\]])/g, "\\$1"). # escape square brackets
replace(/\s/, "\\s"). # match all white space
replace("x", "[xX]") # match all cases
incompletePattern = ///
#{escapePattern(incomplete)}
///
completePattern = ///
#{escapePattern(complete)}
///
# Pattern used to identify all task list items.
# Useful when you need iterate over all items.
itemPattern = ///
^
(?: # prefix, consisting of
\s* # optional leading whitespace
(?:>\s*)* # zero or more blockquotes
(?:[-+*]|(?:\d+\.)) # list item indicator
)
\s* # optional whitespace prefix
( # checkbox
#{escapePattern(complete)}|
#{escapePattern(incomplete)}
)
\s+ # is followed by whitespace
(?!
\(.*?\) # is not part of a [foo](url) link
)
(?= # and is followed by zero or more links
(?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)*
(?:[^\[]|$) # and either a non-link or the end of the string
)
///
# Used to filter out code fences from the source for comparison only.
# http://rubular.com/r/x5EwZVrloI
# Modified slightly due to issues with JS
codeFencesPattern = ///
^`{3} # ```
(?:\s*\w+)? # followed by optional language
[\S\s] # whitespace
.* # code
[\S\s] # whitespace
^`{3}$ # ```
///mg
# Used to filter out potential mismatches (items not in lists).
# http://rubular.com/r/OInl6CiePy
itemsInParasPattern = ///
^
(
#{escapePattern(complete)}|
#{escapePattern(incomplete)}
)
.+
$
///g
# Given the source text, updates the appropriate task list item to match the
# given checked value.
#
# Returns the updated String text.
updateTaskListItem = (source, itemIndex, checked) ->
clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').
replace(itemsInParasPattern, '').split("\n")
index = 0
result = for line in source.split("\n")
if line in clean && line.match(itemPattern)
index += 1
if index == itemIndex
line =
if checked
line.replace(incompletePattern, complete)
else
line.replace(completePattern, incomplete)
line
result.join("\n")
# Updates the $field value to reflect the state of $item.
# Triggers the `tasklist:change` event before the value has changed, and fires
# a `tasklist:changed` event once the value has changed.
updateTaskList = ($item) ->
$container = $item.closest '.js-task-list-container'
$field = $container.find '.js-task-list-field'
index = 1 + $container.find('.task-list-item-checkbox').index($item)
checked = $item.prop 'checked'
event = $.Event 'tasklist:change'
$field.trigger event, [index, checked]
unless event.isDefaultPrevented()
$field.val updateTaskListItem($field.val(), index, checked)
$field.trigger 'change'
$field.trigger 'tasklist:changed', [index, checked]
# When the task list item checkbox is updated, submit the change
$(document).on 'change', '.task-list-item-checkbox', ->
updateTaskList $(this)
# Enables TaskList item changes.
enableTaskList = ($container) ->
if $container.find('.js-task-list-field').length > 0
$container.
find('.task-list-item').addClass('enabled').
find('.task-list-item-checkbox').attr('disabled', null)
$container.addClass('is-task-list-enabled').
trigger 'tasklist:enabled'
# Enables a collection of TaskList containers.
enableTaskLists = ($containers) ->
for container in $containers
enableTaskList $(container)
# Disable TaskList item changes.
disableTaskList = ($container) ->
$container.
find('.task-list-item').removeClass('enabled').
find('.task-list-item-checkbox').attr('disabled', 'disabled')
$container.removeClass('is-task-list-enabled').
trigger 'tasklist:disabled'
# Disables a collection of TaskList containers.
disableTaskLists = ($containers) ->
for container in $containers
disableTaskList $(container)
$.fn.taskList = (method) ->
$container = $(this).closest('.js-task-list-container')
methods =
enable: enableTaskLists
disable: disableTaskLists
methods[method || 'enable']($container)
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment