Commit 4dd03551 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge remote-tracking branch 'dev/master'

parents 307b0e6c 9ff20606
This diff is collapsed.
<script>
import { GlFormInput, GlFormGroup, GlButton, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
export const i18n = {
currentPassword: __('Current password'),
confirmWebAuthn: __(
'Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.',
),
confirm: __('Are you sure? This will invalidate your registered applications and U2F devices.'),
disableTwoFactor: __('Disable two-factor authentication'),
regenerateRecoveryCodes: __('Regenerate recovery codes'),
};
export default {
name: 'ManageTwoFactorForm',
i18n,
components: {
GlForm,
GlFormInput,
GlFormGroup,
GlButton,
},
inject: [
'webauthnEnabled',
'profileTwoFactorAuthPath',
'profileTwoFactorAuthMethod',
'codesProfileTwoFactorAuthPath',
'codesProfileTwoFactorAuthMethod',
],
data() {
return {
method: '',
action: '#',
};
},
computed: {
confirmText() {
if (this.webauthnEnabled) {
return i18n.confirmWebAuthn;
}
return i18n.confirm;
},
},
methods: {
handleFormSubmit(event) {
this.method = event.submitter.dataset.formMethod;
this.action = event.submitter.dataset.formAction;
},
},
csrf,
};
</script>
<template>
<gl-form
class="gl-display-inline-block"
method="post"
:action="action"
@submit="handleFormSubmit($event)"
>
<input type="hidden" name="_method" data-testid="test-2fa-method-field" :value="method" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<gl-form-group :label="$options.i18n.currentPassword" label-for="current-password">
<gl-form-input
id="current-password"
type="password"
name="current_password"
required
data-qa-selector="current_password_field"
/>
</gl-form-group>
<gl-button
type="submit"
class="btn-danger gl-mr-3 gl-display-inline-block"
data-testid="test-2fa-disable-button"
variant="danger"
:data-confirm="confirmText"
:data-form-action="profileTwoFactorAuthPath"
:data-form-method="profileTwoFactorAuthMethod"
>
{{ $options.i18n.disableTwoFactor }}
</gl-button>
<gl-button
type="submit"
class="gl-display-inline-block"
data-testid="test-2fa-regenerate-codes-button"
:data-form-action="codesProfileTwoFactorAuthPath"
:data-form-method="codesProfileTwoFactorAuthMethod"
>
{{ $options.i18n.regenerateRecoveryCodes }}
</gl-button>
</gl-form>
</template>
import Vue from 'vue';
import { updateHistory, removeParams } from '~/lib/utils/url_utility';
import ManageTwoFactorForm from './components/manage_two_factor_form.vue';
import RecoveryCodes from './components/recovery_codes.vue';
import { SUCCESS_QUERY_PARAM } from './constants';
export const initManageTwoFactorForm = () => {
const el = document.querySelector('.js-manage-two-factor-form');
if (!el) {
return false;
}
const {
webauthnEnabled = false,
profileTwoFactorAuthPath = '',
profileTwoFactorAuthMethod = '',
codesProfileTwoFactorAuthPath = '',
codesProfileTwoFactorAuthMethod = '',
} = el.dataset;
return new Vue({
el,
provide: {
webauthnEnabled,
profileTwoFactorAuthPath,
profileTwoFactorAuthMethod,
codesProfileTwoFactorAuthPath,
codesProfileTwoFactorAuthMethod,
},
render(createElement) {
return createElement(ManageTwoFactorForm);
},
});
};
export const initRecoveryCodes = () => {
const el = document.querySelector('.js-2fa-recovery-codes');
......
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
import { escape, sortBy, template } from 'lodash';
import { escape as lodashEscape, sortBy, template } from 'lodash';
import * as Emoji from '~/emoji';
import axios from '~/lib/utils/axios_utils';
import { s__, __, sprintf } from '~/locale';
......@@ -11,8 +11,21 @@ import { spriteIcon } from './lib/utils/common_utils';
import { parsePikadayDate } from './lib/utils/datetime_utility';
import glRegexp from './lib/utils/regexp';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
/**
* Escapes user input before we pass it to at.js, which
* renders it as HTML in the autocomplete dropdown.
*
* at.js allows you to reference data using `${}` syntax
* (e.g. ${search}) which it replaces with the actual data
* before rendering it in the autocomplete dropdown.
* To prevent user input from executing this `${}` syntax,
* we also need to escape the $ character.
*
* @param string user input
* @return {string} escaped user input
*/
function escape(string) {
return lodashEscape(string).replace(/\$/g, '&dollar;');
}
function createMemberSearchString(member) {
......@@ -44,8 +57,8 @@ export function membersBeforeSave(members) {
return {
username: member.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
search: sanitize(createMemberSearchString(member)),
title,
search: createMemberSearchString(member),
icon: avatarIcon,
availability: member?.availability,
};
......@@ -366,7 +379,7 @@ class GfmAutoComplete {
}
return {
id: i.iid,
title: sanitize(i.title),
title: i.title,
reference: i.reference,
search: `${i.iid} ${i.title}`,
};
......@@ -404,7 +417,7 @@ class GfmAutoComplete {
return {
id: m.iid,
title: sanitize(m.title),
title: m.title,
search: m.title,
expired,
dueDate,
......@@ -456,7 +469,7 @@ class GfmAutoComplete {
}
return {
id: m.iid,
title: sanitize(m.title),
title: m.title,
reference: m.reference,
search: `${m.iid} ${m.title}`,
};
......@@ -492,7 +505,7 @@ class GfmAutoComplete {
beforeSave(merges) {
if (GfmAutoComplete.isLoading(merges)) return merges;
return $.map(merges, (m) => ({
title: sanitize(m.title),
title: m.title,
color: m.color,
search: m.title,
set: m.set,
......@@ -586,7 +599,7 @@ class GfmAutoComplete {
}
return {
id: m.id,
title: sanitize(m.title),
title: m.title,
search: `${m.id} ${m.title}`,
};
});
......
import { mount2faRegistration } from '~/authentication/mount_2fa';
import { initRecoveryCodes } from '~/authentication/two_factor_auth';
import { initRecoveryCodes, initManageTwoFactorForm } from '~/authentication/two_factor_auth';
import { parseBoolean } from '~/lib/utils/common_utils';
const twoFactorNode = document.querySelector('.js-two-factor-auth');
......@@ -14,3 +14,5 @@ if (skippable) {
mount2faRegistration();
initRecoveryCodes();
initManageTwoFactorForm();
......@@ -842,7 +842,7 @@ UsersSelect.prototype.renderApprovalRules = function (elsClassName, approvalRule
const [rule] = approvalRules;
const countText = sprintf(__('(+%{count}&nbsp;rules)'), { count });
const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : '';
const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name;
const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : escape(rule.name);
return `<div class="gl-display-flex gl-font-sm">
<span class="gl-text-truncate" title="${ruleName}">${ruleName}</span>
......
......@@ -45,10 +45,11 @@ class Admin::UsersController < Admin::ApplicationController
end
def impersonate
if can?(user, :log_in)
if can?(user, :log_in) && !impersonation_in_progress?
session[:impersonator_id] = current_user.id
warden.set_user(user, scope: :user)
clear_access_token_session_keys!
log_impersonation_event
......@@ -57,7 +58,9 @@ class Admin::UsersController < Admin::ApplicationController
redirect_to root_path
else
flash[:alert] =
if user.blocked?
if impersonation_in_progress?
_("You are already impersonating another user")
elsif user.blocked?
_("You cannot impersonate a blocked user")
elsif user.internal?
_("You cannot impersonate an internal user")
......
......@@ -3,6 +3,12 @@
module Impersonation
include Gitlab::Utils::StrongMemoize
SESSION_KEYS_TO_DELETE = %w(
github_access_token gitea_access_token gitlab_access_token
bitbucket_token bitbucket_refresh_token bitbucket_server_personal_access_token
bulk_import_gitlab_access_token fogbugz_token
).freeze
def current_user
user = super
......@@ -14,7 +20,7 @@ module Impersonation
protected
def check_impersonation_availability
return unless session[:impersonator_id]
return unless impersonation_in_progress?
unless Gitlab.config.gitlab.impersonation_enabled
stop_impersonation
......@@ -27,14 +33,25 @@ module Impersonation
warden.set_user(impersonator, scope: :user)
session[:impersonator_id] = nil
clear_access_token_session_keys!
current_user
end
def impersonation_in_progress?
session[:impersonator_id].present?
end
def log_impersonation_event
Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{current_user.username}")
end
def clear_access_token_session_keys!
access_tokens_keys = session.keys & SESSION_KEYS_TO_DELETE
access_tokens_keys.each { |key| session.delete(key) }
end
def impersonator
strong_memoize(:impersonator) do
User.find(session[:impersonator_id]) if session[:impersonator_id]
......
......@@ -66,11 +66,13 @@ class Import::GiteaController < Import::GithubController
override :client_options
def client_options
{ host: provider_url, api_version: 'v1' }
verified_url, provider_hostname = verify_blocked_uri
{ host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s, api_version: 'v1', hostname: provider_hostname }
end
def verify_blocked_uri
Gitlab::UrlBlocker.validate!(
@verified_url_and_hostname ||= Gitlab::UrlBlocker.validate!(
provider_url,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
......
......@@ -47,6 +47,8 @@ class Profiles::PasswordsController < Profiles::ApplicationController
password_attributes[:password_automatically_set] = false
unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
handle_invalid_current_password_attempt!
redirect_to edit_profile_password_path, alert: _('You must provide a valid current password')
return
end
......@@ -85,6 +87,12 @@ class Profiles::PasswordsController < Profiles::ApplicationController
render_404 unless @user.allow_password_authentication?
end
def handle_invalid_current_password_attempt!
Gitlab::AppLogger.info(message: 'Invalid current password when attempting to update user password', username: @user.username, ip: request.remote_ip)
@user.increment_failed_attempts!
end
def user_params
params.require(:user).permit(:current_password, :password, :password_confirmation)
end
......
......@@ -3,6 +3,8 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_two_factor_requirement
before_action :ensure_verified_primary_email, only: [:show, :create]
before_action :validate_current_password, only: [:create, :codes, :destroy]
before_action do
push_frontend_feature_flag(:webauthn)
end
......@@ -134,6 +136,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
def validate_current_password
return if current_user.valid_password?(params[:current_password])
current_user.increment_failed_attempts!
redirect_to profile_two_factor_auth_path, alert: _('You must provide a valid current password')
end
def build_qr_code
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3)
......
......@@ -34,13 +34,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def import
@projects = current_user.authorized_projects.order_id_desc
@projects = Project.visible_to_user_and_access_level(current_user, Gitlab::Access::MAINTAINER).order_id_desc
end
def apply_import
source_project = Project.find(params[:source_project_id])
if can?(current_user, :read_project_member, source_project)
if can?(current_user, :admin_project_member, source_project)
status = @project.team.import(source_project, current_user)
notice = status ? "Successfully imported" : "Import failed"
else
......
......@@ -19,6 +19,7 @@ class ProjectsController < Projects::ApplicationController
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create, :resolve]
before_action :repository, except: [:index, :new, :create, :resolve]
before_action :verify_git_import_enabled, only: [:create]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
before_action :authorize_download_code!, only: [:refs]
......@@ -496,6 +497,10 @@ class ProjectsController < Projects::ApplicationController
url_for(safe_params)
end
def verify_git_import_enabled
render_404 if project_params[:import_url] && !git_import_enabled?
end
def project_export_enabled
render_404 unless Gitlab::CurrentSettings.project_export_enabled?
end
......
......@@ -36,14 +36,10 @@ class UploadsController < ApplicationController
end
def find_model
return unless params[:id]
upload_model_class.find(params[:id])
end
def authorize_access!
return unless model
authorized =
case model
when Note
......@@ -68,8 +64,6 @@ class UploadsController < ApplicationController
end
def authorize_create_access!
return unless model
authorized =
case model
when User
......
......@@ -3,7 +3,7 @@
module Types
class GroupInvitationType < BaseObject
expose_permissions Types::PermissionTypes::Group
authorize :read_group
authorize :admin_group
implements InvitationInterface
......
......@@ -9,7 +9,7 @@ module Types
implements InvitationInterface
authorize :read_project
authorize :admin_project
field :project, Types::ProjectType, null: true,
description: 'Project ID for the project of the invitation.'
......
# frozen_string_literal: true
module ExternalLinkHelper
include ActionView::Helpers::TextHelper
def external_link(body, url, options = {})
link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
link = link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
"#{body}#{sprite_icon('external-link', css_class: 'gl-ml-1')}".html_safe
end
sanitize(link, tags: %w(a svg use), attributes: %w(target rel data-testid class href).concat(options.stringify_keys.keys))
end
end
......@@ -44,7 +44,7 @@ module IconsHelper
content_tag(
:svg,
content_tag(:use, '', { 'xlink:href' => "#{sprite_icon_path}##{icon_name}" } ),
content_tag(:use, '', { 'href' => "#{sprite_icon_path}##{icon_name}" } ),
class: css_classes.empty? ? nil : css_classes.join(' '),
data: { testid: "#{icon_name}-icon" }
)
......
......@@ -1886,7 +1886,8 @@ class User < ApplicationRecord
def password_expired_if_applicable?
return false if bot?
return false unless password_expired? && password_automatically_set?
return false unless password_expired?
return false if password_automatically_set?
return false unless allow_password_authentication?
true
......
......@@ -117,6 +117,7 @@ module Projects
trash_relation_repositories!
trash_project_repositories!
destroy_web_hooks!
destroy_project_bots!
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
......@@ -149,6 +150,16 @@ module Projects
end
end
# The project can have multiple project bots with personal access tokens generated.
# We need to remove them when a project is deleted
# rubocop: disable CodeReuse/ActiveRecord
def destroy_project_bots!
project.members.includes(:user).references(:user).merge(User.project_bot).each do |member|
Users::DestroyService.new(current_user).execute(member.user, skip_authorization: true)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def remove_registry_tags
return true unless Gitlab.config.registry.enabled
return false unless remove_legacy_registry_tags
......
......@@ -17,13 +17,7 @@
= _("You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.")
%p
= _('If you lose your recovery codes you can generate new ones, invalidating all previous codes.')
%div
= link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
method: :delete,
data: { confirm: webauthn_enabled ? _('Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.') : _('Are you sure? This will invalidate your registered applications and U2F devices.') },
class: 'gl-button btn btn-danger gl-mr-3'
= form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
= submit_tag _('Regenerate recovery codes'), class: 'gl-button btn btn-default'
.js-manage-two-factor-form{ data: { webauthn_enabled: webauthn_enabled, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
- else
%p
......@@ -53,6 +47,11 @@
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
.form-group
= label_tag :current_password, _('Current password'), class: 'label-bold'
= password_field_tag :current_password, nil, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
%p.form-text.text-muted
= _('Your current password is required to register a two-factor authenticator app.')
.gl-mt-3
= submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' }
......
......@@ -16,6 +16,7 @@
%li= _('Job logs and artifacts')
%li= _('Container registry images')
%li= _('CI variables')
%li= _('Pipeline triggers')
%li= _('Webhooks')
%li= _('Any encrypted tokens')
- if project.export_status == :finished
......
......@@ -51,6 +51,11 @@ Doorkeeper.configure do
# Issue access tokens with refresh token (disabled by default)
use_refresh_token
# Forbids creating/updating applications with arbitrary scopes that are
# not in configuration, i.e. `default_scopes` or `optional_scopes`.
# (disabled by default)
enforce_configured_scopes
# Forces the usage of the HTTPS protocol in non-native redirect uris (enabled
# by default in non-development environments). OAuth2 delegates security in
# communication to the HTTPS protocol so it is wise to keep this enabled.
......
# frozen_string_literal: true
# See https://github.com/omniauth/omniauth-oauth2/blob/v1.7.1/lib/omniauth/strategies/oauth2.rb#L84-L101
# for the original version of this code.
#
# Note: We need to override `callback_phase` directly (instead of using a module with `include` or `prepend`),
# because the method has a `super` call which needs to go to the `OmniAuth::Strategy` module,
# and it also deletes `omniauth.state` from the session as a side effect.
module OmniAuth
module Strategies
class OAuth2
alias_method :original_callback_phase, :callback_phase
# Monkey patch until PR is merged and released upstream
# https://github.com/omniauth/omniauth-oauth2/pull/129
def callback_phase
original_callback_phase
error = request.params["error_reason"].presence || request.params["error"].presence
# Monkey patch #1:
#
# Swap the order of these conditions around so the `state` param is verified *first*,
# before using the error params returned by the provider.
#
# This avoids content spoofing attacks by crafting a URL with malicious messages,
# because the `state` param is only present in the session after a valid OAuth2 authentication flow.
if !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state"))
fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
elsif error
fail!(error, CallbackError.new(request.params["error"], request.params["error_description"].presence || request.params["error_reason"].presence, request.params["error_uri"]))
else
self.access_token = build_access_token
self.access_token = access_token.refresh! if access_token.expired?
super
end
rescue ::OAuth2::Error, CallbackError => e
fail!(:invalid_credentials, e)
rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
fail!(:timeout, e)
rescue ::SocketError => e
fail!(:failed_to_connect, e)
# Monkey patch #2:
#
# Also catch errors from Faraday.
# See https://github.com/omniauth/omniauth-oauth2/pull/129
# and https://github.com/oauth-xx/oauth2/issues/152
#
# This can be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/340933
rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed => e
fail!(:timeout, e)
end
......
# frozen_string_literal: true
class CleanupOrphanProjectAccessTokens < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
TMP_INDEX_NAME = 'idx_users_on_user_type_project_bots_batched'
def up
users_table = define_batchable_model('users')
add_concurrent_index(:users, :id, name: TMP_INDEX_NAME, where: 'user_type = 6')
accumulated_orphans = []
users_table.where(user_type: 6).each_batch(of: 500) do |relation|
orphan_ids = relation.where("not exists(select 1 from members where members.user_id = users.id)").pluck(:id)
orphan_ids.each_slice(10) do |ids|
users_table.where(id: ids).update_all(state: 'deactivated')
end
accumulated_orphans += orphan_ids
end
schedule_deletion(accumulated_orphans)
ensure
remove_concurrent_index_by_name(:users, TMP_INDEX_NAME)
end
def down
remove_concurrent_index_by_name(:users, TMP_INDEX_NAME) if index_exists_by_name?(:users, TMP_INDEX_NAME)
end
private
def schedule_deletion(orphan_ids)
return unless deletion_worker
orphan_ids.each_slice(100) do |ids|
job_arguments = ids.map do |orphan_id|
[orphan_id, orphan_id, { skip_authorization: true }]
end
deletion_worker.bulk_perform_async(job_arguments)
end
rescue StandardError
# Ignore any errors or interface changes since this part of migration is optional
end
def deletion_worker
@deletion_worker = "DeleteUserWorker".safe_constantize unless defined?(@deletion_worker)
@deletion_worker
end
end
6fcf3ff9867df68f5e9603ae0311b29bec33aa5c5b826786b094ab0960ebcd90
\ No newline at end of file
......@@ -11,6 +11,9 @@ This API is in an alpha stage and considered unstable.
The response payload may be subject to change or breakage
across GitLab releases.
> - Introduced in GitLab 12.1.
> - Pagination introduced in 14.4.
Every call to this endpoint requires authentication. To perform this call, user should be authorized to read repository.
To see vulnerabilities in response, user should be authorized to read
[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard).
......@@ -60,3 +63,10 @@ Example response:
}
]
```
## Dependencies pagination
By default, `GET` requests return 20 results at a time because the API results
are paginated.
Read more on [pagination](index.md#pagination).
......@@ -259,7 +259,7 @@ GET /users?with_custom_attributes=true
## Single user
Get a single user. This endpoint can be accessed without authentication.
Get a single user.
### For user
......@@ -806,7 +806,7 @@ Example response:
### Followers and following
Get the followers of a user. This endpoint can be accessed without authentication.
Get the followers of a user.
```plaintext
GET /users/:id/followers
......
......@@ -75,6 +75,7 @@ To enable 2FA:
1. **In GitLab:**
1. Enter the six-digit pin number from the entry on your device into the **Pin
code** field.
1. Enter your current password.
1. Select **Submit**.
If the pin you entered was correct, a message displays indicating that
......@@ -365,7 +366,8 @@ If you ever need to disable 2FA:
1. Sign in to your GitLab account.
1. Go to your [**User settings**](../index.md#access-your-user-settings).
1. Go to **Account**.
1. Click **Disable**, under **Two-Factor Authentication**.
1. Select **Manage two-factor authentication**.
1. Under **Two-Factor Authentication**, enter your current password and select **Disable**.
This clears all your two-factor authentication registrations, including mobile
applications and U2F / WebAuthn devices.
......@@ -460,7 +462,7 @@ To regenerate 2FA recovery codes, you need access to a desktop browser:
1. Go to your [**User settings**](../index.md#access-your-user-settings).
1. Select **Account > Two-Factor Authentication (2FA)**.
1. If you've already configured 2FA, click **Manage two-factor authentication**.
1. In the **Register Two-Factor Authenticator** pane, click **Regenerate recovery codes**.
1. In the **Register Two-Factor Authenticator** pane, enter your current password and select **Regenerate recovery codes**.
NOTE:
If you regenerate 2FA recovery codes, save them. You can't use any previously created 2FA codes.
......
......@@ -135,9 +135,11 @@ The following items are **not** exported:
- Build traces and artifacts
- Container registry images
- CI/CD variables
- Pipeline triggers
- Webhooks
- Any encrypted tokens
- Merge Request Approvers
- Repository size limits
These content rules also apply to creating projects from templates on the
[group](../../group/custom_project_templates.md)
......@@ -261,7 +263,7 @@ reduce the repository size for another import attempt.
git gc --prune=now --aggressive
# Prepare recreating an importable file
git bundle create ../project.bundle smaller-tmp-main
git bundle create ../project.bundle <default-branch-name>
cd ..
mv project/ ../"$EXPORT"-project
cd ..
......
......@@ -48,7 +48,7 @@ class GfmAutoCompleteEE extends GfmAutoComplete {
return {
id: m.iid,
reference: m.reference,
title: m.title.replace(/<(?:.|\n)*?>/gm, ''),
title: m.title,
search: `${m.iid} ${m.title}`,
};
});
......@@ -82,7 +82,7 @@ class GfmAutoCompleteEE extends GfmAutoComplete {
}
return {
id: m.id,
title: m.title.replace(/<(?:.|\n)*?>/gm, ''),
title: m.title,
reference: m.reference,
search: `${m.id} ${m.title}`,
};
......
......@@ -8,9 +8,7 @@ module Autocomplete
# projects.
#
# params - A Hash containing additional parameters to set.
#
# The supported parameters are those supported by
# `Autocomplete::ProjectFinder`.
# The supported parameters are those supported by `Autocomplete::ProjectFinder`.
def initialize(current_user, params = {})
@current_user = current_user
@params = params
......@@ -22,8 +20,21 @@ module Autocomplete
.new(current_user, params)
.execute
project ? project.invited_groups : Group.none
return Group.none unless project
invited_groups(project)
end
# rubocop: enable CodeReuse/Finder
private
def invited_groups(project)
invited_groups = project.invited_groups
Group.from_union([
invited_groups.public_to_user(current_user),
invited_groups.for_authorized_group_members(current_user)
])
end
end
end
......@@ -245,12 +245,12 @@ class EpicsFinder < IssuableFinder
# If we don't account for confidential (assume it will be filtered later by
# with_confidentiality_access_check) then as long as the user can see all
# epics in this group they can see in all subgroups. This is only true for
# private top level groups because it's possible that a top level public
# epics in this group they can see in all subgroups if member of parent group.
# This is only true for private top level groups because it's possible that a top level public
# group has private subgroups and therefore they would not necessarily be
# able to read epics in the private subgroup even though they can in the
# parent group.
!include_confidential && parent.private? && Ability.allowed?(current_user, :read_epic, parent)
!include_confidential && Ability.allowed?(current_user, :list_subgroup_epics, parent)
end
def by_confidential(items)
......
......@@ -31,6 +31,11 @@ class EpicIssue < ApplicationRecord
select(selection).in_epic(node.parent_ids)
end
# TODO add this method to validate records (see https://gitlab.com/gitlab-org/gitlab/-/issues/339514)
def epic_and_issue_at_same_group_hierarchy?
epic.group.self_and_hierarchy.include?(issue.project.group)
end
private
def validate_confidential_epic
......
......@@ -19,6 +19,7 @@ module MergeRequests
validates :external_url, presence: true, uniqueness: { scope: :project_id }, addressable_url: true
validates :name, uniqueness: { scope: :project_id }, presence: true
validate :protected_branches_must_belong_to_project
def async_execute(data)
return unless protected_branches.none? || protected_branches.by_name(data[:object_attributes][:target_branch]).any?
......@@ -43,5 +44,9 @@ module MergeRequests
def payload_data(merge_request_hook_data)
merge_request_hook_data.merge(external_approval_rule: self.to_h)
end
def protected_branches_must_belong_to_project
errors.add(:base, 'all protected branches must exist within the project') unless protected_branches.all? { |b| project.protected_branches.include?(b) }
end
end
end
......@@ -145,6 +145,10 @@ module EE
rule { guest }.policy do
enable :read_wiki
enable :read_group_release_stats
# Only used on specific scenario to filter out subgroup epics not visible
# to user when showing parent group epics list
enable :list_subgroup_epics
end
rule { reporter }.policy do
......
......@@ -10,7 +10,7 @@ module EE
def associations_to_preload
return super unless epics_available?
super << :epic
super.concat([:epic, :epic_issue])
end
override :header_to_value_hash
......@@ -18,11 +18,22 @@ module EE
return super unless epics_available?
super.merge({
'Epic ID' => -> (issue) { issue.epic&.id },
'Epic Title' => -> (issue) { issue.epic&.title }
'Epic ID' => epic_issue_safe(:id),
'Epic Title' => epic_issue_safe(:title)
})
end
def epic_issue_safe(attribute)
lambda do |issue|
epic = issue.epic
next if epic.nil?
next unless issue.epic_issue.epic_and_issue_at_same_group_hierarchy? # TODO This check can be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/339514
epic[attribute]
end
end
def epics_available?
strong_memoize(:epics_available) do
project.group&.feature_available?(:epics)
......
......@@ -18,6 +18,7 @@ module EE
def rewrite_epic_issue
return unless epic_issue = original_entity.epic_issue
return unless can?(current_user, :update_epic, epic_issue.epic.group)
return unless epic_and_issue_at_same_group_hierarchy?(new_entity, epic_issue.epic)
updated = epic_issue.update(issue_id: new_entity.id)
......@@ -26,6 +27,12 @@ module EE
original_entity.reset
end
def epic_and_issue_at_same_group_hierarchy?(new_issue, epic)
temp_epic_issue = EpicIssue.new(issue_id: new_issue.id, epic_id: epic.id)
temp_epic_issue.epic_and_issue_at_same_group_hierarchy?
end
def track_epic_issue_moved_from_project
return unless original_entity.epic_issue
......
......@@ -2,6 +2,8 @@
module API
class Dependencies < ::API::Base
include PaginationParams
feature_category :dependency_scanning
helpers do
......@@ -31,6 +33,7 @@ module API
coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
desc: "Returns dependencies belonging to specified package managers: #{::Security::DependencyListService::FILTER_PACKAGE_MANAGERS_VALUES.join(', ')}.",
values: ::Security::DependencyListService::FILTER_PACKAGE_MANAGERS_VALUES
use :pagination
end
get ':id/dependencies' do
......@@ -39,7 +42,7 @@ module API
::Gitlab::Tracking.event(self.options[:for].name, 'view_dependencies', project: user_project, user: current_user, namespace: user_project.namespace)
dependency_params = declared_params(include_missing: false).merge(project: user_project)
dependencies = dependencies_by(dependency_params)
dependencies = paginate(::Gitlab::ItemsCollection.new(dependencies_by(dependency_params)))
present dependencies, with: ::EE::API::Entities::Dependency, user: current_user, project: user_project
end
......
......@@ -37,8 +37,14 @@ module EE
expose :compliance_frameworks do |project, _|
[project.compliance_framework_setting&.compliance_management_framework&.name].compact
end
expose :issues_template, if: ->(project, _) { project.feature_available?(:issuable_default_templates) }
expose :merge_requests_template, if: ->(project, _) { project.feature_available?(:issuable_default_templates) }
expose :issues_template, if: ->(project, options) do
project.feature_available?(:issuable_default_templates) &&
Ability.allowed?(options[:current_user], :read_issue, project)
end
expose :merge_requests_template, if: ->(project, options) do
project.feature_available?(:issuable_default_templates) &&
Ability.allowed?(options[:current_user], :read_merge_request, project)
end
expose :merge_pipelines_enabled?, as: :merge_pipelines_enabled, if: ->(project, _) { project.feature_available?(:merge_pipelines) }
expose :merge_trains_enabled?, as: :merge_trains_enabled, if: ->(project, _) { project.feature_available?(:merge_pipelines) }
end
......
......@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe AutocompleteController do
let(:project) { create(:project) }
let(:user) { project.owner }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
context 'GET users' do
let!(:user2) { create(:user) }
let!(:non_member) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:non_member) { create(:user) }
context 'project members' do
before do
......@@ -58,12 +58,14 @@ RSpec.describe AutocompleteController do
end
context 'groups' do
let(:matching_group) { create(:group) }
let(:non_matching_group) { create(:group) }
let(:user2) { create(:user) }
before do
project.invited_groups << matching_group
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:authorized_private_group) { create(:group, :private) }
let_it_be(:unauthorized_private_group) { create(:group, :private) }
let_it_be(:non_invited_group) { create(:group, :public) }
before_all do
authorized_private_group.add_guest(user)
project.invited_groups = [public_group, authorized_private_group, unauthorized_private_group]
end
context "while fetching all groups belonging to a project" do
......@@ -72,14 +74,17 @@ RSpec.describe AutocompleteController do
get(:project_groups, params: { project_id: project.id })
end
it 'returns a single group', :aggregate_failures do
expect(json_response).to be_kind_of(Array)
expect(json_response.size).to eq(1)
expect(json_response.first.values_at('id', 'name')).to eq [matching_group.id, matching_group.name]
it 'returns groups invited to the project that the user can see' do
expect(json_response).to contain_exactly(
a_hash_including("id" => authorized_private_group.id),
a_hash_including("id" => public_group.id)
)
end
end
context "while fetching all groups belonging to a project the current user cannot access" do
let(:user2) { create(:user) }
before do
sign_in(user2)
get(:project_groups, params: { project_id: project.id })
......@@ -91,7 +96,7 @@ RSpec.describe AutocompleteController do
context "while fetching all groups belonging to an invalid project ID" do
before do
sign_in(user)
get(:project_groups, params: { project_id: 'invalid' })
get(:project_groups, params: { project_id: non_existing_record_id })
end
it { expect(response).to be_not_found }
......@@ -119,7 +124,7 @@ RSpec.describe AutocompleteController do
end
context 'as admin' do
let(:user) { create(:admin) }
let_it_be(:user) { create(:admin) }
describe "while searching for a project by namespace" do
let(:search) { group.path }
......@@ -171,7 +176,7 @@ RSpec.describe AutocompleteController do
end
context 'as admin' do
let(:user) { create(:admin) }
let_it_be(:user) { create(:admin) }
describe "while searching for a namespace by group path" do
let(:search) { 'group' }
......
......@@ -29,6 +29,7 @@ RSpec.describe 'Account recovery regular check callout' do
visit profile_two_factor_auth_path
fill_in 'pin_code', with: user_two_factor_disabled.reload.current_otp
fill_in 'current_password', with: user_two_factor_disabled.password
click_button 'Register with two-factor app'
......
......@@ -3,24 +3,43 @@
require 'spec_helper'
RSpec.describe Autocomplete::ProjectInvitedGroupsFinder do
let(:user) { create(:user) }
describe '#execute' do
context 'without a project ID' do
it 'returns an empty relation' do
expect(described_class.new(user).execute).to be_empty
end
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:authorized_private_group) { create(:group, :private) }
let_it_be(:unauthorized_private_group) { create(:group, :private) }
let_it_be(:non_invited_group) { create(:group, :public) }
before_all do
authorized_private_group.add_guest(user)
project.invited_groups = [authorized_private_group, unauthorized_private_group, public_group]
end
it 'raises ActiveRecord::RecordNotFound if the project does not exist' do
finder = described_class.new(user, project_id: non_existing_record_id)
expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
context 'with a project ID' do
it 'returns the groups invited to the project' do
project = create(:project, :public)
group = create(:group)
it 'raises ActiveRecord::RecordNotFound if the user is not authorized to see the project' do
finder = described_class.new(user, project_id: project.id)
create(:project_group_link, project: project, group: group)
expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'returns an empty relation without a project ID' do
expect(described_class.new(user).execute).to be_empty
end
context 'with a project the user is authorized to see' do
before_all do
project.add_guest(user)
end
it 'returns groups invited to the project that the user can see' do
expect(described_class.new(user, project_id: project.id).execute)
.to eq([group])
.to contain_exactly(authorized_private_group, public_group)
end
end
end
......
......@@ -207,6 +207,26 @@ RSpec.describe EpicsFinder do
is_expected.to contain_exactly(subgroup_epic, subgroup2_epic)
end
context 'when user is a member of a subgroup project' do
let_it_be(:subgroup3) { create(:group, :private, parent: group) }
let_it_be(:subgroup3_epic) { create(:epic, group: subgroup3) }
let_it_be(:subgroup3_project) { create(:project, group: subgroup3) }
let_it_be(:project_member) { create(:user) }
let(:finder) { described_class.new(project_member, finder_params) }
let(:finder_params) { { include_descendant_groups: true, include_ancestor_groups: true, group_id: group.id } }
before do
subgroup3_project.add_reporter(project_member)
end
subject { finder.execute }
it 'gets only epics from the project ancestor groups' do
is_expected.to contain_exactly(epic1, epic2, epic3, subgroup3_epic)
end
end
context 'when include_descendant_groups is false' do
context 'and include_ancestor_groups is false' do
let(:finder_params) { { include_descendant_groups: false, include_ancestor_groups: false } }
......
......@@ -815,6 +815,61 @@ RSpec.describe User do
end
end
describe '#password_expired_if_applicable?' do
let(:user) { build(:user, password_expires_at: password_expires_at) }
subject { user.password_expired_if_applicable? }
shared_examples 'password expired not applicable' do
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when password_expires_at is in the past' do
let(:password_expires_at) { 1.minute.ago }
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when password_expires_at is in the future' do
let(:password_expires_at) { 1.minute.from_now }
it 'returns false' do
is_expected.to be_falsey
end
end
end
context 'when password_automatically_set is true' do
context 'with a SCIM identity' do
let_it_be(:scim_identity) { create(:scim_identity, active: true) }
let_it_be(:user) { scim_identity.user }
it_behaves_like 'password expired not applicable'
end
context 'with a SAML identity' do
let_it_be(:saml_identity) { create(:group_saml_identity) }
let_it_be(:user) { saml_identity.user }
it_behaves_like 'password expired not applicable'
end
context 'with a smartcard identity' do
let_it_be(:smartcard_identity) { create(:smartcard_identity) }
let_it_be(:user) { smartcard_identity.user }
it_behaves_like 'password expired not applicable'
end
end
end
describe '#user_authorized_by_provisioning_group?' do
context 'when user is provisioned by group' do
let(:group) { build(:group) }
......
......@@ -77,4 +77,31 @@ RSpec.describe EpicIssue do
end
end
end
describe '#epic_and_issue_at_same_group_hierarchy?' do
let_it_be(:group) { create(:group) }
let_it_be(:group_a) { create(:group, parent: group) }
let_it_be(:group_b) { create(:group, parent: group) }
let_it_be(:group_a_project_1) { create(:project, group: group) }
let(:epic) { build(:epic, group: group_a) }
subject { described_class.new(epic: epic, issue: issue).epic_and_issue_at_same_group_hierarchy? }
context 'when epic and issue are at same group hierarchy' do
let_it_be(:group_a_project_2) { create(:project, group: group_a) }
let(:issue) { build(:issue, project: group_a_project_2) }
it { is_expected.to eq(true) }
end
context 'when epic and issue are at different group hierarchies' do
let_it_be(:group_b_project_1) { create(:project, group: group_b) }
let(:issue) { build(:issue, project: group_b_project_1) }
it { is_expected.to eq(false) }
end
end
end
......@@ -14,6 +14,15 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
it { is_expected.to validate_presence_of(:external_url) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
describe 'protected_branches_must_belong_to_project' do
let(:check) { build(:external_status_check, protected_branches: [create(:protected_branch)]) }
it 'is invalid' do
expect(check).to be_invalid
expect(check.errors.messages[:base]).to eq ['all protected branches must exist within the project']
end
end
end
describe 'to_h' do
......@@ -26,8 +35,8 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:check_belonging_to_different_project) { create(:external_status_check) }
let_it_be(:check_with_no_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: []) }
let_it_be(:check_with_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: merge_request.target_branch)]) }
let_it_be(:check_with_non_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'testbranch')]) }
let_it_be(:check_with_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: merge_request.target_branch, project: merge_request.project)]) }
let_it_be(:check_with_non_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'testbranch', project: merge_request.project)]) }
it 'returns the correct collection of checks' do
expect(merge_request.project.external_status_checks.applicable_to_branch(merge_request.target_branch)).to contain_exactly(check_with_no_protected_branches, check_with_applicable_protected_branches)
......@@ -58,7 +67,7 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
end
context 'when data target branch matches a protected branch' do
let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'test')]) }
let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'test', project: merge_request.project)]) }
it 'enqueues the status check' do
expect(ApprovalRules::ExternalApprovalRulePayloadWorker).to receive(:perform_async).once
......@@ -68,7 +77,7 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
end
context 'when data target branch does not match a protected branch' do
let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'new-branch')]) }
let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'new-branch', project: merge_request.project)]) }
it 'does not enqueue the status check' do
expect(ApprovalRules::ExternalApprovalRulePayloadWorker).to receive(:perform_async).never
......
......@@ -69,7 +69,7 @@ RSpec.describe GroupPolicy do
context 'when user is guest' do
let(:current_user) { guest }
it { is_expected.to be_allowed(:read_epic, :read_epic_board) }
it { is_expected.to be_allowed(:read_epic, :read_epic_board, :list_subgroup_epics) }
it { is_expected.to be_disallowed(*(epic_rules - [:read_epic, :read_epic_board, :read_epic_board_list])) }
end
......
......@@ -211,7 +211,7 @@ RSpec.describe MergeRequestPresenter do
context 'without applicable branches' do
before do
create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'testbranch')])
create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'testbranch', project: project)])
end
it { is_expected.to eq(nil) }
......@@ -227,7 +227,7 @@ RSpec.describe MergeRequestPresenter do
context 'with applicable branches' do
before do
create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch)])
create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch, project: project)])
end
it { is_expected.to eq(exposed_path) }
......
......@@ -28,11 +28,12 @@ RSpec.describe API::Dependencies do
request
end
it 'returns all dependencies' do
it 'returns paginated dependencies' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/dependencies', dir: 'ee')
expect(response).to include_pagination_headers
expect(json_response.length).to eq(21)
expect(json_response.length).to eq(20)
end
it 'returns vulnerabilities info' do
......@@ -71,6 +72,17 @@ RSpec.describe API::Dependencies do
end
end
end
context 'with pagination params' do
let(:params) { { per_page: 5, page: 5 } }
it 'returns paginated dependencies' do
expect(response).to match_response_schema('public_api/v4/dependencies', dir: 'ee')
expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
end
end
end
context 'without permissions to see vulnerabilities' do
......
......@@ -276,31 +276,59 @@ RSpec.describe API::Projects do
end
end
context 'issuable default templates feature is available' do
before do
stub_licensed_features(issuable_default_templates: true)
end
context 'issuable default templates' do
let(:project) { create(:project, :public) }
it 'returns issuable default templates' do
subject
context 'when feature is available' do
before do
stub_licensed_features(issuable_default_templates: true)
end
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key 'issues_template'
expect(json_response).to have_key 'merge_requests_template'
end
end
it 'returns issuable default templates' do
subject
context 'issuable default templates feature not available' do
before do
stub_licensed_features(issuable_default_templates: false)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key 'issues_template'
expect(json_response).to have_key 'merge_requests_template'
end
context 'when user does not have permission to see issues' do
let(:project) { create(:project, :public, :issues_private) }
it 'does not return issue default templates' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to have_key 'issues_template'
expect(json_response).to have_key 'merge_requests_template'
end
end
context 'when user does not have permission to see merge requests' do
let(:project) { create(:project, :public, :merge_requests_private) }
it 'does not return merge request default templates' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key 'issues_template'
expect(json_response).not_to have_key 'merge_requests_template'
end
end
end
it 'does not return issuable default templates' do
subject
context 'issuable default templates feature not available' do
before do
stub_licensed_features(issuable_default_templates: false)
end
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to have_key 'issues_template'
expect(json_response).not_to have_key 'merge_requests_template'
it 'does not return issuable default templates' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to have_key 'issues_template'
expect(json_response).not_to have_key 'merge_requests_template'
end
end
end
......
......@@ -39,8 +39,8 @@ RSpec.describe API::StatusChecks do
end
context 'when merge request has received status check responses' do
let!(:non_applicable_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'different-branch')]) }
let!(:branch_specific_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch)]) }
let!(:non_applicable_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'different-branch', project: project)]) }
let!(:branch_specific_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch, project: project)]) }
let!(:status_check_response) { create(:status_check_response, external_status_check: rule, merge_request: merge_request, sha: sha) }
it 'returns a 200' do
......@@ -288,6 +288,24 @@ RSpec.describe API::StatusChecks do
expect(response).to have_gitlab_http_status(:success)
end
context 'when referencing a protected branch outside of the project' do
let_it_be(:protected_branch) { create(:protected_branch) }
let(:params) do
{ name: 'New rule', external_url: 'https://gitlab.com/test/example.json', protected_branch_ids: protected_branch.id }
end
subject do
put api(single_object_url, project.owner), params: params
end
it 'is invalid' do
subject
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
context 'with protected branches' do
let_it_be(:protected_branch) { create(:protected_branch, project: project) }
......
......@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe Issues::MoveService do
let(:user) { create(:user) }
let(:old_project) { create(:project) }
let(:new_project) { create(:project, group: create(:group)) }
let(:group) { create(:group) }
let(:old_project) { create(:project, group: group) }
let(:new_project) { create(:project, group: group) }
let(:old_issue) { create(:issue, project: old_project, author: user) }
let(:move_service) { described_class.new(project: old_project, current_user: user) }
......@@ -88,7 +89,8 @@ RSpec.describe Issues::MoveService do
describe '#rewrite_epic_issue' do
context 'issue assigned to epic' do
let!(:epic_issue) { create(:epic_issue, issue: old_issue) }
let(:epic) { create(:epic, group: group) }
let!(:epic_issue) { create(:epic_issue, issue: old_issue, epic: epic) }
before do
stub_licensed_features(epics: true)
......@@ -124,6 +126,19 @@ RSpec.describe Issues::MoveService do
end
end
context 'when epic is not in the same group hierarchy' do
let(:new_group) { create(:group) }
let(:new_project) { create(:project, group: new_group) }
it 'does not rewrite epic' do
new_group.add_reporter(user)
epic_issue.epic.group.add_reporter(user)
new_issue = move_service.execute(old_issue, new_project)
expect(new_issue.epic_issue).to be_nil
end
end
context 'epic update fails' do
it 'does not send usage data for changed epic action' do
allow_next_instance_of(::EpicIssue) do |epic_issue|
......
......@@ -43,6 +43,20 @@ RSpec.describe Issues::ExportCsvService do
expect(csv[0]['Epic Title']).to eq(epic.title)
expect(csv[1]['Epic Title']).to be_nil
end
context 'when epic and issue are not from same group hierarchy' do
let(:epic) { create(:epic) }
specify 'epic ID' do
expect(csv[0]['Epic ID']).to be_nil
expect(csv[1]['Epic ID']).to be_nil
end
specify 'epic Title' do
expect(csv[0]['Epic Title']).to be_nil
expect(csv[1]['Epic Title']).to be_nil
end
end
end
end
end
......
......@@ -22,7 +22,7 @@ RSpec.describe 'profiles/personal_access_tokens/_token_expiry_notification.html.
it 'contains the correct content', :aggregate_failures do
expect(rendered).to have_selector '[data-feature-id="profile_personal_access_token_expiry"]'
expect(rendered).to match /<use xlink:href=".+?icons-.+?#error">/
expect(rendered).to match /<use href=".+?icons-.+?#error">/
expect(rendered).to have_content '2 tokens have expired'
expect(rendered).to have_content 'Until revoked, expired personal access tokens pose a security risk.'
end
......
......@@ -46,7 +46,7 @@ RSpec.describe('shared/credentials_inventory/_expiry_date.html.haml') do
end
it 'has an icon' do
expect(rendered).to match(/<use xlink:href=".+?icons-.+?#warning">/)
expect(rendered).to match(/<use href=".+?icons-.+?#warning">/)
end
end
......@@ -59,7 +59,7 @@ RSpec.describe('shared/credentials_inventory/_expiry_date.html.haml') do
end
it 'has an icon' do
expect(rendered).to match(/<use xlink:href=".+?icons-.+?#error">/)
expect(rendered).to match(/<use href=".+?icons-.+?#error">/)
end
end
end
......
......@@ -4,6 +4,10 @@ module API
class ImportBitbucketServer < ::API::Base
feature_category :importers
before do
forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('bitbucket_server')
end
helpers do
def client
@client ||= BitbucketServer::Client.new(credentials)
......
......@@ -46,6 +46,8 @@ module API
source = find_source(source_type, params[:id])
query = params[:query]
authorize_admin_source!(source_type, source)
invitations = paginate(retrieve_member_invitations(source, query))
present_member_invitations invitations
......
......@@ -89,6 +89,10 @@ module API
Gitlab::AppLogger.info({ message: "File exceeds maximum size", file_bytes: file.size, project_id: user_project.id, project_path: user_project.full_path, upload_allowed: allowed })
end
end
def check_import_by_url_is_enabled
forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('git')
end
end
helpers do
......@@ -267,6 +271,7 @@ module API
attrs = declared_params(include_missing: false)
attrs = translate_params_for_compatibility(attrs)
filter_attributes_using_license!(attrs)
check_import_by_url_is_enabled if params[:import_url].present?
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
......
......@@ -140,7 +140,10 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id", feature_category: :users do
forbidden!('Not authorized!') unless current_user
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
opts = { with: current_user&.admin? ? Entities::UserDetailsWithAdmin : Entities::User, current_user: current_user }
......@@ -156,6 +159,7 @@ module API
end
get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users do
user = find_user(params[:user_id])
not_found!('User') unless user && can?(current_user, :read_user, user)
present user.status || {}, with: Entities::UserStatus
......@@ -203,6 +207,8 @@ module API
use :pagination
end
get ':id/following', feature_category: :users do
forbidden!('Not authorized!') unless current_user
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
......@@ -217,6 +223,8 @@ module API
use :pagination
end
get ':id/followers', feature_category: :users do
forbidden!('Not authorized!') unless current_user
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
......
......@@ -26,14 +26,17 @@ module Banzai
# Pattern to match a standard markdown link
#
# Rubular: http://rubular.com/r/2EXEQ49rg5
LINK_OR_IMAGE_PATTERN = %r{
(?<preview_operator>!)?
\[(?<text>.+?)\]
\(
(?<new_link>.+?)
(?<title>\ ".+?")?
\)
}x.freeze
#
# This pattern is vulnerable to malicious inputs, so use Gitlab::UntrustedRegexp
# to place bounds on execution time
LINK_OR_IMAGE_PATTERN = Gitlab::UntrustedRegexp.new(
'(?P<preview_operator>!)?' \
'\[(?P<text>.+?)\]' \
'\(' \
'(?P<new_link>.+?)' \
'(?P<title>\ ".+?")?' \
'\)'
)
# Text matching LINK_OR_IMAGE_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
......@@ -48,7 +51,7 @@ module Banzai
doc.xpath(TEXT_QUERY).each do |node|
content = node.to_html
next unless content.match(LINK_OR_IMAGE_PATTERN)
next unless LINK_OR_IMAGE_PATTERN.match(content)
html = spaced_link_filter(content)
......
......@@ -172,7 +172,11 @@ module Gitlab
user = find_with_user_password(login, password)
return unless user
raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
verifier = TwoFactorAuthVerifier.new(user)
if user.two_factor_enabled? || verifier.two_factor_authentication_enforced?
raise Gitlab::Auth::MissingPersonalAccessTokenError
end
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
......
......@@ -342,6 +342,10 @@ module Gitlab
Gitlab::PathRegex.repository_git_lfs_route_regex.match?(current_request.path)
end
def git_or_lfs_request?
git_request? || git_lfs_request?
end
def archive_request?
current_request.path.include?('/-/archive/')
end
......
......@@ -35,13 +35,31 @@ module Gitlab
find_user_from_static_object_token(request_format) ||
find_user_from_basic_auth_job ||
find_user_from_job_token ||
find_user_from_lfs_token ||
find_user_from_personal_access_token ||
find_user_from_basic_auth_password
find_user_from_personal_access_token_for_api_or_git ||
find_user_for_git_or_lfs_request
rescue Gitlab::Auth::AuthenticationError
nil
end
# To prevent Rack Attack from incorrectly rate limiting
# authenticated Git activity, we need to authenticate the user
# from other means (e.g. HTTP Basic Authentication) only if the
# request originated from a Git or Git LFS
# request. Repositories::GitHttpClientController or
# Repositories::LfsApiController normally does the authentication,
# but Rack Attack runs before those controllers.
def find_user_for_git_or_lfs_request
return unless git_or_lfs_request?
find_user_from_lfs_token || find_user_from_basic_auth_password
end
def find_user_from_personal_access_token_for_api_or_git
return unless api_request? || git_or_lfs_request?
find_user_from_personal_access_token
end
def valid_access_token?(scopes: [])
validate_access_token!(scopes: scopes)
......
......@@ -9,6 +9,10 @@ module Gitlab
@current_user = current_user
end
def two_factor_authentication_enforced?
two_factor_authentication_required? && two_factor_grace_period_expired?
end
def two_factor_authentication_required?
Gitlab::CurrentSettings.require_two_factor_authentication? ||
current_user&.require_two_factor_authentication_from_group?
......
# frozen_string_literal: true
require 'fogbugz'
module Gitlab
module FogbugzImport
# Custom adapter to validate the URL before each request
# This way we avoid DNS rebinds or other unsafe requests
::Fogbugz.adapter[:http] = HttpAdapter
end
end
# frozen_string_literal: true
require 'fogbugz'
module Gitlab
module FogbugzImport
class Client
......
# frozen_string_literal: true
module Gitlab
module FogbugzImport
class HttpAdapter
def initialize(options = {})
@root_url = options[:uri]
end
def request(action, options = {})
uri = Gitlab::Utils.append_path(@root_url, 'api.asp')
params = { 'cmd' => action }.merge(options.fetch(:params, {}))
response = Gitlab::HTTP.post(uri, body: params)
response.body
end
end
end
end
......@@ -37,6 +37,7 @@ excluded_attributes:
- :trial_ends_on
- :shared_runners_minute_limit
- :extra_shared_runners_minutes_limit
- :repository_size_limit
epics:
- :state_id
......
......@@ -88,7 +88,6 @@ tree:
- :external_pull_request
- :merge_request
- :auto_devops
- :triggers
- :pipeline_schedules
- :container_expiration_policy
- protected_branches:
......@@ -211,6 +210,7 @@ excluded_attributes:
- :show_default_award_emojis
- :services
- :exported_protected_branches
- :repository_size_limit
namespaces:
- :runners_token
- :runners_token_encrypted
......
......@@ -8,9 +8,10 @@ module Gitlab
attr_reader :access_token, :host, :api_version, :wait_for_rate_limit_reset
def initialize(access_token, host: nil, api_version: 'v3', wait_for_rate_limit_reset: true)
def initialize(access_token, host: nil, api_version: 'v3', wait_for_rate_limit_reset: true, hostname: nil)
@access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '')
@hostname = hostname
@api_version = api_version
@users = {}
@wait_for_rate_limit_reset = wait_for_rate_limit_reset
......@@ -28,7 +29,8 @@ module Gitlab
# If there is no config, we're connecting to github.com and we
# should verify ssl.
connection_options: {
ssl: { verify: config ? config['verify_ssl'] : true }
ssl: { verify: config ? config['verify_ssl'] : true },
headers: { host: @hostname }.compact
}
)
end
......
......@@ -2,18 +2,20 @@
module Gitlab
class StringRegexMarker < StringRangeMarker
# rubocop: disable CodeReuse/ActiveRecord
def mark(regex, group: 0, &block)
ranges = []
offset = 0
raw_line.scan(regex) do
begin_index, end_index = Regexp.last_match.offset(group)
while match = regex.match(raw_line[offset..])
begin_index = match.begin(group) + offset
end_index = match.end(group) + offset
ranges << (begin_index..(end_index - 1))
offset = end_index
end
super(ranges, &block)
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
......@@ -38575,6 +38575,9 @@ msgstr ""
msgid "You are already a member of this %{member_source}."
msgstr ""
msgid "You are already impersonating another user"
msgstr ""
msgid "You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
......@@ -39289,6 +39292,9 @@ msgstr ""
msgid "Your commit email is used for web based operations, such as edits and merges."
msgstr ""
msgid "Your current password is required to register a two-factor authenticator app."
msgstr ""
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr ""
......
......@@ -92,6 +92,14 @@ RSpec.describe Admin::ImpersonationsController do
expect(warden.user).to eq(impersonator)
end
it 'clears token session keys' do
session[:bitbucket_token] = SecureRandom.hex(8)
delete :destroy
expect(session[:bitbucket_token]).to be_nil
end
end
# base case
......
......@@ -794,6 +794,14 @@ RSpec.describe Admin::UsersController do
expect(flash[:alert]).to eq("You are now impersonating #{user.username}")
end
it 'clears token session keys' do
session[:github_access_token] = SecureRandom.hex(8)
post :impersonate, params: { id: user.username }
expect(session[:github_access_token]).to be_nil
end
end
context "when impersonation is disabled" do
......@@ -807,5 +815,20 @@ RSpec.describe Admin::UsersController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when impersonating an admin and attempting to impersonate again' do
let(:admin2) { create(:admin) }
before do
post :impersonate, params: { id: admin2.username }
end
it 'does not allow double impersonation', :aggregate_failures do
post :impersonate, params: { id: user.username }
expect(flash[:alert]).to eq(_('You are already impersonating another user'))
expect(warden.user).to eq(admin2)
end
end
end
end
......@@ -54,6 +54,48 @@ RSpec.describe Import::GiteaController do
end
end
end
context 'when DNS Rebinding protection is enabled' do
let(:token) { 'gitea token' }
let(:ip_uri) { 'http://167.99.148.217' }
let(:uri) { 'try.gitea.io' }
let(:https_uri) { "https://#{uri}" }
let(:http_uri) { "http://#{uri}" }
before do
session[:gitea_access_token] = token
allow(Gitlab::UrlBlocker).to receive(:validate!).with(https_uri, anything).and_return([Addressable::URI.parse(https_uri), uri])
allow(Gitlab::UrlBlocker).to receive(:validate!).with(http_uri, anything).and_return([Addressable::URI.parse(ip_uri), uri])
allow(Gitlab::LegacyGithubImport::Client).to receive(:new).and_return(double('Gitlab::LegacyGithubImport::Client', repos: [], orgs: []))
end
context 'when provided host url is using https' do
let(:host_url) { https_uri }
it 'uses unchanged host url to send request to Gitea' do
expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: https_uri, api_version: 'v1', hostname: 'try.gitea.io')
get :status, format: :json
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when provided host url is using http' do
let(:host_url) { http_uri }
it 'uses changed host url to send request to Gitea' do
expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: 'http://167.99.148.217', api_version: 'v1', hostname: 'try.gitea.io')
get :status, format: :json
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
end
......
......@@ -98,6 +98,19 @@ RSpec.describe Oauth::ApplicationsController do
end
describe 'POST #create' do
let(:oauth_params) do
{
doorkeeper_application: {
name: 'foo',
redirect_uri: redirect_uri,
scopes: scopes
}
}
end
let(:redirect_uri) { 'http://example.org' }
let(:scopes) { ['api'] }
subject { post :create, params: oauth_params }
it 'creates an application' do
......@@ -116,38 +129,42 @@ RSpec.describe Oauth::ApplicationsController do
expect(response).to redirect_to(profile_path)
end
context 'redirect_uri' do
context 'when redirect_uri is invalid' do
let(:redirect_uri) { 'javascript://alert()' }
render_views
it 'shows an error for a forbidden URI' do
invalid_uri_params = {
doorkeeper_application: {
name: 'foo',
redirect_uri: 'javascript://alert()',
scopes: ['api']
}
}
post :create, params: invalid_uri_params
subject
expect(response.body).to include 'Redirect URI is forbidden by the server'
expect(response).to render_template('doorkeeper/applications/index')
end
end
context 'when scopes are not present' do
let(:scopes) { [] }
render_views
it 'shows an error for blank scopes' do
invalid_uri_params = {
doorkeeper_application: {
name: 'foo',
redirect_uri: 'http://example.org'
}
}
post :create, params: invalid_uri_params
subject
expect(response.body).to include 'Scopes can&#39;t be blank'
expect(response).to render_template('doorkeeper/applications/index')
end
end
context 'when scopes are invalid' do
let(:scopes) { %w(api foo) }
render_views
it 'shows an error for invalid scopes' do
subject
expect(response.body).to include 'Scopes doesn&#39;t match configured on the server.'
expect(response).to render_template('doorkeeper/applications/index')
end
end
......@@ -185,14 +202,4 @@ RSpec.describe Oauth::ApplicationsController do
def disable_user_oauth
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
end
def oauth_params
{
doorkeeper_application: {
name: 'foo',
redirect_uri: 'http://example.org',
scopes: ['api']
}
}
end
end
......@@ -35,6 +35,27 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
end
shared_examples 'user must enter a valid current password' do
let(:current_password) { '123' }
it 'requires the current password', :aggregate_failures do
go
expect(response).to redirect_to(profile_two_factor_auth_path)
expect(flash[:alert]).to eq(_('You must provide a valid current password'))
end
context 'when the user is on the last sign in attempt' do
it do
user.update!(failed_attempts: User.maximum_attempts.pred)
go
expect(user.reload).to be_access_locked
end
end
end
describe 'GET show' do
let_it_be_with_reload(:user) { create(:user) }
......@@ -69,9 +90,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do
let_it_be_with_reload(:user) { create(:user) }
let(:pin) { 'pin-code' }
let(:current_password) { user.password }
def go
post :create, params: { pin_code: pin }
post :create, params: { pin_code: pin, current_password: current_password }
end
context 'with valid pin' do
......@@ -136,21 +158,25 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
end
it_behaves_like 'user must enter a valid current password'
it_behaves_like 'user must first verify their primary email address'
end
describe 'POST codes' do
let_it_be_with_reload(:user) { create(:user, :two_factor) }
let(:current_password) { user.password }
it 'presents plaintext codes for the user to save' do
expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c))
post :codes
post :codes, params: { current_password: current_password }
expect(assigns[:codes]).to match_array %w(a b c)
end
it 'persists the generated codes' do
post :codes
post :codes, params: { current_password: current_password }
user.reload
expect(user.otp_backup_codes).not_to be_empty
......@@ -159,12 +185,18 @@ RSpec.describe Profiles::TwoFactorAuthsController do
it 'dismisses the `TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK` callout' do
expect(controller.helpers).to receive(:dismiss_two_factor_auth_recovery_settings_check)
post :codes
post :codes, params: { current_password: current_password }
end
it_behaves_like 'user must enter a valid current password' do
let(:go) { post :codes, params: { current_password: current_password } }
end
end
describe 'DELETE destroy' do
subject { delete :destroy }
subject { delete :destroy, params: { current_password: current_password } }
let(:current_password) { user.password }
context 'for a user that has 2FA enabled' do
let_it_be_with_reload(:user) { create(:user, :two_factor) }
......@@ -187,6 +219,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do
expect(flash[:notice])
.to eq _('Two-factor authentication has been disabled successfully!')
end
it_behaves_like 'user must enter a valid current password' do
let(:go) { delete :destroy, params: { current_password: current_password } }
end
end
context 'for a user that does not have 2FA enabled' do
......
......@@ -624,9 +624,9 @@ RSpec.describe Projects::ProjectMembersController do
end
end
context 'when user can access source project members' do
context 'when user can admin source project members' do
before do
another_project.add_guest(user)
another_project.add_maintainer(user)
end
include_context 'import applied'
......@@ -640,7 +640,11 @@ RSpec.describe Projects::ProjectMembersController do
end
end
context 'when user is not member of a source project' do
context "when user can't admin source project members" do
before do
another_project.add_developer(user)
end
include_context 'import applied'
it 'does not import team members' do
......
......@@ -419,6 +419,47 @@ RSpec.describe ProjectsController do
end
end
describe 'POST create' do
let!(:params) do
{
path: 'foo',
description: 'bar',
import_url: project.http_url_to_repo,
namespace_id: user.namespace.id
}
end
subject { post :create, params: { project: params } }
before do
sign_in(user)
end
context 'when import by url is disabled' do
before do
stub_application_setting(import_sources: [])
end
it 'does not create project and reports an error' do
expect { subject }.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when import by url is enabled' do
before do
stub_application_setting(import_sources: ['git'])
end
it 'creates project' do
expect { subject }.to change { Project.count }
expect(response).to have_gitlab_http_status(:redirect)
end
end
end
describe 'GET edit' do
it 'allows an admin user to access the page', :enable_admin_mode do
sign_in(create(:user, :admin))
......
......@@ -666,6 +666,6 @@ RSpec.describe UploadsController do
def post_authorize(verified: true)
request.headers.merge!(workhorse_internal_api_request_header) if verified
post :authorize, params: { model: 'personal_snippet', id: model.id }, format: :json
post :authorize, params: params, format: :json
end
end
......@@ -78,40 +78,80 @@ RSpec.describe 'Profile > Password' do
end
end
context 'Change passowrd' do
context 'Change password' do
let(:new_password) { '22233344' }
before do
sign_in(user)
visit(edit_profile_password_path)
end
it 'does not change user passowrd without old one' do
page.within '.update-password' do
fill_passwords('22233344', '22233344')
shared_examples 'user enters an incorrect current password' do
subject do
page.within '.update-password' do
fill_in 'user_current_password', with: user_current_password
fill_passwords(new_password, new_password)
end
end
page.within '.flash-container' do
expect(page).to have_content 'You must provide a valid current password'
end
end
it 'handles the invalid password attempt, and prompts the user to try again', :aggregate_failures do
expect(Gitlab::AppLogger).to receive(:info)
.with(message: 'Invalid current password when attempting to update user password', username: user.username, ip: user.current_sign_in_ip)
subject
user.reload
it 'does not change password with invalid old password' do
page.within '.update-password' do
fill_in 'user_current_password', with: 'invalid'
fill_passwords('password', 'confirmation')
expect(user.failed_attempts).to eq(1)
expect(user.valid_password?(new_password)).to eq(false)
expect(current_path).to eq(edit_profile_password_path)
page.within '.flash-container' do
expect(page).to have_content('You must provide a valid current password')
end
end
page.within '.flash-container' do
expect(page).to have_content 'You must provide a valid current password'
it 'locks the user account when user passes the maximum attempts threshold', :aggregate_failures do
user.update!(failed_attempts: User.maximum_attempts.pred)
subject
expect(current_path).to eq(new_user_session_path)
page.within '.flash-container' do
expect(page).to have_content('Your account is locked.')
end
end
end
it 'changes user password' do
page.within '.update-password' do
fill_in "user_current_password", with: user.password
fill_passwords('22233344', '22233344')
context 'when current password is blank' do
let(:user_current_password) { nil }
it_behaves_like 'user enters an incorrect current password'
end
context 'when current password is incorrect' do
let(:user_current_password) {'invalid' }
it_behaves_like 'user enters an incorrect current password'
end
context 'when the password reset is successful' do
subject do
page.within '.update-password' do
fill_in "user_current_password", with: user.password
fill_passwords(new_password, new_password)
end
end
expect(current_path).to eq new_user_session_path
it 'changes the password, logs the user out and prompts them to sign in again', :aggregate_failures do
expect { subject }.to change { user.reload.valid_password?(new_password) }.to(true)
expect(current_path).to eq new_user_session_path
page.within '.flash-container' do
expect(page).to have_content('Password was successfully updated. Please sign in again.')
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Two factor auths' do
context 'when signed in' do
before do
allow(Gitlab).to receive(:com?) { true }
end
context 'when user has two-factor authentication disabled' do
let(:user) { create(:user ) }
before do
sign_in(user)
end
it 'requires the current password to set up two factor authentication', :js do
visit profile_two_factor_auth_path
register_2fa(user.reload.current_otp, '123')
expect(page).to have_content('You must provide a valid current password')
register_2fa(user.reload.current_otp, user.password)
expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.')
click_button 'Copy codes'
click_link 'Proceed'
expect(page).to have_content('Status: Enabled')
end
end
context 'when user has two-factor authentication enabled' do
let(:user) { create(:user, :two_factor) }
before do
sign_in(user)
end
it 'requires the current_password to disable two-factor authentication', :js do
visit profile_two_factor_auth_path
fill_in 'current_password', with: '123'
click_button 'Disable two-factor authentication'
page.accept_alert
expect(page).to have_content('You must provide a valid current password')
fill_in 'current_password', with: user.password
click_button 'Disable two-factor authentication'
page.accept_alert
expect(page).to have_content('Two-factor authentication has been disabled successfully!')
expect(page).to have_content('Enable two-factor authentication')
end
it 'requires the current_password to regernate recovery codes', :js do
visit profile_two_factor_auth_path
fill_in 'current_password', with: '123'
click_button 'Regenerate recovery codes'
expect(page).to have_content('You must provide a valid current password')
fill_in 'current_password', with: user.password
click_button 'Regenerate recovery codes'
expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.')
end
end
def register_2fa(pin, password)
fill_in 'pin_code', with: pin
fill_in 'current_password', with: password
click_button 'Register with two-factor app'
end
end
end
......@@ -807,6 +807,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
expect(current_path).to eq(profile_two_factor_auth_path)
fill_in 'pin_code', with: user.reload.current_otp
fill_in 'current_password', with: user.password
click_button 'Register with two-factor app'
click_button 'Copy codes'
......
......@@ -7579,23 +7579,6 @@
}
}
],
"triggers": [
{
"id": 123,
"token": "cdbfasdf44a5958c83654733449e585",
"project_id": 5,
"owner_id": 1,
"created_at": "2017-01-16T15:25:28.637Z",
"updated_at": "2017-01-16T15:25:28.637Z"
},
{
"id": 456,
"token": "33a66349b5ad01fc00174af87804e40",
"project_id": 5,
"created_at": "2017-01-16T15:25:29.637Z",
"updated_at": "2017-01-16T15:25:29.637Z"
}
],
"pipeline_schedules": [
{
"id": 1,
......
{"id":123,"token":"cdbfasdf44a5958c83654733449e585","project_id":5,"owner_id":1,"created_at":"2017-01-16T15:25:28.637Z","updated_at":"2017-01-16T15:25:28.637Z"}
{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"}
{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"}
\ No newline at end of file
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ManageTwoFactorForm Disable button renders the component correctly 1`] = `
VueWrapper {
"_emitted": Object {},
"_emittedByOrder": Array [],
"isFunctionalComponent": undefined,
}
`;
exports[`ManageTwoFactorForm Disable button renders the component correctly 2`] = `
<form
action="#"
class="gl-display-inline-block"
method="post"
>
<input
data-testid="test-2fa-method-field"
name="_method"
type="hidden"
/>
<input
name="authenticity_token"
type="hidden"
/>
<div
class="form-group gl-form-group"
id="__BVID__15"
role="group"
>
<label
class="d-block col-form-label"
for="current-password"
id="__BVID__15__BV_label_"
>
Current password
</label>
<div
class="bv-no-focus-ring"
>
<input
aria-required="true"
class="gl-form-input form-control"
data-qa-selector="current_password_field"
id="current-password"
name="current_password"
required="required"
type="password"
/>
<!---->
<!---->
<!---->
</div>
</div>
<button
class="btn btn-danger gl-mr-3 gl-display-inline-block btn-danger btn-md gl-button"
data-confirm="Are you sure? This will invalidate your registered applications and U2F devices."
data-form-action="2fa_auth_path"
data-form-method="2fa_auth_method"
data-testid="test-2fa-disable-button"
type="submit"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Disable two-factor authentication
</span>
</button>
<button
class="btn gl-display-inline-block btn-default btn-md gl-button"
data-form-action="2fa_codes_path"
data-form-method="2fa_codes_method"
data-testid="test-2fa-regenerate-codes-button"
type="submit"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Regenerate recovery codes
</span>
</button>
</form>
`;
import { within } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ManageTwoFactorForm, {
i18n,
} from '~/authentication/two_factor_auth/components/manage_two_factor_form.vue';
describe('ManageTwoFactorForm', () => {
let wrapper;
const createComponent = (options = {}) => {
wrapper = extendedWrapper(
mount(ManageTwoFactorForm, {
provide: {
webauthnEnabled: options?.webauthnEnabled || false,
profileTwoFactorAuthPath: '2fa_auth_path',
profileTwoFactorAuthMethod: '2fa_auth_method',
codesProfileTwoFactorAuthPath: '2fa_codes_path',
codesProfileTwoFactorAuthMethod: '2fa_codes_method',
},
}),
);
};
const queryByText = (text, options) => within(wrapper.element).queryByText(text, options);
const queryByLabelText = (text, options) =>
within(wrapper.element).queryByLabelText(text, options);
beforeEach(() => {
createComponent();
});
describe('Current password field', () => {
it('renders the current password field', () => {
expect(queryByLabelText(i18n.currentPassword).tagName).toEqual('INPUT');
});
});
describe('Disable button', () => {
it('renders the component correctly', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('has the right confirm text', () => {
expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual(
i18n.confirm,
);
});
describe('when webauthnEnabled', () => {
beforeEach(() => {
createComponent({
webauthnEnabled: true,
});
});
it('has the right confirm text', () => {
expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual(
i18n.confirmWebAuthn,
);
});
});
it('modifies the form action and method when submitted through the button', async () => {
const form = wrapper.find('form');
const disableButton = wrapper.findByTestId('test-2fa-disable-button').element;
const methodInput = wrapper.findByTestId('test-2fa-method-field').element;
form.trigger('submit', { submitter: disableButton });
await wrapper.vm.$nextTick();
expect(form.element.getAttribute('action')).toEqual('2fa_auth_path');
expect(methodInput.getAttribute('value')).toEqual('2fa_auth_method');
});
});
describe('Regenerate recovery codes button', () => {
it('renders the button', () => {
expect(queryByText(i18n.regenerateRecoveryCodes)).toEqual(expect.any(HTMLElement));
});
it('modifies the form action and method when submitted through the button', async () => {
const form = wrapper.find('form');
const regenerateCodesButton = wrapper.findByTestId('test-2fa-regenerate-codes-button')
.element;
const methodInput = wrapper.findByTestId('test-2fa-method-field').element;
form.trigger('submit', { submitter: regenerateCodesButton });
await wrapper.vm.$nextTick();
expect(form.element.getAttribute('action')).toEqual('2fa_codes_path');
expect(methodInput.getAttribute('value')).toEqual('2fa_codes_method');
});
});
});
......@@ -574,6 +574,15 @@ describe('GfmAutoComplete', () => {
}),
).toBe('<li><small>grp/proj#5</small> Some Issue</li>');
});
it('escapes title in the template as it is user input', () => {
expect(
GfmAutoComplete.Issues.templateFunction({
id: 5,
title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
}),
).toBe('<li><small>5</small> &dollar;{search}&lt;script&gt;oh no &dollar;</li>');
});
});
describe('GfmAutoComplete.Members', () => {
......@@ -608,16 +617,18 @@ describe('GfmAutoComplete', () => {
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
it('should add escaped title if title is set', () => {
it('escapes title in the template as it is user input', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: 'MyGroup+',
title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
).toBe(
'<li>IMG my-group <small>&dollar;{search}&lt;script&gt;oh no &dollar;</small> <i class="icon"/></li>',
);
});
it('should add user availability status if availabilityStatus is set', () => {
......@@ -782,6 +793,15 @@ describe('GfmAutoComplete', () => {
${'/unlabel ~'} | ${assignedLabels}
`('$input shows $output.length labels', expectLabels);
});
it('escapes title in the template as it is user input', () => {
const color = '#123456';
const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
expect(GfmAutoComplete.Labels.templateFunction(color, title)).toBe(
'<li><span class="dropdown-label-box" style="background: #123456"></span> &dollar;{search}&lt;script&gt;oh no &dollar;</li>',
);
});
});
describe('emoji', () => {
......@@ -829,4 +849,15 @@ describe('GfmAutoComplete', () => {
});
});
});
describe('milestones', () => {
it('escapes title in the template as it is user input', () => {
const expired = false;
const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
expect(GfmAutoComplete.Milestones.templateFunction(title, expired)).toBe(
'<li>&dollar;{search}&lt;script&gt;oh no &dollar;</li>',
);
});
});
});
import { escape } from 'lodash';
import UsersSelect from '~/users_select/index';
import {
createInputsModelExpectation,
createUnassignedExpectation,
......@@ -91,5 +93,19 @@ describe('~/users_select/index', () => {
expect(findDropdownItemsModel()).toEqual(expectation);
});
});
describe('renderApprovalRules', () => {
const ruleNames = ['simple-name', '"\'<>&', '"><script>alert(1)<script>'];
it.each(ruleNames)('escapes rule name correctly for %s', (name) => {
const escapedName = escape(name);
expect(
UsersSelect.prototype.renderApprovalRules('reviewer', [{ name }]),
).toMatchInterpolatedText(
`<div class="gl-display-flex gl-font-sm"> <span class="gl-text-truncate" title="${escapedName}">${escapedName}</span> </div>`,
);
});
});
});
});
......@@ -7,7 +7,7 @@ RSpec.describe Types::GroupInvitationType do
specify { expect(described_class.graphql_name).to eq('GroupInvitation') }
specify { expect(described_class).to require_graphql_authorizations(:read_group) }
specify { expect(described_class).to require_graphql_authorizations(:admin_group) }
it 'has the expected fields' do
expected_fields = %w[
......
......@@ -7,7 +7,7 @@ RSpec.describe Types::ProjectInvitationType do
specify { expect(described_class.graphql_name).to eq('ProjectInvitation') }
specify { expect(described_class).to require_graphql_authorizations(:read_project) }
specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
it 'has the expected fields' do
expected_fields = %w[
......
......@@ -13,8 +13,14 @@ RSpec.describe ExternalLinkHelper do
it 'allows options when creating external link with icon' do
link = external_link('https://gitlab.com', 'https://gitlab.com', { "data-foo": "bar", class: "externalLink" }).to_s
expect(link).to start_with('<a target="_blank" rel="noopener noreferrer" data-foo="bar" class="externalLink" href="https://gitlab.com">https://gitlab.com')
expect(link).to include('data-testid="external-link-icon"')
end
it 'sanitizes and returns external link with icon' do
link = external_link('sanitized link content', 'javascript:alert()').to_s
expect(link).not_to include('href="javascript:alert()"')
expect(link).to start_with('<a target="_blank" rel="noopener noreferrer">sanitized link content')
expect(link).to include('data-testid="external-link-icon"')
end
end
......@@ -35,22 +35,22 @@ RSpec.describe IconsHelper do
it 'returns svg icon html with DEFAULT_ICON_SIZE' do
expect(sprite_icon(icon_name).to_s)
.to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
.to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html without size class' do
expect(sprite_icon(icon_name, size: nil).to_s)
.to eq "<svg data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
.to eq "<svg data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes' do
expect(sprite_icon(icon_name, size: 72).to_s)
.to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
.to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes + additional class' do
expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
.to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
.to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
describe 'non existing icon' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'OmniAuth::Strategies::OAuth2', type: :strategy do
let(:strategy) { [OmniAuth::Strategies::OAuth2] }
it 'verifies the gem version' do
current_version = OmniAuth::OAuth2::VERSION
expected_version = '1.7.1'
expect(current_version).to eq(expected_version), <<~EOF
New version #{current_version} of the `omniauth-oauth2` gem detected!
Please check if the monkey patches in `config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb`
are still needed, and either update/remove them, or bump the version in this spec.
EOF
end
context 'when a custom error message is passed from an OAuth2 provider' do
let(:message) { 'Please go to https://evil.com' }
let(:state) { 'secret' }
let(:callback_path) { '/users/auth/oauth2/callback' }
let(:params) { { state: state, error: 'evil_key', error_description: message } }
let(:error) { last_request.env['omniauth.error'] }
before do
env('rack.session', { 'omniauth.state' => state })
end
it 'returns the custom error message if the state is valid' do
get callback_path, **params
expect(error.message).to eq("evil_key | #{message}")
end
it 'returns the custom `error_reason` message if the `error_description` is blank' do
get callback_path, **params.merge(error_description: ' ', error_reason: 'custom reason')
expect(error.message).to eq('evil_key | custom reason')
end
it 'returns a CSRF error if the state is invalid' do
get callback_path, **params.merge(state: 'invalid')
expect(error.message).to eq('csrf_detected | CSRF detected')
end
it 'returns a CSRF error if the state is missing' do
get callback_path, **params.without(:state)
expect(error.message).to eq('csrf_detected | CSRF detected')
end
end
end
......@@ -63,6 +63,16 @@ RSpec.describe Banzai::Filter::SpacedLinkFilter do
end
end
it 'does not process malicious input' do
Timeout.timeout(10) do
doc = filter('[ (](' * 60_000)
found_links = doc.css('a')
expect(found_links.size).to eq(0)
end
end
it 'converts multiple URLs' do
link1 = '[first](slug one)'
link2 = '[second](http://example.com/slug two)'
......
......@@ -81,32 +81,72 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
expect(subject.find_sessionless_user(:api)).to eq job_token_user
end
it 'returns lfs_token user if no job_token user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_lfs_token)
.and_return(lfs_token_user)
expect(subject.find_sessionless_user(:api)).to eq lfs_token_user
end
it 'returns basic_auth_access_token user if no lfs_token user found' do
it 'returns nil even if basic_auth_access_token is available' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_personal_access_token)
.and_return(basic_auth_access_token_user)
expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user
expect(subject.find_sessionless_user(:api)).to be_nil
end
it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do
it 'returns nil even if find_user_from_lfs_token is available' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_basic_auth_password)
.and_return(basic_auth_password_user)
.to receive(:find_user_from_lfs_token)
.and_return(lfs_token_user)
expect(subject.find_sessionless_user(:api)).to eq basic_auth_password_user
expect(subject.find_sessionless_user(:api)).to be_nil
end
it 'returns nil if no user found' do
expect(subject.find_sessionless_user(:api)).to be_blank
expect(subject.find_sessionless_user(:api)).to be_nil
end
context 'in an API request' do
before do
env['SCRIPT_NAME'] = '/api/v4/projects'
end
it 'returns basic_auth_access_token user if no job_token_user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_personal_access_token)
.and_return(basic_auth_access_token_user)
expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user
end
end
context 'in a Git request' do
before do
env['SCRIPT_NAME'] = '/group/project.git/info/refs'
end
it 'returns lfs_token user if no job_token user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_lfs_token)
.and_return(lfs_token_user)
expect(subject.find_sessionless_user(nil)).to eq lfs_token_user
end
it 'returns basic_auth_access_token user if no lfs_token user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_personal_access_token)
.and_return(basic_auth_access_token_user)
expect(subject.find_sessionless_user(nil)).to eq basic_auth_access_token_user
end
it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_basic_auth_password)
.and_return(basic_auth_password_user)
expect(subject.find_sessionless_user(nil)).to eq basic_auth_password_user
end
it 'returns nil if no user found' do
expect(subject.find_sessionless_user(nil)).to be_blank
end
end
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
......
......@@ -3,33 +3,50 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do
let(:user) { create(:user) }
using RSpec::Parameterized::TableSyntax
subject { described_class.new(user) }
subject(:verifier) { described_class.new(user) }
describe '#two_factor_authentication_required?' do
describe 'when it is required on application level' do
it 'returns true' do
stub_application_setting require_two_factor_authentication: true
let(:user) { build_stubbed(:user, otp_grace_period_started_at: Time.zone.now) }
expect(subject.two_factor_authentication_required?).to be_truthy
end
end
describe '#two_factor_authentication_enforced?' do
subject { verifier.two_factor_authentication_enforced? }
describe 'when it is required on group level' do
it 'returns true' do
allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(true)
where(:instance_level_enabled, :group_level_enabled, :grace_period_expired, :should_be_enforced) do
false | false | true | false
true | false | false | false
true | false | true | true
false | true | false | false
false | true | true | true
end
expect(subject.two_factor_authentication_required?).to be_truthy
with_them do
before do
stub_application_setting(require_two_factor_authentication: instance_level_enabled)
allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled)
stub_application_setting(two_factor_grace_period: grace_period_expired ? 0 : 1.month.in_hours)
end
it { is_expected.to eq(should_be_enforced) }
end
end
describe 'when it is not required' do
it 'returns false when not required on group level' do
allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(false)
describe '#two_factor_authentication_required?' do
subject { verifier.two_factor_authentication_required? }
where(:instance_level_enabled, :group_level_enabled, :should_be_required) do
true | false | true
false | true | true
false | false | false
end
expect(subject.two_factor_authentication_required?).to be_falsey
with_them do
before do
stub_application_setting(require_two_factor_authentication: instance_level_enabled)
allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled)
end
it { is_expected.to eq(should_be_required) }
end
end
......@@ -85,25 +102,21 @@ RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do
end
describe '#two_factor_grace_period_expired?' do
before do
allow(user).to receive(:otp_grace_period_started_at).and_return(4.hours.ago)
end
it 'returns true if the grace period has expired' do
allow(subject).to receive(:two_factor_grace_period).and_return(2)
stub_application_setting two_factor_grace_period: 0
expect(subject.two_factor_grace_period_expired?).to be_truthy
end
it 'returns false if the grace period has not expired' do
allow(subject).to receive(:two_factor_grace_period).and_return(6)
stub_application_setting two_factor_grace_period: 1.month.in_hours
expect(subject.two_factor_grace_period_expired?).to be_falsey
end
context 'when otp_grace_period_started_at is nil' do
it 'returns false' do
allow(user).to receive(:otp_grace_period_started_at).and_return(nil)
user.otp_grace_period_started_at = nil
expect(subject.two_factor_grace_period_expired?).to be_falsey
end
......
......@@ -386,7 +386,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
shared_examples 'with an invalid access token' do
it 'fails for a non-member' do
expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
.to have_attributes(auth_failure )
.to have_attributes(auth_failure)
end
context 'when project bot user is blocked' do
......@@ -396,7 +396,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'fails for a blocked project bot' do
expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
.to have_attributes(auth_failure )
.to have_attributes(auth_failure)
end
end
end
......@@ -466,6 +466,41 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
.to have_attributes(auth_failure)
end
context 'when 2fa is enabled globally' do
let_it_be(:user) do
create(:user, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
end
before do
stub_application_setting(require_two_factor_authentication: true)
end
it 'fails if grace period expired' do
stub_application_setting(two_factor_grace_period: 0)
expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
it 'goes through if grace period is not expired yet' do
stub_application_setting(two_factor_grace_period: 72)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
.to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
context 'when 2fa is enabled personally' do
let(:user) do
create(:user, :two_factor, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
end
it 'fails' do
expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
end
it 'goes through lfs authentication' do
user = create(
:user,
......@@ -757,16 +792,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
describe 'find_with_user_password' do
let!(:user) do
create(:user,
username: username,
password: password,
password_confirmation: password)
username: username,
password: password,
password_confirmation: password)
end
let(:username) { 'John' } # username isn't lowercase, test this
let(:password) { 'my-secret' }
it "finds user by valid login/password" do
expect( gl_auth.find_with_user_password(username, password) ).to eql user
expect(gl_auth.find_with_user_password(username, password)).to eql user
end
it 'finds user by valid email/password with case-insensitive email' do
......@@ -779,12 +814,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it "does not find user with invalid password" do
password = 'wrong'
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it "does not find user with invalid login" do
user = 'wrong'
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
include_examples 'user login operation with unique ip limit' do
......@@ -796,13 +831,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'finds the user in deactivated state' do
user.deactivate!
expect( gl_auth.find_with_user_password(username, password) ).to eql user
expect(gl_auth.find_with_user_password(username, password)).to eql user
end
it "does not find user in blocked state" do
user.block
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it 'does not find user in locked state' do
......@@ -814,13 +849,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it "does not find user in ldap_blocked state" do
user.ldap_block
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it 'does not find user in blocked_pending_approval state' do
user.block_pending_approval
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
context 'with increment_failed_attempts' do
......
......@@ -4,23 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::FogbugzImport::Importer do
let(:project) { create(:project_empty_repo) }
let(:importer) { described_class.new(project) }
let(:repo) do
instance_double(Gitlab::FogbugzImport::Repository,
safe_name: 'vim',
path: 'vim',
raw_data: '')
end
let(:import_data) { { 'repo' => repo } }
let(:credentials) do
{
'fb_session' => {
'uri' => 'https://testing.fogbugz.com',
'token' => 'token'
}
}
end
let(:fogbugz_project) { { 'ixProject' => project.id, 'sProject' => 'vim' } }
let(:import_data) { { 'repo' => fogbugz_project } }
let(:base_url) { 'https://testing.fogbugz.com' }
let(:token) { 'token' }
let(:credentials) { { 'fb_session' => { 'uri' => base_url, 'token' => token } } }
let(:closed_bug) do
{
......@@ -46,18 +34,22 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
let(:fogbugz_bugs) { [opened_bug, closed_bug] }
subject(:importer) { described_class.new(project) }
before do
project.create_import_data(data: import_data, credentials: credentials)
allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listCategories).and_return([])
allow_any_instance_of(Gitlab::FogbugzImport::Client).to receive(:cases).and_return(fogbugz_bugs)
stub_fogbugz('listProjects', projects: { project: [fogbugz_project], count: 1 })
stub_fogbugz('listCategories', categories: { category: [], count: 0 })
stub_fogbugz('search', cases: { case: fogbugz_bugs, count: fogbugz_bugs.size })
end
it 'imports bugs' do
expect { importer.execute }.to change { Issue.count }.by(2)
expect { subject.execute }.to change { Issue.count }.by(2)
end
it 'imports opened bugs' do
importer.execute
subject.execute
issue = Issue.where(project_id: project.id).find_by_title(opened_bug[:sTitle])
......@@ -65,10 +57,54 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
end
it 'imports closed bugs' do
importer.execute
subject.execute
issue = Issue.where(project_id: project.id).find_by_title(closed_bug[:sTitle])
expect(issue.state_id).to eq(Issue.available_states[:closed])
end
context 'verify url' do
context 'when host is localhost' do
let(:base_url) { 'https://localhost:3000' }
it 'does not allow localhost requests' do
expect { subject.execute }
.to raise_error(
::Gitlab::HTTP::BlockedUrlError,
"URL 'https://localhost:3000/api.asp' is blocked: Requests to localhost are not allowed"
)
end
end
context 'when host is on local network' do
let(:base_url) { 'http://192.168.0.1' }
it 'does not allow localhost requests' do
expect { subject.execute }
.to raise_error(
::Gitlab::HTTP::BlockedUrlError,
"URL 'http://192.168.0.1/api.asp' is blocked: Requests to the local network are not allowed"
)
end
end
context 'when host is ftp protocol' do
let(:base_url) { 'ftp://testing' }
it 'only accept http and https requests' do
expect { subject.execute }
.to raise_error(
HTTParty::UnsupportedURIScheme,
"'ftp://testing/api.asp' Must be HTTP, HTTPS or Generic"
)
end
end
end
def stub_fogbugz(command, response)
stub_request(:post, "#{base_url}/api.asp")
.with(body: hash_including({ 'cmd' => command, 'token' => token }))
.to_return(status: 200, body: response.to_xml(root: :response))
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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