diff options
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 Binary files differnew file mode 100644 index 00000000000..f63b32557ac --- /dev/null +++ b/doc/user/profile/img/change_password_v13_0.png 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 Binary files differnew file mode 100644 index 00000000000..51a7c29cdfa --- /dev/null +++ b/doc/user/profile/img/unknown_sign_in_email_v13_0.png 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') |