Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue27
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js2
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js9
-rw-r--r--app/controllers/admin/logs_controller.rb24
-rw-r--r--app/controllers/concerns/known_sign_in.rb31
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb3
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/mailers/emails/profile.rb10
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/views/admin/logs/show.html.haml24
-rw-r--r--app/views/admin/projects/show.html.haml28
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml6
-rw-r--r--app/views/notify/unknown_sign_in_email.html.haml14
-rw-r--r--app/views/notify/unknown_sign_in_email.text.haml10
-rw-r--r--app/views/search/results/_blob_data.html.haml2
-rw-r--r--changelogs/unreleased/bvl-remove-admin-logs.yml5
-rw-r--r--changelogs/unreleased/dblessing-email-on-new-signin.yml5
-rw-r--r--changelogs/unreleased/id-fix-diff-link-on-search.yml5
-rw-r--r--config/routes/admin.rb1
-rw-r--r--doc/topics/git/partial_clone.md2
-rw-r--r--doc/user/profile/img/change_password_v13_0.pngbin0 -> 15919 bytes
-rw-r--r--doc/user/profile/img/unknown_sign_in_email_v13_0.pngbin0 -> 20047 bytes
-rw-r--r--doc/user/profile/index.md18
-rw-r--r--doc/user/profile/unknown_sign_in_notification.md16
-rw-r--r--locale/gitlab.pot45
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb9
-rw-r--r--spec/controllers/sessions_controller_spec.rb5
-rw-r--r--spec/features/admin/admin_browses_logs_spec.rb20
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb4
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js35
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js99
-rw-r--r--spec/frontend/static_site_editor/mock_data.js2
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js23
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js192
-rw-r--r--spec/mailers/emails/profile_spec.rb40
-rw-r--r--spec/routing/admin_routing_spec.rb7
-rw-r--r--spec/services/notification_service_spec.rb11
-rw-r--r--spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb59
-rw-r--r--spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb9
41 files changed, 450 insertions, 368 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
index a2efa6f0e0c..b4c68767ab3 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -5,7 +5,6 @@ import {
GlLoadingIcon,
GlTable,
GlAlert,
- GlIcon,
GlNewDropdown,
GlNewDropdownItem,
GlTabs,
@@ -15,7 +14,7 @@ import {
import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import getAlerts from '../graphql/queries/getAlerts.query.graphql';
-import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
+import { ALERTS_STATUS, ALERTS_STATUS_TABS } from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const tdClass = 'table-col d-flex d-md-table-cell align-items-center';
@@ -31,11 +30,6 @@ export default {
},
fields: [
{
- key: 'severity',
- label: s__('AlertManagement|Severity'),
- tdClass: `${tdClass} rounded-top text-capitalize`,
- },
- {
key: 'startedAt',
label: s__('AlertManagement|Start time'),
tdClass,
@@ -68,7 +62,6 @@ export default {
[ALERTS_STATUS.ACKNOWLEDGED]: s__('AlertManagement|Acknowledged'),
[ALERTS_STATUS.RESOLVED]: s__('AlertManagement|Resolved'),
},
- severityLabels: ALERTS_SEVERITY_LABELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
GlEmptyState,
@@ -79,7 +72,6 @@ export default {
TimeAgo,
GlNewDropdown,
GlNewDropdownItem,
- GlIcon,
GlTabs,
GlTab,
GlBadge,
@@ -185,21 +177,6 @@ export default {
fixed
stacked="md"
>
- <template #cell(severity)="{ item }">
- <div
- class="d-inline-flex align-items-center justify-content-between"
- data-testid="severityField"
- >
- <gl-icon
- class="mr-2"
- :size="12"
- :name="`severity-${item.severity.toLowerCase()}`"
- :class="`icon-${item.severity.toLowerCase()}`"
- />
- {{ $options.severityLabels[item.severity] }}
- </div>
- </template>
-
<template #cell(startedAt)="{ item }">
<time-ago v-if="item.startedAt" :time="item.startedAt" />
</template>
@@ -213,7 +190,7 @@ export default {
</template>
<template #cell(status)="{ item }">
- <gl-new-dropdown class="w-100" :text="item.status">
+ <gl-new-dropdown :text="item.status">
<gl-new-dropdown-item v-for="(label, field) in $options.statuses" :key="field">
{{ label }}
</gl-new-dropdown-item>
diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js
index 82732ab118f..4794cf5eead 100644
--- a/app/assets/javascripts/static_site_editor/constants.js
+++ b/app/assets/javascripts/static_site_editor/constants.js
@@ -15,3 +15,5 @@ export const LOAD_CONTENT_ERROR = __(
);
export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor');
+
+export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
index b4e4e94d4f2..49135d2141b 100644
--- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
@@ -1,4 +1,5 @@
import Api from '~/api';
+import Tracking from '~/tracking';
import { s__, sprintf } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
@@ -8,6 +9,7 @@ import {
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
+ TRACKING_ACTION_CREATE_COMMIT,
} from '../constants';
const createBranch = (projectId, branch) =>
@@ -18,8 +20,10 @@ const createBranch = (projectId, branch) =>
throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
});
-const commitContent = (projectId, message, branch, sourcePath, content) =>
- Api.commitMultiple(
+const commitContent = (projectId, message, branch, sourcePath, content) => {
+ Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
+
+ return Api.commitMultiple(
projectId,
convertObjectPropsToSnakeCase({
branch,
@@ -35,6 +39,7 @@ const commitContent = (projectId, message, branch, sourcePath, content) =>
).catch(() => {
throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
});
+};
const createMergeRequest = (projectId, title, sourceBranch, targetBranch = DEFAULT_TARGET_BRANCH) =>
Api.createProjectMergeRequest(
diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb
deleted file mode 100644
index 3ae0aef0fa4..00000000000
--- a/app/controllers/admin/logs_controller.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::LogsController < Admin::ApplicationController
- before_action :loggers
-
- def show
- end
-
- private
-
- def loggers
- @loggers ||= [
- Gitlab::AppJsonLogger,
- Gitlab::GitLogger,
- Gitlab::EnvironmentLogger,
- Gitlab::SidekiqLogger,
- Gitlab::RepositoryCheckLogger,
- Gitlab::ProjectServiceLogger,
- Gitlab::Kubernetes::Logger
- ]
- end
-end
-
-Admin::LogsController.prepend_if_ee('EE::Admin::LogsController')
diff --git a/app/controllers/concerns/known_sign_in.rb b/app/controllers/concerns/known_sign_in.rb
new file mode 100644
index 00000000000..97883d8d08c
--- /dev/null
+++ b/app/controllers/concerns/known_sign_in.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module KnownSignIn
+ include Gitlab::Utils::StrongMemoize
+
+ private
+
+ def verify_known_sign_in
+ return unless current_user
+
+ notify_user unless known_remote_ip?
+ end
+
+ def known_remote_ip?
+ known_ip_addresses.include?(request.remote_ip)
+ end
+
+ def sessions
+ strong_memoize(:session) do
+ ActiveSession.list(current_user).reject(&:is_impersonated)
+ end
+ end
+
+ def known_ip_addresses
+ [current_user.last_sign_in_ip, sessions.map(&:ip_address)].flatten
+ end
+
+ def notify_user
+ current_user.notification_service.unknown_sign_in(current_user, request.remote_ip)
+ end
+end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 534afcbab99..4c595313cb6 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -6,6 +6,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include Devise::Controllers::Rememberable
include AuthHelper
include InitializesCurrentUserMode
+ include KnownSignIn
+
+ after_action :verify_known_sign_in
protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 8a39e5257bd..9e8075d4bcc 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -7,6 +7,7 @@ class SessionsController < Devise::SessionsController
include Recaptcha::ClientHelper
include Recaptcha::Verify
include RendersLdapServers
+ include KnownSignIn
skip_before_action :check_two_factor_requirement, only: [:destroy]
# replaced with :require_no_authentication_without_flash
@@ -27,6 +28,7 @@ class SessionsController < Devise::SessionsController
before_action :frontend_tracking_data, only: [:new]
after_action :log_failed_login, if: :action_new_and_failed_login?
+ after_action :verify_known_sign_in, only: [:create]
helper_method :captcha_enabled?, :captcha_on_login_required?
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index d7f4a4be0bf..9ea0b9cb584 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -59,7 +59,7 @@ module NavHelper
end
def admin_monitoring_nav_links
- %w(system_info background_jobs logs health_check requests_profiles)
+ %w(system_info background_jobs health_check requests_profiles)
end
def group_issues_sub_menu_items
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 441439444d5..4b19149a833 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -44,6 +44,16 @@ module Emails
mail(to: @user.notification_email, subject: subject(_("Your Personal Access Tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
end
end
+
+ def unknown_sign_in_email(user, ip)
+ @user = user
+ @ip = ip
+ @target_url = edit_profile_password_url
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ mail(to: @user.notification_email, subject: subject(_("Unknown sign-in from new location")))
+ end
+ end
end
end
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 38e1d9532a6..c931b5a848f 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -161,6 +161,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.remote_mirror_update_failed_email(remote_mirror.id, user.id).message
end
+ def unknown_sign_in_email
+ Notify.unknown_sign_in_email(user, '127.0.0.1').message
+ end
+
private
def project
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 91e19d190bd..0a8d8e769ec 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -66,6 +66,14 @@ class NotificationService
mailer.access_token_about_to_expire_email(user).deliver_later
end
+ # Notify a user when a previously unknown IP or device is used to
+ # sign in to their account
+ def unknown_sign_in(user, ip)
+ return unless user.can?(:receive_notifications)
+
+ mailer.unknown_sign_in_email(user, ip).deliver_later
+ end
+
# When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
deleted file mode 100644
index eb93f645ea6..00000000000
--- a/app/views/admin/logs/show.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- page_title "Logs"
-
-%ul.nav-links.log-tabs.nav.nav-tabs
- - @loggers.each do |klass|
- %li.nav-item
- = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }, class: "#{active_when(klass == @loggers.first)} nav-link"
-.row-content-block
- To prevent performance issues admin logs output the last 2000 lines
-.tab-content
- - @loggers.each do |klass|
- .tab-pane{ class: active_when(klass == @loggers.first), id: klass.file_name_noext }
- .file-holder#README
- .js-file-title.file-title
- %i.fa.fa-file
- = klass.file_name
- .float-right
- = link_to '#', class: 'log-bottom' do
- %i.fa.fa-arrow-down
- Scroll down
- .file-content.logs
- %ol
- - klass.read_latest.each do |line|
- %li
- %p= line
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 7274099806d..8abc4c37e70 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -14,11 +14,9 @@
.col-md-12
.card
.card-header.alert.alert-danger
- Last repository check
- = "(#{time_ago_with_tooltip(@project.last_repository_check_at)})"
- failed. See
- = link_to 'repocheck.log', admin_logs_path
- for error messages.
+ - last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")
+ - last_check_message = last_check_message % { last_check_timestamp: time_ago_with_tooltip(@project.last_repository_check_at) }
+ = last_check_message.html_safe
.row
.col-md-6
.card
@@ -135,24 +133,18 @@
.card.repository-check
.card-header
- Repository check
+ = _("Repository check")
.card-body
= form_for @project, url: repository_check_admin_project_path(@project), method: :post do |f|
.form-group
- if @project.last_repository_check_at.nil?
- This repository has never been checked.
+ = _("This repository has never been checked.")
+ - elsif @project.last_repository_check_failed?
+ - failed_message = _("This repository was last checked %{last_check_timestamp}. The check %{strong_start}failed.%{strong_end} See the 'repocheck.log' file for error messages.")
+ - failed_message = failed_message % { last_check_timestamp: @project.last_repository_check_at.to_s(:medium), strong_start: "<strong class='cred'>", strong_end: "</strong>" }
+ = failed_message.html_safe
- else
- This repository was last checked
- = @project.last_repository_check_at.to_s(:medium) + '.'
- The check
- - if @project.last_repository_check_failed?
- = succeed '.' do
- %strong.cred failed
- See
- = link_to 'repocheck.log', admin_logs_path
- for error messages.
- - else
- passed.
+ = _("This repository was last checked %{last_check_timestamp}. The check passed.") % { last_check_timestamp: @project.last_repository_check_at.to_s(:medium) }
= link_to icon('question-circle'), help_page_path('administration/repository_checks')
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 3151368bb3f..28e52dc85db 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -56,7 +56,7 @@
= _('Monitoring')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } }
- = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w(system_info background_jobs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_system_info_path do
%strong.fly-out-top-item-name
= _('Monitoring')
@@ -69,10 +69,6 @@
= link_to admin_background_jobs_path, title: _('Background Jobs') do
%span
= _('Background Jobs')
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: _('Logs') do
- %span
- = _('Logs')
= nav_link(controller: :health_check) do
= link_to admin_health_check_path, title: _('Health Check') do
%span
diff --git a/app/views/notify/unknown_sign_in_email.html.haml b/app/views/notify/unknown_sign_in_email.html.haml
new file mode 100644
index 00000000000..a4123fada1b
--- /dev/null
+++ b/app/views/notify/unknown_sign_in_email.html.haml
@@ -0,0 +1,14 @@
+%p
+ = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+%p
+ = _('A sign-in to your account has been made from the following IP address: %{ip}.') % { ip: @ip }
+%p
+ - password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' }
+ = _('If you recently signed in and recognize the IP address, you may disregard this email.')
+ = _('If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}.').html_safe % { password_link_start: password_link_start, password_link_end: '</a>'.html_safe }
+ = _('Passwords should be unique and not used for any other sites or services.')
+
+- unless @user.two_factor_enabled?
+ %p
+ - mfa_link_start = '<a href="https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html" target="_blank">'.html_safe
+ = _('To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method.').html_safe % { mfa_link_start: mfa_link_start, mfa_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/unknown_sign_in_email.text.haml b/app/views/notify/unknown_sign_in_email.text.haml
new file mode 100644
index 00000000000..f3efc4c4fcd
--- /dev/null
+++ b/app/views/notify/unknown_sign_in_email.text.haml
@@ -0,0 +1,10 @@
+= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+
+= _('A sign-in to your account has been made from the following IP address: %{ip}') % { ip: @ip }
+
+= _('If you recently signed in and recognize the IP address, you may disregard this email.')
+= _('If you did not recently sign in, you should immediately change your password: %{password_link}.') % { password_link: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' }
+= _('Passwords should be unique and not used for any other sites or services.')
+
+- unless @user.two_factor_enabled?
+ = _('To further protect your account, consider configuring a two-factor authentication method: %{mfa_link}.') % { mfa_link: 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html' }
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
index 01e42224428..218de30d707 100644
--- a/app/views/search/results/_blob_data.html.haml
+++ b/app/views/search/results/_blob_data.html.haml
@@ -7,4 +7,4 @@
= search_blob_title(project, path)
- if blob.data
.file-content.code.term{ data: { qa_selector: 'file_text_content' } }
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
diff --git a/changelogs/unreleased/bvl-remove-admin-logs.yml b/changelogs/unreleased/bvl-remove-admin-logs.yml
new file mode 100644
index 00000000000..daf1ea8beda
--- /dev/null
+++ b/changelogs/unreleased/bvl-remove-admin-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove logs from the admin pages
+merge_request: 30485
+author:
+type: removed
diff --git a/changelogs/unreleased/dblessing-email-on-new-signin.yml b/changelogs/unreleased/dblessing-email-on-new-signin.yml
new file mode 100644
index 00000000000..6bf0922d842
--- /dev/null
+++ b/changelogs/unreleased/dblessing-email-on-new-signin.yml
@@ -0,0 +1,5 @@
+---
+title: Send email notification for unknown sign-ins
+merge_request: 29741
+author:
+type: added
diff --git a/changelogs/unreleased/id-fix-diff-link-on-search.yml b/changelogs/unreleased/id-fix-diff-link-on-search.yml
new file mode 100644
index 00000000000..defc9c4e27a
--- /dev/null
+++ b/changelogs/unreleased/id-fix-diff-link-on-search.yml
@@ -0,0 +1,5 @@
+---
+title: Fix blob link for the code search
+merge_request: 30473
+author:
+type: fixed
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 8ba6a6a09e2..5809be67556 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -81,7 +81,6 @@ namespace :admin do
post :preview, on: :collection
end
- resource :logs, only: [:show]
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md
index fcb7d8630f5..c776824f433 100644
--- a/doc/topics/git/partial_clone.md
+++ b/doc/topics/git/partial_clone.md
@@ -65,7 +65,7 @@ reduce the size of your working copy.
```plaintext
# Clone the repo excluding all files
-$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com/git
+$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com.git
Cloning into 'www-gitlab-com'...
remote: Enumerating objects: 678296, done.
remote: Counting objects: 100% (678296/678296), done.
diff --git a/doc/user/profile/img/change_password_v13_0.png b/doc/user/profile/img/change_password_v13_0.png
new file mode 100644
index 00000000000..f63b32557ac
--- /dev/null
+++ b/doc/user/profile/img/change_password_v13_0.png
Binary files differ
diff --git a/doc/user/profile/img/unknown_sign_in_email_v13_0.png b/doc/user/profile/img/unknown_sign_in_email_v13_0.png
new file mode 100644
index 00000000000..51a7c29cdfa
--- /dev/null
+++ b/doc/user/profile/img/unknown_sign_in_email_v13_0.png
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 66ee19437ae..383c7fe73aa 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -17,6 +17,11 @@ There are several ways to create users on GitLab. See the [creating users docume
There are several ways to sign into your GitLab account.
See the [authentication topic](../../topics/authentication/index.md) for more details.
+### Unknown sign-in
+
+GitLab will notify you if a sign-in occurs that is from an unknown IP address.
+See [Unknown Sign-In Notification](unknown_sign_in_notification.md) for more details.
+
## User profile
To access your profile:
@@ -44,6 +49,7 @@ To access your profile settings:
From there, you can:
- Update your personal information
+- Change your [password](#changing-your-password)
- Set a [custom status](#current-status) for your profile
- Manage your [commit email](#commit-email) for your profile
- Manage [2FA](account/two_factor_authentication.md)
@@ -60,6 +66,18 @@ From there, you can:
- [View your active sessions](active_sessions.md) and revoke any of them if necessary
- Access your audit log, a security log of important events involving your account
+## Changing your password
+
+1. Navigate to your [profile's](#profile-settings) **Settings > Password**.
+1. Enter your current password in the 'Current password' field.
+1. Enter your desired new password twice, once in the 'New password' field and
+ once in the 'Password confirmation' field.
+1. Click the 'Save password' button.
+
+If you don't know your current password, select the 'I forgot my password' link.
+
+![Change your password](./img/change_password_v13_0.png)
+
## Changing your username
Your `username` is a unique [`namespace`](../group/index.md#namespaces)
diff --git a/doc/user/profile/unknown_sign_in_notification.md b/doc/user/profile/unknown_sign_in_notification.md
new file mode 100644
index 00000000000..9400ead1922
--- /dev/null
+++ b/doc/user/profile/unknown_sign_in_notification.md
@@ -0,0 +1,16 @@
+# Email notification for unknown sign-ins
+
+When a user successfully signs in from a previously unknown IP address,
+GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially
+malicious or unauthorized sign-ins.
+
+There are two methods used to identify a known sign-in:
+
+- Last sign-in IP: The current sign-in IP address is checked against the last sign-in
+ IP address.
+- Current active sessions: If the user has an existing active session from the
+ same IP address. See [Active Sessions](active_sessions.md).
+
+## Example email
+
+![Unknown sign in email](./img/unknown_sign_in_email_v13_0.png)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ecd16804a27..985d0b2f95c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -918,6 +918,12 @@ msgstr ""
msgid "A secure token that identifies an external storage request."
msgstr ""
+msgid "A sign-in to your account has been made from the following IP address: %{ip}"
+msgstr ""
+
+msgid "A sign-in to your account has been made from the following IP address: %{ip}."
+msgstr ""
+
msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project."
msgstr ""
@@ -1774,9 +1780,6 @@ msgstr ""
msgid "AlertManagement|Resolved"
msgstr ""
-msgid "AlertManagement|Severity"
-msgstr ""
-
msgid "AlertManagement|Start time"
msgstr ""
@@ -11207,12 +11210,21 @@ msgstr ""
msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
msgstr ""
+msgid "If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}."
+msgstr ""
+
+msgid "If you did not recently sign in, you should immediately change your password: %{password_link}."
+msgstr ""
+
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
msgstr ""
+msgid "If you recently signed in and recognize the IP address, you may disregard this email."
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -12222,6 +12234,9 @@ msgstr ""
msgid "Last reply by"
msgstr ""
+msgid "Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages."
+msgstr ""
+
msgid "Last repository check run"
msgstr ""
@@ -14937,6 +14952,9 @@ msgstr ""
msgid "Password was successfully updated. Please login with it"
msgstr ""
+msgid "Passwords should be unique and not used for any other sites or services."
+msgstr ""
+
msgid "Past due"
msgstr ""
@@ -17712,6 +17730,9 @@ msgstr ""
msgid "Repository URL"
msgstr ""
+msgid "Repository check"
+msgstr ""
+
msgid "Repository check was triggered."
msgstr ""
@@ -21778,9 +21799,18 @@ msgstr ""
msgid "This repository"
msgstr ""
+msgid "This repository has never been checked."
+msgstr ""
+
msgid "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
msgstr ""
+msgid "This repository was last checked %{last_check_timestamp}. The check %{strong_start}failed.%{strong_end} See the 'repocheck.log' file for error messages."
+msgstr ""
+
+msgid "This repository was last checked %{last_check_timestamp}. The check passed."
+msgstr ""
+
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""
@@ -22178,6 +22208,12 @@ msgstr ""
msgid "To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}."
msgstr ""
+msgid "To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method."
+msgstr ""
+
+msgid "To further protect your account, consider configuring a two-factor authentication method: %{mfa_link}."
+msgstr ""
+
msgid "To get started you enter your FogBugz URL and login information below. In the next steps, you'll be able to map users and select the projects you want to import."
msgstr ""
@@ -22691,6 +22727,9 @@ msgstr ""
msgid "Unknown response text"
msgstr ""
+msgid "Unknown sign-in from new location"
+msgstr ""
+
msgid "Unlimited"
msgstr ""
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 9537ff62f8b..4fcc251ec27 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -144,6 +144,10 @@ describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode:
let(:extern_uid) { 'my-uid' }
let(:provider) { :github }
+ it_behaves_like 'known sign in' do
+ let(:post_action) { post provider }
+ end
+
it 'allows sign in' do
post provider
@@ -287,6 +291,11 @@ describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode:
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
end
+ it_behaves_like 'known sign in' do
+ let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') }
+ let(:post_action) { post :saml, params: { SAMLResponse: mock_saml_response } }
+ end
+
context 'sign up' do
before do
user.destroy
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index eb24532b35f..a65698a5b56 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -99,6 +99,11 @@ describe SessionsController do
set_devise_mapping(context: @request)
end
+ it_behaves_like 'known sign in' do
+ let(:user) { create(:user) }
+ let(:post_action) { post(:create, params: { user: { login: user.username, password: user.password } }) }
+ end
+
context 'when using standard authentications' do
context 'invalid password' do
it 'does not authenticate user' do
diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb
deleted file mode 100644
index 45e860e1536..00000000000
--- a/spec/features/admin/admin_browses_logs_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe 'Admin browses logs' do
- before do
- sign_in(create(:admin))
- end
-
- it 'shows available log files' do
- visit admin_logs_path
-
- expect(page).to have_link 'application_json.log'
- expect(page).to have_link 'git_json.log'
- expect(page).to have_link 'test.log'
- expect(page).to have_link 'sidekiq.log'
- expect(page).to have_link 'repocheck.log'
- expect(page).to have_link 'kubernetes.log'
- end
-end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 9949595fddf..0fdc7346535 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -40,6 +40,9 @@ describe 'User searches for code' do
find('.btn-search').click
expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
+
+ find("#L3").click
+ expect(current_url).to match(/master\/.gitignore#L3/)
end
it 'search mutiple words with refs switching' do
@@ -57,6 +60,7 @@ describe 'User searches for code' do
expect(page).to have_selector('.results', text: expected_result)
expect(find_field('dashboard_search').value).to eq(search)
+ expect(find("#L1502")[:href]).to match(/v1.0.0\/files\/markdown\/ruby-style-guide.md#L1502/)
end
end
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
index 81d966a42b5..662c39fc84d 100644
--- a/spec/frontend/alert_management/components/alert_management_list_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -6,7 +6,6 @@ import {
GlLoadingIcon,
GlNewDropdown,
GlBadge,
- GlIcon,
GlTab,
} from '@gitlab/ui';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -26,7 +25,6 @@ describe('AlertManagementList', () => {
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findNumberOfAlertsBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo);
- const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
function mountComponent({
props = {
@@ -185,37 +183,6 @@ describe('AlertManagementList', () => {
expect(findStatusDropdown().exists()).toBe(true);
});
- it('shows correct severity icons', () => {
- mountComponent({
- props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: mockAlerts, errored: false },
- loading: false,
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlTable).exists()).toBe(true);
- expect(
- findAlertsTable()
- .find(GlIcon)
- .classes('icon-critical'),
- ).toBe(true);
- });
- });
-
- it('Internationalizes severity text', () => {
- mountComponent({
- props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: mockAlerts, errored: false },
- loading: false,
- });
-
- expect(
- findSeverityFields()
- .at(0)
- .text(),
- ).toBe('Critical');
- });
-
describe('handle date fields', () => {
it('should display time ago dates when values provided', () => {
mountComponent({
@@ -226,7 +193,6 @@ describe('AlertManagementList', () => {
iid: 1,
startedAt: '2020-03-17T23:18:14.996Z',
endedAt: '2020-04-17T23:18:14.996Z',
- severity: 'high',
},
],
errored: false,
@@ -245,7 +211,6 @@ describe('AlertManagementList', () => {
iid: 1,
startedAt: null,
endedAt: null,
- severity: 'high',
},
],
errored: false,
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
new file mode 100644
index 00000000000..7db6dba770e
--- /dev/null
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -0,0 +1,99 @@
+import { shallowMount } from '@vue/test-utils';
+import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+
+const cronIntervalPresets = {
+ everyDay: '0 4 * * *',
+ everyWeek: '0 4 * * 0',
+ everyMonth: '0 4 1 * *',
+};
+
+describe('Interval Pattern Input Component', () => {
+ let oldWindowGl;
+ let wrapper;
+
+ const findEveryDayRadio = () => wrapper.find('#every-day');
+ const findEveryWeekRadio = () => wrapper.find('#every-week');
+ const findEveryMonthRadio = () => wrapper.find('#every-month');
+ const findCustomRadio = () => wrapper.find('#custom');
+ const findCustomInput = () => wrapper.find('#schedule_cron');
+ const selectEveryDayRadio = () => findEveryDayRadio().setChecked();
+ const selectEveryWeekRadio = () => findEveryWeekRadio().setChecked();
+ const selectEveryMonthRadio = () => findEveryMonthRadio().setChecked();
+ const selectCustomRadio = () => findCustomRadio().trigger('click');
+
+ const createWrapper = (props = {}) => {
+ if (wrapper) {
+ throw new Error('A wrapper already exists');
+ }
+
+ wrapper = shallowMount(IntervalPatternInput, {
+ propsData: { ...props },
+ });
+ };
+
+ beforeEach(() => {
+ oldWindowGl = window.gl;
+ window.gl = {
+ ...(window.gl || {}),
+ pipelineScheduleFieldErrors: {
+ updateFormValidityState: jest.fn(),
+ },
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ window.gl = oldWindowGl;
+ });
+
+ describe('when prop initialCronInterval is passed', () => {
+ describe('and prop initialCronInterval is custom', () => {
+ beforeEach(() => {
+ createWrapper({ initialCronInterval: '1 2 3 4 5' });
+ });
+
+ it('the input is enabled', () => {
+ expect(findCustomInput().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ describe('and prop initialCronInterval is a preset', () => {
+ beforeEach(() => {
+ createWrapper({ initialCronInterval: cronIntervalPresets.everyDay });
+ });
+
+ it('the input is disabled', () => {
+ expect(findCustomInput().attributes('disabled')).toBe('disabled');
+ });
+ });
+ });
+
+ describe('when prop initialCronInterval is not passed', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('the input is enabled since custom is default value', () => {
+ expect(findCustomInput().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ describe('User Actions', () => {
+ it.each`
+ desc | initialCronInterval | act | expectedValue
+ ${'when everyday is selected, update value'} | ${'1 2 3 4 5'} | ${selectEveryDayRadio} | ${cronIntervalPresets.everyDay}
+ ${'when everyweek is selected, update value'} | ${'1 2 3 4 5'} | ${selectEveryWeekRadio} | ${cronIntervalPresets.everyWeek}
+ ${'when everymonth is selected, update value'} | ${'1 2 3 4 5'} | ${selectEveryMonthRadio} | ${cronIntervalPresets.everyMonth}
+ ${'when custom is selected, add space to value'} | ${cronIntervalPresets.everyMonth} | ${selectCustomRadio} | ${`${cronIntervalPresets.everyMonth} `}
+ `('$desc', ({ initialCronInterval, act, expectedValue }) => {
+ createWrapper({ initialCronInterval });
+
+ act();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCustomInput().element.value).toBe(expectedValue);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js
index c90ecc96521..371695e913e 100644
--- a/spec/frontend/static_site_editor/mock_data.js
+++ b/spec/frontend/static_site_editor/mock_data.js
@@ -45,3 +45,5 @@ export const createMergeRequestResponse = {
iid: '123',
web_url: '/merge_requests/123',
};
+
+export const trackingCategory = 'projects:static_site_editor:show';
diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
index 5dd9ea86221..a1e9ff4ec4c 100644
--- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
+++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
@@ -1,11 +1,13 @@
import Api from '~/api';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import {
DEFAULT_TARGET_BRANCH,
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
+ TRACKING_ACTION_CREATE_COMMIT,
} from '~/static_site_editor/constants';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
@@ -18,6 +20,7 @@ import {
createMergeRequestResponse,
sourcePath,
sourceContent as content,
+ trackingCategory,
} from '../mock_data';
jest.mock('~/static_site_editor/services/generate_branch_name');
@@ -25,6 +28,8 @@ jest.mock('~/static_site_editor/services/generate_branch_name');
describe('submitContentChanges', () => {
const mergeRequestTitle = `Update ${sourcePath} file`;
const branch = 'branch-name';
+ let trackingSpy;
+ let origPage;
beforeEach(() => {
jest.spyOn(Api, 'createBranch').mockResolvedValue({ data: commitBranchResponse });
@@ -34,6 +39,15 @@ describe('submitContentChanges', () => {
.mockResolvedValue({ data: createMergeRequestResponse });
generateBranchName.mockReturnValue(branch);
+
+ origPage = document.body.dataset.page;
+ document.body.dataset.page = trackingCategory;
+ trackingSpy = mockTracking(document.body.dataset.page, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ document.body.dataset.page = origPage;
+ unmockTracking();
});
it('creates a branch named after the username and target branch', () => {
@@ -69,6 +83,15 @@ describe('submitContentChanges', () => {
});
});
+ it('sends the correct tracking event when committing content changes', () => {
+ return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
+ expect(trackingSpy).toHaveBeenCalledWith(
+ document.body.dataset.page,
+ TRACKING_ACTION_CREATE_COMMIT,
+ );
+ });
+ });
+
it('notifies error when content could not be committed', () => {
Api.commitMultiple.mockRejectedValueOnce();
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
deleted file mode 100644
index b20bc96f9be..00000000000
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
-
-Vue.use(Translate);
-
-const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput);
-const inputNameAttribute = 'schedule[cron]';
-
-const cronIntervalPresets = {
- everyDay: '0 4 * * *',
- everyWeek: '0 4 * * 0',
- everyMonth: '0 4 1 * *',
-};
-
-window.gl = window.gl || {};
-
-window.gl.pipelineScheduleFieldErrors = {
- updateFormValidityState: () => {},
-};
-
-describe('Interval Pattern Input Component', function() {
- describe('when prop initialCronInterval is passed (edit)', function() {
- describe('when prop initialCronInterval is custom', function() {
- beforeEach(function() {
- this.initialCronInterval = '1 2 3 4 5';
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- initialCronInterval: this.initialCronInterval,
- },
- }).$mount();
- });
-
- it('is initialized as a Vue component', function() {
- expect(this.intervalPatternComponent).toBeDefined();
- });
-
- it('prop initialCronInterval is set', function() {
- expect(this.intervalPatternComponent.initialCronInterval).toBe(this.initialCronInterval);
- });
-
- it('sets isEditable to true', function(done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(true);
- done();
- });
- });
- });
-
- describe('when prop initialCronInterval is preset', function() {
- beforeEach(function() {
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- inputNameAttribute,
- initialCronInterval: '0 4 * * *',
- },
- }).$mount();
- });
-
- it('is initialized as a Vue component', function() {
- expect(this.intervalPatternComponent).toBeDefined();
- });
-
- it('sets isEditable to false', function(done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(false);
- done();
- });
- });
- });
- });
-
- describe('when prop initialCronInterval is not passed (new)', function() {
- beforeEach(function() {
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- inputNameAttribute,
- },
- }).$mount();
- });
-
- it('is initialized as a Vue component', function() {
- expect(this.intervalPatternComponent).toBeDefined();
- });
-
- it('prop initialCronInterval is set', function() {
- const defaultInitialCronInterval = '';
-
- expect(this.intervalPatternComponent.initialCronInterval).toBe(defaultInitialCronInterval);
- });
-
- it('sets isEditable to true', function(done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(true);
- done();
- });
- });
- });
-
- describe('User Actions', function() {
- beforeEach(function() {
- // For an unknown reason, some browsers do not propagate click events
- // on radio buttons in a way Vue can register. So, we have to mount
- // to a fixture.
- setFixtures('<div id="my-mount"></div>');
-
- this.initialCronInterval = '1 2 3 4 5';
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- initialCronInterval: this.initialCronInterval,
- },
- }).$mount('#my-mount');
- });
-
- it('cronInterval is updated when everyday preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-day').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyDay);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- cronIntervalPresets.everyDay,
- );
- done();
- });
- });
-
- it('cronInterval is updated when everyweek preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-week').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyWeek);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- cronIntervalPresets.everyWeek,
- );
-
- done();
- });
- });
-
- it('cronInterval is updated when everymonth preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyMonth);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- cronIntervalPresets.everyMonth,
- );
- done();
- });
- });
-
- it('only a space is added to cronInterval (trimmed later) when custom radio is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
- this.intervalPatternComponent.$el.querySelector('#custom').click();
-
- Vue.nextTick(() => {
- const intervalWithSpaceAppended = `${cronIntervalPresets.everyMonth} `;
-
- expect(this.intervalPatternComponent.cronInterval).toBe(intervalWithSpaceAppended);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- intervalWithSpaceAppended,
- );
- done();
- });
- });
-
- it('text input is disabled when preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(false);
- expect(
- this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled,
- ).toBe(true);
- done();
- });
- });
-
- it('text input is enabled when custom is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
- this.intervalPatternComponent.$el.querySelector('#custom').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(true);
- expect(
- this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled,
- ).toBe(false);
- done();
- });
- });
- });
-});
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 58c04fb4834..f84bf43b9c4 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -156,4 +156,44 @@ describe Emails::Profile do
it { expect { Notify.access_token_about_to_expire_email('foo') }.not_to raise_error }
end
end
+
+ describe 'user unknown sign in email' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:ip) { '169.0.0.1' }
+
+ subject { Notify.unknown_sign_in_email(user, ip) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'is sent to the user' do
+ expect(subject).to deliver_to user.email
+ end
+
+ it 'has the correct subject' do
+ expect(subject).to have_subject /^Unknown sign-in from new location$/
+ end
+
+ it 'mentions the unknown sign-in IP' do
+ expect(subject).to have_body_text /A sign-in to your account has been made from the following IP address: #{ip}./
+ end
+
+ it 'includes a link to the change password page' do
+ expect(subject).to have_body_text /#{edit_profile_password_path}/
+ end
+
+ it 'mentions two factor authentication when two factor is not enabled' do
+ expect(subject).to have_body_text /two-factor authentication/
+ end
+
+ context 'when two factor authentication is enabled' do
+ it 'does not mention two factor authentication' do
+ two_factor_user = create(:user, :two_factor)
+
+ expect( Notify.unknown_sign_in_email(two_factor_user, ip) )
+ .not_to have_body_text /two-factor authentication/
+ end
+ end
+ end
end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 10cf76b607f..25216b0c712 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -113,13 +113,6 @@ describe Admin::HookLogsController, 'routing' do
end
end
-# admin_logs GET /admin/logs(.:format) admin/logs#show
-describe Admin::LogsController, "routing" do
- it "to #show" do
- expect(get("/admin/logs")).to route_to('admin/logs#show')
- end
-end
-
# admin_background_jobs GET /admin/background_jobs(.:format) admin/background_jobs#show
describe Admin::BackgroundJobsController, "routing" do
it "to #show" do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 163ca0b9bc3..2134166cd6d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -240,6 +240,17 @@ describe NotificationService, :mailer do
end
end
+ describe '#unknown_sign_in' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:ip) { '127.0.0.1' }
+
+ subject { notification.unknown_sign_in(user, ip) }
+
+ it 'sends email to the user' do
+ expect { subject }.to have_enqueued_email(user, ip, mail: 'unknown_sign_in_email')
+ end
+ end
+
describe 'Notes' do
context 'issue note' do
let(:project) { create(:project, :private) }
diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
new file mode 100644
index 00000000000..60abb76acec
--- /dev/null
+++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'known sign in' do
+ def stub_remote_ip(ip)
+ request.remote_ip = ip
+ end
+
+ def stub_user_ip(ip)
+ user.update!(current_sign_in_ip: ip)
+ end
+
+ context 'with a valid post' do
+ context 'when remote IP does not match user last sign in IP' do
+ before do
+ stub_user_ip('127.0.0.1')
+ stub_remote_ip('169.0.0.1')
+ end
+
+ it 'notifies the user' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:unknown_sign_in)
+ end
+
+ post_action
+ end
+ end
+
+ context 'when remote IP matches an active session' do
+ before do
+ existing_sessions = ActiveSession.session_ids_for_user(user.id)
+ existing_sessions.each { |sessions| ActiveSession.destroy(user, sessions) }
+
+ stub_user_ip('169.0.0.1')
+ stub_remote_ip('127.0.0.1')
+
+ ActiveSession.set(user, request)
+ end
+
+ it 'does not notify the user' do
+ expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
+
+ post_action
+ end
+ end
+
+ context 'when remote IP address matches last sign in IP' do
+ before do
+ stub_user_ip('127.0.0.1')
+ stub_remote_ip('127.0.0.1')
+ end
+
+ it 'does not notify the user' do
+ expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
+
+ post_action
+ end
+ end
+ end
+end
diff --git a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
index aee2b0baf92..2f8a75a81c8 100644
--- a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
@@ -58,15 +58,6 @@ describe 'layouts/nav/sidebar/_admin' do
it_behaves_like 'page has active sub tab', 'Users'
end
- context 'on logs' do
- before do
- allow(controller).to receive(:controller_name).and_return('logs')
- end
-
- it_behaves_like 'page has active tab', 'Monitoring'
- it_behaves_like 'page has active sub tab', 'Logs'
- end
-
context 'on messages' do
before do
allow(controller).to receive(:controller_name).and_return('broadcast_messages')