summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Release Tools Bot <delivery-team+release-tools@gitlab.com>2021-09-30 13:31:51 +0000
committerGitLab Release Tools Bot <delivery-team+release-tools@gitlab.com>2021-09-30 13:31:51 +0000
commita5131ced0f04bd5e8bc58fc54b60f5e93ed93b4c (patch)
tree1454260eb7f96d397a80ee31c445a90da35c7082
parentaee004311cd93409176ea4f6e2bdcd0601487e4b (diff)
parentc362490dfe27056da8c796e3b3e5a0f5e42acfaa (diff)
downloadgitlab-ce-a5131ced0f04bd5e8bc58fc54b60f5e93ed93b4c.tar.gz
Merge remote-tracking branch 'dev/14-3-stable' into 14-3-stable
-rw-r--r--CHANGELOG.md34
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue98
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/index.js31
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js33
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js4
-rw-r--r--app/assets/javascripts/users_select/index.js2
-rw-r--r--app/controllers/admin/users_controller.rb7
-rw-r--r--app/controllers/concerns/impersonation.rb19
-rw-r--r--app/controllers/import/gitea_controller.rb6
-rw-r--r--app/controllers/profiles/passwords_controller.rb8
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb10
-rw-r--r--app/controllers/projects/project_members_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb5
-rw-r--r--app/controllers/uploads_controller.rb6
-rw-r--r--app/graphql/types/group_invitation_type.rb2
-rw-r--r--app/graphql/types/project_invitation_type.rb2
-rw-r--r--app/helpers/external_link_helper.rb5
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/models/user.rb3
-rw-r--r--app/services/projects/destroy_service.rb11
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml13
-rw-r--r--app/views/projects/_export.html.haml1
-rw-r--r--config/initializers/doorkeeper.rb5
-rw-r--r--config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb42
-rw-r--r--db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb54
-rw-r--r--db/schema_migrations/202109140953101
-rw-r--r--doc/api/dependencies.md10
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/user/profile/account/two_factor_authentication.md6
-rw-r--r--doc/user/project/settings/import_export.md6
-rw-r--r--lib/api/import_bitbucket_server.rb4
-rw-r--r--lib/api/invitations.rb2
-rw-r--r--lib/api/projects.rb5
-rw-r--r--lib/api/users.rb8
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb21
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/auth/auth_finders.rb4
-rw-r--r--lib/gitlab/auth/request_authenticator.rb24
-rw-r--r--lib/gitlab/auth/two_factor_auth_verifier.rb4
-rw-r--r--lib/gitlab/fogbugz_import.rb11
-rw-r--r--lib/gitlab/fogbugz_import/client.rb2
-rw-r--r--lib/gitlab/fogbugz_import/http_adapter.rb21
-rw-r--r--lib/gitlab/import_export/group/import_export.yml1
-rw-r--r--lib/gitlab/import_export/project/import_export.yml2
-rw-r--r--lib/gitlab/legacy_github_import/client.rb6
-rw-r--r--lib/gitlab/string_regex_marker.rb10
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb8
-rw-r--r--spec/controllers/admin/users_controller_spec.rb23
-rw-r--r--spec/controllers/import/gitea_controller_spec.rb42
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb63
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb46
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb10
-rw-r--r--spec/controllers/projects_controller_spec.rb41
-rw-r--r--spec/controllers/uploads_controller_spec.rb2
-rw-r--r--spec/features/profiles/password_spec.rb78
-rw-r--r--spec/features/profiles/two_factor_auths_spec.rb88
-rw-r--r--spec/features/users/login_spec.rb1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json17
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson2
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap99
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js98
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js37
-rw-r--r--spec/frontend/users_select/index_spec.js16
-rw-r--r--spec/graphql/types/group_invitation_type_spec.rb2
-rw-r--r--spec/graphql/types/project_invitation_type_spec.rb2
-rw-r--r--spec/helpers/external_link_helper_spec.rb8
-rw-r--r--spec/helpers/icons_helper_spec.rb8
-rw-r--r--spec/initializers/100_patch_omniauth_oauth2_spec.rb56
-rw-r--r--spec/lib/banzai/filter/spaced_link_filter_spec.rb10
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb70
-rw-r--r--spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb61
-rw-r--r--spec/lib/gitlab/auth_spec.rb59
-rw-r--r--spec/lib/gitlab/fogbugz_import/importer_spec.rb80
-rw-r--r--spec/lib/gitlab/git_access_spec.rb35
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml10
-rw-r--r--spec/lib/gitlab/legacy_github_import/client_spec.rb9
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb14
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb12
-rw-r--r--spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb47
-rw-r--r--spec/models/user_spec.rb82
-rw-r--r--spec/policies/global_policy_spec.rb18
-rw-r--r--spec/requests/api/import_bitbucket_server_spec.rb14
-rw-r--r--spec/requests/api/invitations_spec.rb40
-rw-r--r--spec/requests/api/projects_spec.rb10
-rw-r--r--spec/requests/api/users_spec.rb171
-rw-r--r--spec/requests/git_http_spec.rb16
-rw-r--r--spec/requests/lfs_http_spec.rb6
-rw-r--r--spec/requests/rack_attack_global_spec.rb45
-rw-r--r--spec/services/projects/destroy_service_spec.rb10
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb10
-rw-r--r--workhorse/internal/artifacts/entry.go3
95 files changed, 1698 insertions, 460 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd4d29fd535..31faa40327b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,40 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 14.3.1 (2021-09-30)
+
+### Security (29 changes)
+
+- [Fix permissions check on project members import](gitlab-org/security/gitlab@63ba9ad2a1067eb74df493e273707bb64a13a197) ([merge request](gitlab-org/security/gitlab!1858))
+- [Require password param for 2FA changes](gitlab-org/security/gitlab@f246cfbd15344ba74a0182276bf63f0b5f1a4a31) ([merge request](gitlab-org/security/gitlab!1813))
+- [Respect disabled import sources when initiating import via API](gitlab-org/security/gitlab@046e964b0151fc8c58063281a39af063ffb678bd) ([merge request](gitlab-org/security/gitlab!1846))
+- [Return 404 if model id wasn't passed to UploadsController](gitlab-org/security/gitlab@747e6f0e4aec39462f296fd56b37df1c255d29cb) ([merge request](gitlab-org/security/gitlab!1843))
+- [Scrub artifacts signed URL in SendEntry logs](gitlab-org/security/gitlab@f6c57892ddc9518efaace1021346b42b4c805a1c) ([merge request](gitlab-org/security/gitlab!1840))
+- [Prevent double-impersonation and impersonation breakout](gitlab-org/security/gitlab@615d418f9315ca3b3619689c47201f618cf6bde9) ([merge request](gitlab-org/security/gitlab!1834))
+- [Clear session access tokens when starting/stopping impersonation](gitlab-org/security/gitlab@62c2e0d3ed73f2d7ded90d04fe232ff6ae2f6136) ([merge request](gitlab-org/security/gitlab!1831))
+- [Prevent users from bypassing 2FA on certain pages](gitlab-org/security/gitlab@0b41838b36da09a9230de4d8449040a701464de7) ([merge request](gitlab-org/security/gitlab!1827))
+- [Use validated URL when sending request to Gitea Importer](gitlab-org/security/gitlab@26731d762f6503fe1b8b509be11c56e77601a552) ([merge request](gitlab-org/security/gitlab!1822))
+- [Fix XSS in Jira link](gitlab-org/security/gitlab@d41060acb2aa151119042db9162a102d4e2c15ab) ([merge request](gitlab-org/security/gitlab!1819)) **GitLab Enterprise Edition**
+- [Fix fogbugz importer DNS Rebind SSRF](gitlab-org/security/gitlab@cc13d57c66cc65e6f920bdeab57b9fdb9d6baac1) ([merge request](gitlab-org/security/gitlab!1814))
+- [Remove related project access tokens when a project is deleted](gitlab-org/security/gitlab@d32c0d57d5b39601034c4c4ae983ea80c05db429) ([merge request](gitlab-org/security/gitlab!1810))
+- [Require group admin access to list pending invites](gitlab-org/security/gitlab@911bb0cb78e00934c491af59729fa84fffae7676) ([merge request](gitlab-org/security/gitlab!1793))
+- [Do not export and import repository_size_limit](gitlab-org/security/gitlab@0f3feca459895fc6665f8b0dfc16d4dcd7112944) ([merge request](gitlab-org/security/gitlab!1770))
+- [Escapes MR approval rule names correctly](gitlab-org/security/gitlab@4fcd97230bbc31780fe14d75694bb6433d57e677) ([merge request](gitlab-org/security/gitlab!1807))
+- [Filter shared groups autocomplete by permitted](gitlab-org/security/gitlab@b5144abb0516af61686402c2ad720967d11cb03c) ([merge request](gitlab-org/security/gitlab!1804)) **GitLab Enterprise Edition**
+- [Require access token for git when 2fa is required](gitlab-org/security/gitlab@ea22f67b47bf0d6c801f2bf6c9672c0ea5afd30c) ([merge request](gitlab-org/security/gitlab!1794))
+- [Prohibit anonymous access for specific user API endpoint](gitlab-org/security/gitlab@c52890997ad574812ae4da968f2f6ecfd9f7ff59) ([merge request](gitlab-org/security/gitlab!1792))
+- [Disable exporting pipeline triggers on project export](gitlab-org/security/gitlab@f7f18fbdd8e81a9b3e0650250316c7bb17ac1956) ([merge request](gitlab-org/security/gitlab!1791))
+- [Add pagination to dependencies API](gitlab-org/security/gitlab@203328889059564ba6085663b21355149c01e501) ([merge request](gitlab-org/security/gitlab!1726)) **GitLab Enterprise Edition**
+- [Do not allow status checks to exist with external protected branches](gitlab-org/security/gitlab@327d8080e7e7b0bc77b7933f8026ec0cf1abd99a) ([merge request](gitlab-org/security/gitlab!1788)) **GitLab Enterprise Edition**
+- [Permission check issuable template API data](gitlab-org/security/gitlab@de7851c2ab58c31df49c8a406ed0c3f3ad779e26) ([merge request](gitlab-org/security/gitlab!1785)) **GitLab Enterprise Edition**
+- [Apply account locking to password reset page](gitlab-org/security/gitlab@050dfa71191ffaea77a4a18e0dea1f3336f40db5) ([merge request](gitlab-org/security/gitlab!1782))
+- [Enforce configured scopes for Oauth applications](gitlab-org/security/gitlab@ce83bb14b5a4521f889086a439f1628041843589) ([merge request](gitlab-org/security/gitlab!1779))
+- [Verify state before using errors from OAuth2 OmniAuth providers](gitlab-org/security/gitlab@dcc2cad6c03255ac70f29ed9c0f6c8bc11ac1018) ([merge request](gitlab-org/security/gitlab!1776))
+- [Prevent moving epic issues to different group hierarchy](gitlab-org/security/gitlab@167601717f2ad46fee2320af6ac49674026501be) ([merge request](gitlab-org/security/gitlab!1772)) **GitLab Enterprise Edition**
+- [Fix GFM autocomplete xss](gitlab-org/security/gitlab@8816ab6af1d1aa752f22da7850d4d1c983f2d43a) ([merge request](gitlab-org/security/gitlab!1767))
+- [Prevent showing not allowed subgroup epics](gitlab-org/security/gitlab@b841c78c47b6a56b618186720bffc26922807356) ([merge request](gitlab-org/security/gitlab!1764)) **GitLab Enterprise Edition**
+- [Fix denial-of-service attack in Markdown parser](gitlab-org/security/gitlab@5e5973b5c28862381729408ba4df650c3d4f7ce0) ([merge request](gitlab-org/security/gitlab!1730))
+
## 14.3.0 (2021-09-21)
### Added (111 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 1cbb0aa64c4..7ea95c24dd9 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-14.3.0 \ No newline at end of file
+14.3.1 \ No newline at end of file
diff --git a/VERSION b/VERSION
index 1cbb0aa64c4..7ea95c24dd9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-14.3.0 \ No newline at end of file
+14.3.1 \ No newline at end of file
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
new file mode 100644
index 00000000000..280c222c380
--- /dev/null
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlFormInput, GlFormGroup, GlButton, GlForm } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { __ } from '~/locale';
+
+export const i18n = {
+ currentPassword: __('Current password'),
+ confirmWebAuthn: __(
+ 'Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.',
+ ),
+ confirm: __('Are you sure? This will invalidate your registered applications and U2F devices.'),
+ disableTwoFactor: __('Disable two-factor authentication'),
+ regenerateRecoveryCodes: __('Regenerate recovery codes'),
+};
+
+export default {
+ name: 'ManageTwoFactorForm',
+ i18n,
+ components: {
+ GlForm,
+ GlFormInput,
+ GlFormGroup,
+ GlButton,
+ },
+ inject: [
+ 'webauthnEnabled',
+ 'profileTwoFactorAuthPath',
+ 'profileTwoFactorAuthMethod',
+ 'codesProfileTwoFactorAuthPath',
+ 'codesProfileTwoFactorAuthMethod',
+ ],
+ data() {
+ return {
+ method: '',
+ action: '#',
+ };
+ },
+ computed: {
+ confirmText() {
+ if (this.webauthnEnabled) {
+ return i18n.confirmWebAuthn;
+ }
+
+ return i18n.confirm;
+ },
+ },
+ methods: {
+ handleFormSubmit(event) {
+ this.method = event.submitter.dataset.formMethod;
+ this.action = event.submitter.dataset.formAction;
+ },
+ },
+ csrf,
+};
+</script>
+
+<template>
+ <gl-form
+ class="gl-display-inline-block"
+ method="post"
+ :action="action"
+ @submit="handleFormSubmit($event)"
+ >
+ <input type="hidden" name="_method" data-testid="test-2fa-method-field" :value="method" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+
+ <gl-form-group :label="$options.i18n.currentPassword" label-for="current-password">
+ <gl-form-input
+ id="current-password"
+ type="password"
+ name="current_password"
+ required
+ data-qa-selector="current_password_field"
+ />
+ </gl-form-group>
+
+ <gl-button
+ type="submit"
+ class="btn-danger gl-mr-3 gl-display-inline-block"
+ data-testid="test-2fa-disable-button"
+ variant="danger"
+ :data-confirm="confirmText"
+ :data-form-action="profileTwoFactorAuthPath"
+ :data-form-method="profileTwoFactorAuthMethod"
+ >
+ {{ $options.i18n.disableTwoFactor }}
+ </gl-button>
+ <gl-button
+ type="submit"
+ class="gl-display-inline-block"
+ data-testid="test-2fa-regenerate-codes-button"
+ :data-form-action="codesProfileTwoFactorAuthPath"
+ :data-form-method="codesProfileTwoFactorAuthMethod"
+ >
+ {{ $options.i18n.regenerateRecoveryCodes }}
+ </gl-button>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/authentication/two_factor_auth/index.js b/app/assets/javascripts/authentication/two_factor_auth/index.js
index 5e59c44e8cd..f663c0705e6 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/index.js
+++ b/app/assets/javascripts/authentication/two_factor_auth/index.js
@@ -1,8 +1,39 @@
import Vue from 'vue';
import { updateHistory, removeParams } from '~/lib/utils/url_utility';
+import ManageTwoFactorForm from './components/manage_two_factor_form.vue';
import RecoveryCodes from './components/recovery_codes.vue';
import { SUCCESS_QUERY_PARAM } from './constants';
+export const initManageTwoFactorForm = () => {
+ const el = document.querySelector('.js-manage-two-factor-form');
+
+ if (!el) {
+ return false;
+ }
+
+ const {
+ webauthnEnabled = false,
+ profileTwoFactorAuthPath = '',
+ profileTwoFactorAuthMethod = '',
+ codesProfileTwoFactorAuthPath = '',
+ codesProfileTwoFactorAuthMethod = '',
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ webauthnEnabled,
+ profileTwoFactorAuthPath,
+ profileTwoFactorAuthMethod,
+ codesProfileTwoFactorAuthPath,
+ codesProfileTwoFactorAuthMethod,
+ },
+ render(createElement) {
+ return createElement(ManageTwoFactorForm);
+ },
+ });
+};
+
export const initRecoveryCodes = () => {
const el = document.querySelector('.js-2fa-recovery-codes');
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 470c785f7e4..cb63c86a4fa 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
-import { escape, sortBy, template } from 'lodash';
+import { escape as lodashEscape, sortBy, template } from 'lodash';
import * as Emoji from '~/emoji';
import axios from '~/lib/utils/axios_utils';
import { s__, __, sprintf } from '~/locale';
@@ -11,8 +11,21 @@ import { spriteIcon } from './lib/utils/common_utils';
import { parsePikadayDate } from './lib/utils/datetime_utility';
import glRegexp from './lib/utils/regexp';
-function sanitize(str) {
- return str.replace(/<(?:.|\n)*?>/gm, '');
+/**
+ * Escapes user input before we pass it to at.js, which
+ * renders it as HTML in the autocomplete dropdown.
+ *
+ * at.js allows you to reference data using `${}` syntax
+ * (e.g. ${search}) which it replaces with the actual data
+ * before rendering it in the autocomplete dropdown.
+ * To prevent user input from executing this `${}` syntax,
+ * we also need to escape the $ character.
+ *
+ * @param string user input
+ * @return {string} escaped user input
+ */
+function escape(string) {
+ return lodashEscape(string).replace(/\$/g, '&dollar;');
}
function createMemberSearchString(member) {
@@ -44,8 +57,8 @@ export function membersBeforeSave(members) {
return {
username: member.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
- title: sanitize(title),
- search: sanitize(createMemberSearchString(member)),
+ title,
+ search: createMemberSearchString(member),
icon: avatarIcon,
availability: member?.availability,
};
@@ -366,7 +379,7 @@ class GfmAutoComplete {
}
return {
id: i.iid,
- title: sanitize(i.title),
+ title: i.title,
reference: i.reference,
search: `${i.iid} ${i.title}`,
};
@@ -404,7 +417,7 @@ class GfmAutoComplete {
return {
id: m.iid,
- title: sanitize(m.title),
+ title: m.title,
search: m.title,
expired,
dueDate,
@@ -456,7 +469,7 @@ class GfmAutoComplete {
}
return {
id: m.iid,
- title: sanitize(m.title),
+ title: m.title,
reference: m.reference,
search: `${m.iid} ${m.title}`,
};
@@ -492,7 +505,7 @@ class GfmAutoComplete {
beforeSave(merges) {
if (GfmAutoComplete.isLoading(merges)) return merges;
return $.map(merges, (m) => ({
- title: sanitize(m.title),
+ title: m.title,
color: m.color,
search: m.title,
set: m.set,
@@ -586,7 +599,7 @@ class GfmAutoComplete {
}
return {
id: m.id,
- title: sanitize(m.title),
+ title: m.title,
search: `${m.id} ${m.title}`,
};
});
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 50835333a54..f6f136f2402 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -1,5 +1,5 @@
import { mount2faRegistration } from '~/authentication/mount_2fa';
-import { initRecoveryCodes } from '~/authentication/two_factor_auth';
+import { initRecoveryCodes, initManageTwoFactorForm } from '~/authentication/two_factor_auth';
import { parseBoolean } from '~/lib/utils/common_utils';
const twoFactorNode = document.querySelector('.js-two-factor-auth');
@@ -14,3 +14,5 @@ if (skippable) {
mount2faRegistration();
initRecoveryCodes();
+
+initManageTwoFactorForm();
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 69b3c27173f..8ed92e6b948 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -842,7 +842,7 @@ UsersSelect.prototype.renderApprovalRules = function (elsClassName, approvalRule
const [rule] = approvalRules;
const countText = sprintf(__('(+%{count}&nbsp;rules)'), { count });
const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : '';
- const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name;
+ const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : escape(rule.name);
return `<div class="gl-display-flex gl-font-sm">
<span class="gl-text-truncate" title="${ruleName}">${ruleName}</span>
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 9c556d16913..cdfb3a32f4c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -45,10 +45,11 @@ class Admin::UsersController < Admin::ApplicationController
end
def impersonate
- if can?(user, :log_in)
+ if can?(user, :log_in) && !impersonation_in_progress?
session[:impersonator_id] = current_user.id
warden.set_user(user, scope: :user)
+ clear_access_token_session_keys!
log_impersonation_event
@@ -57,7 +58,9 @@ class Admin::UsersController < Admin::ApplicationController
redirect_to root_path
else
flash[:alert] =
- if user.blocked?
+ if impersonation_in_progress?
+ _("You are already impersonating another user")
+ elsif user.blocked?
_("You cannot impersonate a blocked user")
elsif user.internal?
_("You cannot impersonate an internal user")
diff --git a/app/controllers/concerns/impersonation.rb b/app/controllers/concerns/impersonation.rb
index a4f2c263eb4..539dd9ad69d 100644
--- a/app/controllers/concerns/impersonation.rb
+++ b/app/controllers/concerns/impersonation.rb
@@ -3,6 +3,12 @@
module Impersonation
include Gitlab::Utils::StrongMemoize
+ SESSION_KEYS_TO_DELETE = %w(
+ github_access_token gitea_access_token gitlab_access_token
+ bitbucket_token bitbucket_refresh_token bitbucket_server_personal_access_token
+ bulk_import_gitlab_access_token fogbugz_token
+ ).freeze
+
def current_user
user = super
@@ -14,7 +20,7 @@ module Impersonation
protected
def check_impersonation_availability
- return unless session[:impersonator_id]
+ return unless impersonation_in_progress?
unless Gitlab.config.gitlab.impersonation_enabled
stop_impersonation
@@ -27,14 +33,25 @@ module Impersonation
warden.set_user(impersonator, scope: :user)
session[:impersonator_id] = nil
+ clear_access_token_session_keys!
current_user
end
+ def impersonation_in_progress?
+ session[:impersonator_id].present?
+ end
+
def log_impersonation_event
Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{current_user.username}")
end
+ def clear_access_token_session_keys!
+ access_tokens_keys = session.keys & SESSION_KEYS_TO_DELETE
+
+ access_tokens_keys.each { |key| session.delete(key) }
+ end
+
def impersonator
strong_memoize(:impersonator) do
User.find(session[:impersonator_id]) if session[:impersonator_id]
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 5a4eef352b8..32c9da67e90 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -66,11 +66,13 @@ class Import::GiteaController < Import::GithubController
override :client_options
def client_options
- { host: provider_url, api_version: 'v1' }
+ verified_url, provider_hostname = verify_blocked_uri
+
+ { host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s, api_version: 'v1', hostname: provider_hostname }
end
def verify_blocked_uri
- Gitlab::UrlBlocker.validate!(
+ @verified_url_and_hostname ||= Gitlab::UrlBlocker.validate!(
provider_url,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index 85e901eb3eb..c8c2dd1c7d6 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -47,6 +47,8 @@ class Profiles::PasswordsController < Profiles::ApplicationController
password_attributes[:password_automatically_set] = false
unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
+ handle_invalid_current_password_attempt!
+
redirect_to edit_profile_password_path, alert: _('You must provide a valid current password')
return
end
@@ -85,6 +87,12 @@ class Profiles::PasswordsController < Profiles::ApplicationController
render_404 unless @user.allow_password_authentication?
end
+ def handle_invalid_current_password_attempt!
+ Gitlab::AppLogger.info(message: 'Invalid current password when attempting to update user password', username: @user.username, ip: request.remote_ip)
+
+ @user.increment_failed_attempts!
+ end
+
def user_params
params.require(:user).permit(:current_password, :password, :password_confirmation)
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 5eb46421583..d1b9485f06d 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -3,6 +3,8 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_two_factor_requirement
before_action :ensure_verified_primary_email, only: [:show, :create]
+ before_action :validate_current_password, only: [:create, :codes, :destroy]
+
before_action do
push_frontend_feature_flag(:webauthn)
end
@@ -134,6 +136,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
+ def validate_current_password
+ return if current_user.valid_password?(params[:current_password])
+
+ current_user.increment_failed_attempts!
+
+ redirect_to profile_two_factor_auth_path, alert: _('You must provide a valid current password')
+ end
+
def build_qr_code
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3)
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d0987492d2d..b979276437c 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -34,13 +34,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def import
- @projects = current_user.authorized_projects.order_id_desc
+ @projects = Project.visible_to_user_and_access_level(current_user, Gitlab::Access::MAINTAINER).order_id_desc
end
def apply_import
source_project = Project.find(params[:source_project_id])
- if can?(current_user, :read_project_member, source_project)
+ if can?(current_user, :admin_project_member, source_project)
status = @project.team.import(source_project, current_user)
notice = status ? "Successfully imported" : "Import failed"
else
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index d7c1d87ae4b..de4e51a3a2f 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -19,6 +19,7 @@ class ProjectsController < Projects::ApplicationController
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create, :resolve]
before_action :repository, except: [:index, :new, :create, :resolve]
+ before_action :verify_git_import_enabled, only: [:create]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
before_action :authorize_download_code!, only: [:refs]
@@ -495,6 +496,10 @@ class ProjectsController < Projects::ApplicationController
url_for(safe_params)
end
+ def verify_git_import_enabled
+ render_404 if project_params[:import_url] && !git_import_enabled?
+ end
+
def project_export_enabled
render_404 unless Gitlab::CurrentSettings.project_export_enabled?
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 4077a3d3dac..d040ac7f76c 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -36,14 +36,10 @@ class UploadsController < ApplicationController
end
def find_model
- return unless params[:id]
-
upload_model_class.find(params[:id])
end
def authorize_access!
- return unless model
-
authorized =
case model
when Note
@@ -68,8 +64,6 @@ class UploadsController < ApplicationController
end
def authorize_create_access!
- return unless model
-
authorized =
case model
when User
diff --git a/app/graphql/types/group_invitation_type.rb b/app/graphql/types/group_invitation_type.rb
index 06a997bbc14..9410253553c 100644
--- a/app/graphql/types/group_invitation_type.rb
+++ b/app/graphql/types/group_invitation_type.rb
@@ -3,7 +3,7 @@
module Types
class GroupInvitationType < BaseObject
expose_permissions Types::PermissionTypes::Group
- authorize :read_group
+ authorize :admin_group
implements InvitationInterface
diff --git a/app/graphql/types/project_invitation_type.rb b/app/graphql/types/project_invitation_type.rb
index 507dc2d59c3..b76f05e289f 100644
--- a/app/graphql/types/project_invitation_type.rb
+++ b/app/graphql/types/project_invitation_type.rb
@@ -9,7 +9,7 @@ module Types
implements InvitationInterface
- authorize :read_project
+ authorize :admin_project
field :project, Types::ProjectType, null: true,
description: 'Project ID for the project of the invitation.'
diff --git a/app/helpers/external_link_helper.rb b/app/helpers/external_link_helper.rb
index 058302d1ed8..c951d0daf96 100644
--- a/app/helpers/external_link_helper.rb
+++ b/app/helpers/external_link_helper.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
module ExternalLinkHelper
+ include ActionView::Helpers::TextHelper
+
def external_link(body, url, options = {})
- link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
+ link = link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
"#{body}#{sprite_icon('external-link', css_class: 'gl-ml-1')}".html_safe
end
+ sanitize(link, tags: %w(a svg use), attributes: %w(target rel data-testid class href).concat(options.stringify_keys.keys))
end
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 096a3f2269b..c38b4a7aedf 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -44,7 +44,7 @@ module IconsHelper
content_tag(
:svg,
- content_tag(:use, '', { 'xlink:href' => "#{sprite_icon_path}##{icon_name}" } ),
+ content_tag(:use, '', { 'href' => "#{sprite_icon_path}##{icon_name}" } ),
class: css_classes.empty? ? nil : css_classes.join(' '),
data: { testid: "#{icon_name}-icon" }
)
diff --git a/app/models/user.rb b/app/models/user.rb
index b5f0251f639..a4c8d606911 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1883,7 +1883,8 @@ class User < ApplicationRecord
def password_expired_if_applicable?
return false if bot?
- return false unless password_expired? && password_automatically_set?
+ return false unless password_expired?
+ return false if password_automatically_set?
return false unless allow_password_authentication?
true
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index e27ea7c07e5..afa8de04fca 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -117,6 +117,7 @@ module Projects
trash_relation_repositories!
trash_project_repositories!
destroy_web_hooks!
+ destroy_project_bots!
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
@@ -149,6 +150,16 @@ module Projects
end
end
+ # The project can have multiple project bots with personal access tokens generated.
+ # We need to remove them when a project is deleted
+ # rubocop: disable CodeReuse/ActiveRecord
+ def destroy_project_bots!
+ project.members.includes(:user).references(:user).merge(User.project_bot).each do |member|
+ Users::DestroyService.new(current_user).execute(member.user, skip_authorization: true)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def remove_registry_tags
return true unless Gitlab.config.registry.enabled
return false unless remove_legacy_registry_tags
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 927b6d4edef..d1d6b6301b8 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -17,13 +17,7 @@
= _("You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.")
%p
= _('If you lose your recovery codes you can generate new ones, invalidating all previous codes.')
- %div
- = link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
- method: :delete,
- data: { confirm: webauthn_enabled ? _('Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.') : _('Are you sure? This will invalidate your registered applications and U2F devices.') },
- class: 'gl-button btn btn-danger gl-mr-3'
- = form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
- = submit_tag _('Regenerate recovery codes'), class: 'gl-button btn btn-default'
+ .js-manage-two-factor-form{ data: { webauthn_enabled: webauthn_enabled, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
- else
%p
@@ -53,6 +47,11 @@
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
+ .form-group
+ = label_tag :current_password, _('Current password'), class: 'label-bold'
+ = password_field_tag :current_password, nil, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
+ %p.form-text.text-muted
+ = _('Your current password is required to register a two-factor authenticator app.')
.gl-mt-3
= submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' }
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index eb4630b84d5..97f5cdb54e5 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -16,6 +16,7 @@
%li= _('Job logs and artifacts')
%li= _('Container registry images')
%li= _('CI variables')
+ %li= _('Pipeline triggers')
%li= _('Webhooks')
%li= _('Any encrypted tokens')
- if project.export_status == :finished
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 477d419576a..25bf164c96a 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -51,6 +51,11 @@ Doorkeeper.configure do
# Issue access tokens with refresh token (disabled by default)
use_refresh_token
+ # Forbids creating/updating applications with arbitrary scopes that are
+ # not in configuration, i.e. `default_scopes` or `optional_scopes`.
+ # (disabled by default)
+ enforce_configured_scopes
+
# Forces the usage of the HTTPS protocol in non-native redirect uris (enabled
# by default in non-development environments). OAuth2 delegates security in
# communication to the HTTPS protocol so it is wise to keep this enabled.
diff --git a/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
index 760fcba5935..1ede92609a9 100644
--- a/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
+++ b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
@@ -1,14 +1,46 @@
# frozen_string_literal: true
+# See https://github.com/omniauth/omniauth-oauth2/blob/v1.7.1/lib/omniauth/strategies/oauth2.rb#L84-L101
+# for the original version of this code.
+#
+# Note: We need to override `callback_phase` directly (instead of using a module with `include` or `prepend`),
+# because the method has a `super` call which needs to go to the `OmniAuth::Strategy` module,
+# and it also deletes `omniauth.state` from the session as a side effect.
+
module OmniAuth
module Strategies
class OAuth2
- alias_method :original_callback_phase, :callback_phase
-
- # Monkey patch until PR is merged and released upstream
- # https://github.com/omniauth/omniauth-oauth2/pull/129
def callback_phase
- original_callback_phase
+ error = request.params["error_reason"].presence || request.params["error"].presence
+ # Monkey patch #1:
+ #
+ # Swap the order of these conditions around so the `state` param is verified *first*,
+ # before using the error params returned by the provider.
+ #
+ # This avoids content spoofing attacks by crafting a URL with malicious messages,
+ # because the `state` param is only present in the session after a valid OAuth2 authentication flow.
+ if !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state"))
+ fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
+ elsif error
+ fail!(error, CallbackError.new(request.params["error"], request.params["error_description"].presence || request.params["error_reason"].presence, request.params["error_uri"]))
+ else
+ self.access_token = build_access_token
+ self.access_token = access_token.refresh! if access_token.expired?
+ super
+ end
+ rescue ::OAuth2::Error, CallbackError => e
+ fail!(:invalid_credentials, e)
+ rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
+ fail!(:timeout, e)
+ rescue ::SocketError => e
+ fail!(:failed_to_connect, e)
+ # Monkey patch #2:
+ #
+ # Also catch errors from Faraday.
+ # See https://github.com/omniauth/omniauth-oauth2/pull/129
+ # and https://github.com/oauth-xx/oauth2/issues/152
+ #
+ # This can be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/340933
rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed => e
fail!(:timeout, e)
end
diff --git a/db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb b/db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb
new file mode 100644
index 00000000000..4756bc3dca5
--- /dev/null
+++ b/db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class CleanupOrphanProjectAccessTokens < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ TMP_INDEX_NAME = 'idx_users_on_user_type_project_bots_batched'
+
+ def up
+ users_table = define_batchable_model('users')
+
+ add_concurrent_index(:users, :id, name: TMP_INDEX_NAME, where: 'user_type = 6')
+
+ accumulated_orphans = []
+ users_table.where(user_type: 6).each_batch(of: 500) do |relation|
+ orphan_ids = relation.where("not exists(select 1 from members where members.user_id = users.id)").pluck(:id)
+
+ orphan_ids.each_slice(10) do |ids|
+ users_table.where(id: ids).update_all(state: 'deactivated')
+ end
+
+ accumulated_orphans += orphan_ids
+ end
+
+ schedule_deletion(accumulated_orphans)
+ ensure
+ remove_concurrent_index_by_name(:users, TMP_INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(:users, TMP_INDEX_NAME) if index_exists_by_name?(:users, TMP_INDEX_NAME)
+ end
+
+ private
+
+ def schedule_deletion(orphan_ids)
+ return unless deletion_worker
+
+ orphan_ids.each_slice(100) do |ids|
+ job_arguments = ids.map do |orphan_id|
+ [orphan_id, orphan_id, { skip_authorization: true }]
+ end
+
+ deletion_worker.bulk_perform_async(job_arguments)
+ end
+ rescue StandardError
+ # Ignore any errors or interface changes since this part of migration is optional
+ end
+
+ def deletion_worker
+ @deletion_worker = "DeleteUserWorker".safe_constantize unless defined?(@deletion_worker)
+
+ @deletion_worker
+ end
+end
diff --git a/db/schema_migrations/20210914095310 b/db/schema_migrations/20210914095310
new file mode 100644
index 00000000000..fee7e0b9719
--- /dev/null
+++ b/db/schema_migrations/20210914095310
@@ -0,0 +1 @@
+6fcf3ff9867df68f5e9603ae0311b29bec33aa5c5b826786b094ab0960ebcd90 \ No newline at end of file
diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md
index c8b928ab5b2..6e9c37980ac 100644
--- a/doc/api/dependencies.md
+++ b/doc/api/dependencies.md
@@ -11,6 +11,9 @@ This API is in an alpha stage and considered unstable.
The response payload may be subject to change or breakage
across GitLab releases.
+> - Introduced in GitLab 12.1.
+> - Pagination introduced in 14.4.
+
Every call to this endpoint requires authentication. To perform this call, user should be authorized to read repository.
To see vulnerabilities in response, user should be authorized to read
[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard).
@@ -60,3 +63,10 @@ Example response:
}
]
```
+
+## Dependencies pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](index.md#pagination).
diff --git a/doc/api/users.md b/doc/api/users.md
index dfd2f6cc87d..4ec609e62e9 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -259,7 +259,7 @@ GET /users?with_custom_attributes=true
## Single user
-Get a single user. This endpoint can be accessed without authentication.
+Get a single user.
### For user
@@ -806,7 +806,7 @@ Example response:
### Followers and following
-Get the followers of a user. This endpoint can be accessed without authentication.
+Get the followers of a user.
```plaintext
GET /users/:id/followers
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index 14e6f4dad3a..44537707db6 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -75,6 +75,7 @@ To enable 2FA:
1. **In GitLab:**
1. Enter the six-digit pin number from the entry on your device into the **Pin
code** field.
+ 1. Enter your current password.
1. Select **Submit**.
If the pin you entered was correct, a message displays indicating that
@@ -365,7 +366,8 @@ If you ever need to disable 2FA:
1. Sign in to your GitLab account.
1. Go to your [**User settings**](../index.md#access-your-user-settings).
1. Go to **Account**.
-1. Click **Disable**, under **Two-Factor Authentication**.
+1. Select **Manage two-factor authentication**.
+1. Under **Two-Factor Authentication**, enter your current password and select **Disable**.
This clears all your two-factor authentication registrations, including mobile
applications and U2F / WebAuthn devices.
@@ -460,7 +462,7 @@ To regenerate 2FA recovery codes, you need access to a desktop browser:
1. Go to your [**User settings**](../index.md#access-your-user-settings).
1. Select **Account > Two-Factor Authentication (2FA)**.
1. If you've already configured 2FA, click **Manage two-factor authentication**.
-1. In the **Register Two-Factor Authenticator** pane, click **Regenerate recovery codes**.
+1. In the **Register Two-Factor Authenticator** pane, enter your current password and select **Regenerate recovery codes**.
NOTE:
If you regenerate 2FA recovery codes, save them. You can't use any previously created 2FA codes.
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 662d7e70910..69215e03f28 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -135,9 +135,11 @@ The following items are **not** exported:
- Build traces and artifacts
- Container registry images
- CI/CD variables
+- Pipeline triggers
- Webhooks
- Any encrypted tokens
- Merge Request Approvers
+- Repository size limits
NOTE:
For more details on the specific data persisted in a project export, see the
@@ -255,13 +257,13 @@ reduce the repository size for another import attempt.
git reflog expire --expire=now --all
git gc --prune=now --aggressive
- # Prepare recreating an importable file
+ # Prepare recreating an importable file
git bundle create ../project.bundle smaller-tmp-main
cd ..
mv project/ ../"$EXPORT"-project
cd ..
- # Recreate an importable file
+ # Recreate an importable file
tar -czf "$EXPORT"-smaller.tar.gz --directory="$EXPORT"/ .
```
diff --git a/lib/api/import_bitbucket_server.rb b/lib/api/import_bitbucket_server.rb
index ecd78c6e6db..0f0d62dcbfb 100644
--- a/lib/api/import_bitbucket_server.rb
+++ b/lib/api/import_bitbucket_server.rb
@@ -4,6 +4,10 @@ module API
class ImportBitbucketServer < ::API::Base
feature_category :importers
+ before do
+ forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('bitbucket_server')
+ end
+
helpers do
def client
@client ||= BitbucketServer::Client.new(credentials)
diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb
index 1f437ad5bd3..5cade301d81 100644
--- a/lib/api/invitations.rb
+++ b/lib/api/invitations.rb
@@ -46,6 +46,8 @@ module API
source = find_source(source_type, params[:id])
query = params[:query]
+ authorize_admin_source!(source_type, source)
+
invitations = paginate(retrieve_member_invitations(source, query))
present_member_invitations invitations
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a92d904be84..34e0b528ced 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -89,6 +89,10 @@ module API
Gitlab::AppLogger.info({ message: "File exceeds maximum size", file_bytes: file.size, project_id: user_project.id, project_path: user_project.full_path, upload_allowed: allowed })
end
end
+
+ def check_import_by_url_is_enabled
+ forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('git')
+ end
end
helpers do
@@ -267,6 +271,7 @@ module API
attrs = declared_params(include_missing: false)
attrs = translate_params_for_compatibility(attrs)
filter_attributes_using_license!(attrs)
+ check_import_by_url_is_enabled if params[:import_url].present?
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e3271b8b9b2..944be990c2f 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -140,7 +140,10 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id", feature_category: :users do
+ forbidden!('Not authorized!') unless current_user
+
user = User.find_by(id: params[:id])
+
not_found!('User') unless user && can?(current_user, :read_user, user)
opts = { with: current_user&.admin? ? Entities::UserDetailsWithAdmin : Entities::User, current_user: current_user }
@@ -156,6 +159,7 @@ module API
end
get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users do
user = find_user(params[:user_id])
+
not_found!('User') unless user && can?(current_user, :read_user, user)
present user.status || {}, with: Entities::UserStatus
@@ -203,6 +207,8 @@ module API
use :pagination
end
get ':id/following', feature_category: :users do
+ forbidden!('Not authorized!') unless current_user
+
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
@@ -217,6 +223,8 @@ module API
use :pagination
end
get ':id/followers', feature_category: :users do
+ forbidden!('Not authorized!') unless current_user
+
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index ca26e6d1581..f8d03fd6e50 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -26,14 +26,17 @@ module Banzai
# Pattern to match a standard markdown link
#
# Rubular: http://rubular.com/r/2EXEQ49rg5
- LINK_OR_IMAGE_PATTERN = %r{
- (?<preview_operator>!)?
- \[(?<text>.+?)\]
- \(
- (?<new_link>.+?)
- (?<title>\ ".+?")?
- \)
- }x.freeze
+ #
+ # This pattern is vulnerable to malicious inputs, so use Gitlab::UntrustedRegexp
+ # to place bounds on execution time
+ LINK_OR_IMAGE_PATTERN = Gitlab::UntrustedRegexp.new(
+ '(?P<preview_operator>!)?' \
+ '\[(?P<text>.+?)\]' \
+ '\(' \
+ '(?P<new_link>.+?)' \
+ '(?P<title>\ ".+?")?' \
+ '\)'
+ )
# Text matching LINK_OR_IMAGE_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
@@ -48,7 +51,7 @@ module Banzai
doc.xpath(TEXT_QUERY).each do |node|
content = node.to_html
- next unless content.match(LINK_OR_IMAGE_PATTERN)
+ next unless LINK_OR_IMAGE_PATTERN.match(content)
html = spaced_link_filter(content)
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 1afb2eda149..0970b92723b 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -172,7 +172,11 @@ module Gitlab
user = find_with_user_password(login, password)
return unless user
- raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
+ verifier = TwoFactorAuthVerifier.new(user)
+
+ if user.two_factor_enabled? || verifier.two_factor_authentication_enforced?
+ raise Gitlab::Auth::MissingPersonalAccessTokenError
+ end
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index f6ee08defcf..9c33a5fc872 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -342,6 +342,10 @@ module Gitlab
Gitlab::PathRegex.repository_git_lfs_route_regex.match?(current_request.path)
end
+ def git_or_lfs_request?
+ git_request? || git_lfs_request?
+ end
+
def archive_request?
current_request.path.include?('/-/archive/')
end
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index dfc682e8a5c..08214bbd449 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -35,13 +35,31 @@ module Gitlab
find_user_from_static_object_token(request_format) ||
find_user_from_basic_auth_job ||
find_user_from_job_token ||
- find_user_from_lfs_token ||
- find_user_from_personal_access_token ||
- find_user_from_basic_auth_password
+ find_user_from_personal_access_token_for_api_or_git ||
+ find_user_for_git_or_lfs_request
rescue Gitlab::Auth::AuthenticationError
nil
end
+ # To prevent Rack Attack from incorrectly rate limiting
+ # authenticated Git activity, we need to authenticate the user
+ # from other means (e.g. HTTP Basic Authentication) only if the
+ # request originated from a Git or Git LFS
+ # request. Repositories::GitHttpClientController or
+ # Repositories::LfsApiController normally does the authentication,
+ # but Rack Attack runs before those controllers.
+ def find_user_for_git_or_lfs_request
+ return unless git_or_lfs_request?
+
+ find_user_from_lfs_token || find_user_from_basic_auth_password
+ end
+
+ def find_user_from_personal_access_token_for_api_or_git
+ return unless api_request? || git_or_lfs_request?
+
+ find_user_from_personal_access_token
+ end
+
def valid_access_token?(scopes: [])
validate_access_token!(scopes: scopes)
diff --git a/lib/gitlab/auth/two_factor_auth_verifier.rb b/lib/gitlab/auth/two_factor_auth_verifier.rb
index 86552ef1267..5a203a1fe9c 100644
--- a/lib/gitlab/auth/two_factor_auth_verifier.rb
+++ b/lib/gitlab/auth/two_factor_auth_verifier.rb
@@ -9,6 +9,10 @@ module Gitlab
@current_user = current_user
end
+ def two_factor_authentication_enforced?
+ two_factor_authentication_required? && two_factor_grace_period_expired?
+ end
+
def two_factor_authentication_required?
Gitlab::CurrentSettings.require_two_factor_authentication? ||
current_user&.require_two_factor_authentication_from_group?
diff --git a/lib/gitlab/fogbugz_import.rb b/lib/gitlab/fogbugz_import.rb
new file mode 100644
index 00000000000..a4a52edd83e
--- /dev/null
+++ b/lib/gitlab/fogbugz_import.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'fogbugz'
+
+module Gitlab
+ module FogbugzImport
+ # Custom adapter to validate the URL before each request
+ # This way we avoid DNS rebinds or other unsafe requests
+ ::Fogbugz.adapter[:http] = HttpAdapter
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
index dd747a79673..024c1ae0439 100644
--- a/lib/gitlab/fogbugz_import/client.rb
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'fogbugz'
-
module Gitlab
module FogbugzImport
class Client
diff --git a/lib/gitlab/fogbugz_import/http_adapter.rb b/lib/gitlab/fogbugz_import/http_adapter.rb
new file mode 100644
index 00000000000..bfae7a10f5b
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/http_adapter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module FogbugzImport
+ class HttpAdapter
+ def initialize(options = {})
+ @root_url = options[:uri]
+ end
+
+ def request(action, options = {})
+ uri = Gitlab::Utils.append_path(@root_url, 'api.asp')
+
+ params = { 'cmd' => action }.merge(options.fetch(:params, {}))
+
+ response = Gitlab::HTTP.post(uri, body: params)
+
+ response.body
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
index 630f918a78b..f7ab1677001 100644
--- a/lib/gitlab/import_export/group/import_export.yml
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -37,6 +37,7 @@ excluded_attributes:
- :trial_ends_on
- :shared_runners_minute_limit
- :extra_shared_runners_minutes_limit
+ - :repository_size_limit
epics:
- :state_id
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index fe0974d27a6..8046fedc4f3 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -88,7 +88,6 @@ tree:
- :external_pull_request
- :merge_request
- :auto_devops
- - :triggers
- :pipeline_schedules
- :container_expiration_policy
- protected_branches:
@@ -211,6 +210,7 @@ excluded_attributes:
- :show_default_award_emojis
- :services
- :exported_protected_branches
+ - :repository_size_limit
namespaces:
- :runners_token
- :runners_token_encrypted
diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb
index 4482610523e..48a8e0ce6d7 100644
--- a/lib/gitlab/legacy_github_import/client.rb
+++ b/lib/gitlab/legacy_github_import/client.rb
@@ -8,9 +8,10 @@ module Gitlab
attr_reader :access_token, :host, :api_version, :wait_for_rate_limit_reset
- def initialize(access_token, host: nil, api_version: 'v3', wait_for_rate_limit_reset: true)
+ def initialize(access_token, host: nil, api_version: 'v3', wait_for_rate_limit_reset: true, hostname: nil)
@access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '')
+ @hostname = hostname
@api_version = api_version
@users = {}
@wait_for_rate_limit_reset = wait_for_rate_limit_reset
@@ -28,7 +29,8 @@ module Gitlab
# If there is no config, we're connecting to github.com and we
# should verify ssl.
connection_options: {
- ssl: { verify: config ? config['verify_ssl'] : true }
+ ssl: { verify: config ? config['verify_ssl'] : true },
+ headers: { host: @hostname }.compact
}
)
end
diff --git a/lib/gitlab/string_regex_marker.rb b/lib/gitlab/string_regex_marker.rb
index f1982ff914c..8e0167a433e 100644
--- a/lib/gitlab/string_regex_marker.rb
+++ b/lib/gitlab/string_regex_marker.rb
@@ -2,18 +2,20 @@
module Gitlab
class StringRegexMarker < StringRangeMarker
- # rubocop: disable CodeReuse/ActiveRecord
def mark(regex, group: 0, &block)
ranges = []
+ offset = 0
- raw_line.scan(regex) do
- begin_index, end_index = Regexp.last_match.offset(group)
+ while match = regex.match(raw_line[offset..])
+ begin_index = match.begin(group) + offset
+ end_index = match.end(group) + offset
ranges << (begin_index..(end_index - 1))
+
+ offset = end_index
end
super(ranges, &block)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ae3fbe248bb..03a26c01cd5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -38380,6 +38380,9 @@ msgstr ""
msgid "You are already a member of this %{member_source}."
msgstr ""
+msgid "You are already impersonating another user"
+msgstr ""
+
msgid "You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
@@ -39091,6 +39094,9 @@ msgstr ""
msgid "Your commit email is used for web based operations, such as edits and merges."
msgstr ""
+msgid "Your current password is required to register a two-factor authenticator app."
+msgstr ""
+
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr ""
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index 744c0712d6b..ccf4454c349 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -92,6 +92,14 @@ RSpec.describe Admin::ImpersonationsController do
expect(warden.user).to eq(impersonator)
end
+
+ it 'clears token session keys' do
+ session[:bitbucket_token] = SecureRandom.hex(8)
+
+ delete :destroy
+
+ expect(session[:bitbucket_token]).to be_nil
+ end
end
# base case
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 015c36c9335..3a2b5dcb99d 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -794,6 +794,14 @@ RSpec.describe Admin::UsersController do
expect(flash[:alert]).to eq("You are now impersonating #{user.username}")
end
+
+ it 'clears token session keys' do
+ session[:github_access_token] = SecureRandom.hex(8)
+
+ post :impersonate, params: { id: user.username }
+
+ expect(session[:github_access_token]).to be_nil
+ end
end
context "when impersonation is disabled" do
@@ -807,5 +815,20 @@ RSpec.describe Admin::UsersController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when impersonating an admin and attempting to impersonate again' do
+ let(:admin2) { create(:admin) }
+
+ before do
+ post :impersonate, params: { id: admin2.username }
+ end
+
+ it 'does not allow double impersonation', :aggregate_failures do
+ post :impersonate, params: { id: user.username }
+
+ expect(flash[:alert]).to eq(_('You are already impersonating another user'))
+ expect(warden.user).to eq(admin2)
+ end
+ end
end
end
diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb
index 3e4b159271a..568712d29cb 100644
--- a/spec/controllers/import/gitea_controller_spec.rb
+++ b/spec/controllers/import/gitea_controller_spec.rb
@@ -54,6 +54,48 @@ RSpec.describe Import::GiteaController do
end
end
end
+
+ context 'when DNS Rebinding protection is enabled' do
+ let(:token) { 'gitea token' }
+
+ let(:ip_uri) { 'http://167.99.148.217' }
+ let(:uri) { 'try.gitea.io' }
+ let(:https_uri) { "https://#{uri}" }
+ let(:http_uri) { "http://#{uri}" }
+
+ before do
+ session[:gitea_access_token] = token
+
+ allow(Gitlab::UrlBlocker).to receive(:validate!).with(https_uri, anything).and_return([Addressable::URI.parse(https_uri), uri])
+ allow(Gitlab::UrlBlocker).to receive(:validate!).with(http_uri, anything).and_return([Addressable::URI.parse(ip_uri), uri])
+
+ allow(Gitlab::LegacyGithubImport::Client).to receive(:new).and_return(double('Gitlab::LegacyGithubImport::Client', repos: [], orgs: []))
+ end
+
+ context 'when provided host url is using https' do
+ let(:host_url) { https_uri }
+
+ it 'uses unchanged host url to send request to Gitea' do
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: https_uri, api_version: 'v1', hostname: 'try.gitea.io')
+
+ get :status, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when provided host url is using http' do
+ let(:host_url) { http_uri }
+
+ it 'uses changed host url to send request to Gitea' do
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: 'http://167.99.148.217', api_version: 'v1', hostname: 'try.gitea.io')
+
+ get :status, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index f21ef324884..5bf3b4c48bf 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -98,6 +98,19 @@ RSpec.describe Oauth::ApplicationsController do
end
describe 'POST #create' do
+ let(:oauth_params) do
+ {
+ doorkeeper_application: {
+ name: 'foo',
+ redirect_uri: redirect_uri,
+ scopes: scopes
+ }
+ }
+ end
+
+ let(:redirect_uri) { 'http://example.org' }
+ let(:scopes) { ['api'] }
+
subject { post :create, params: oauth_params }
it 'creates an application' do
@@ -116,38 +129,42 @@ RSpec.describe Oauth::ApplicationsController do
expect(response).to redirect_to(profile_path)
end
- context 'redirect_uri' do
+ context 'when redirect_uri is invalid' do
+ let(:redirect_uri) { 'javascript://alert()' }
+
render_views
it 'shows an error for a forbidden URI' do
- invalid_uri_params = {
- doorkeeper_application: {
- name: 'foo',
- redirect_uri: 'javascript://alert()',
- scopes: ['api']
- }
- }
-
- post :create, params: invalid_uri_params
+ subject
expect(response.body).to include 'Redirect URI is forbidden by the server'
+ expect(response).to render_template('doorkeeper/applications/index')
end
end
context 'when scopes are not present' do
+ let(:scopes) { [] }
+
render_views
it 'shows an error for blank scopes' do
- invalid_uri_params = {
- doorkeeper_application: {
- name: 'foo',
- redirect_uri: 'http://example.org'
- }
- }
-
- post :create, params: invalid_uri_params
+ subject
expect(response.body).to include 'Scopes can&#39;t be blank'
+ expect(response).to render_template('doorkeeper/applications/index')
+ end
+ end
+
+ context 'when scopes are invalid' do
+ let(:scopes) { %w(api foo) }
+
+ render_views
+
+ it 'shows an error for invalid scopes' do
+ subject
+
+ expect(response.body).to include 'Scopes doesn&#39;t match configured on the server.'
+ expect(response).to render_template('doorkeeper/applications/index')
end
end
@@ -185,14 +202,4 @@ RSpec.describe Oauth::ApplicationsController do
def disable_user_oauth
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
end
-
- def oauth_params
- {
- doorkeeper_application: {
- name: 'foo',
- redirect_uri: 'http://example.org',
- scopes: ['api']
- }
- }
- end
end
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 073180cbafd..a0e2cf671af 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -35,6 +35,27 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
end
+ shared_examples 'user must enter a valid current password' do
+ let(:current_password) { '123' }
+
+ it 'requires the current password', :aggregate_failures do
+ go
+
+ expect(response).to redirect_to(profile_two_factor_auth_path)
+ expect(flash[:alert]).to eq(_('You must provide a valid current password'))
+ end
+
+ context 'when the user is on the last sign in attempt' do
+ it do
+ user.update!(failed_attempts: User.maximum_attempts.pred)
+
+ go
+
+ expect(user.reload).to be_access_locked
+ end
+ end
+ end
+
describe 'GET show' do
let_it_be_with_reload(:user) { create(:user) }
@@ -69,9 +90,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do
let_it_be_with_reload(:user) { create(:user) }
let(:pin) { 'pin-code' }
+ let(:current_password) { user.password }
def go
- post :create, params: { pin_code: pin }
+ post :create, params: { pin_code: pin, current_password: current_password }
end
context 'with valid pin' do
@@ -136,21 +158,25 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
end
+ it_behaves_like 'user must enter a valid current password'
+
it_behaves_like 'user must first verify their primary email address'
end
describe 'POST codes' do
let_it_be_with_reload(:user) { create(:user, :two_factor) }
+ let(:current_password) { user.password }
+
it 'presents plaintext codes for the user to save' do
expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c))
- post :codes
+ post :codes, params: { current_password: current_password }
expect(assigns[:codes]).to match_array %w(a b c)
end
it 'persists the generated codes' do
- post :codes
+ post :codes, params: { current_password: current_password }
user.reload
expect(user.otp_backup_codes).not_to be_empty
@@ -159,12 +185,18 @@ RSpec.describe Profiles::TwoFactorAuthsController do
it 'dismisses the `TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK` callout' do
expect(controller.helpers).to receive(:dismiss_two_factor_auth_recovery_settings_check)
- post :codes
+ post :codes, params: { current_password: current_password }
+ end
+
+ it_behaves_like 'user must enter a valid current password' do
+ let(:go) { post :codes, params: { current_password: current_password } }
end
end
describe 'DELETE destroy' do
- subject { delete :destroy }
+ subject { delete :destroy, params: { current_password: current_password } }
+
+ let(:current_password) { user.password }
context 'for a user that has 2FA enabled' do
let_it_be_with_reload(:user) { create(:user, :two_factor) }
@@ -187,6 +219,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do
expect(flash[:notice])
.to eq _('Two-factor authentication has been disabled successfully!')
end
+
+ it_behaves_like 'user must enter a valid current password' do
+ let(:go) { delete :destroy, params: { current_password: current_password } }
+ end
end
context 'for a user that does not have 2FA enabled' do
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index be5c1f0d428..c352524ec14 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -624,9 +624,9 @@ RSpec.describe Projects::ProjectMembersController do
end
end
- context 'when user can access source project members' do
+ context 'when user can admin source project members' do
before do
- another_project.add_guest(user)
+ another_project.add_maintainer(user)
end
include_context 'import applied'
@@ -640,7 +640,11 @@ RSpec.describe Projects::ProjectMembersController do
end
end
- context 'when user is not member of a source project' do
+ context "when user can't admin source project members" do
+ before do
+ another_project.add_developer(user)
+ end
+
include_context 'import applied'
it 'does not import team members' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 8afb80d9cc5..9d070061850 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -408,6 +408,47 @@ RSpec.describe ProjectsController do
end
end
+ describe 'POST create' do
+ let!(:params) do
+ {
+ path: 'foo',
+ description: 'bar',
+ import_url: project.http_url_to_repo,
+ namespace_id: user.namespace.id
+ }
+ end
+
+ subject { post :create, params: { project: params } }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when import by url is disabled' do
+ before do
+ stub_application_setting(import_sources: [])
+ end
+
+ it 'does not create project and reports an error' do
+ expect { subject }.not_to change { Project.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when import by url is enabled' do
+ before do
+ stub_application_setting(import_sources: ['git'])
+ end
+
+ it 'creates project' do
+ expect { subject }.to change { Project.count }
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+
describe 'GET edit' do
it 'allows an admin user to access the page', :enable_admin_mode do
sign_in(create(:user, :admin))
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 043fd97f1ad..2aa9b86b20e 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -666,6 +666,6 @@ RSpec.describe UploadsController do
def post_authorize(verified: true)
request.headers.merge!(workhorse_internal_api_request_header) if verified
- post :authorize, params: { model: 'personal_snippet', id: model.id }, format: :json
+ post :authorize, params: params, format: :json
end
end
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index c9059395377..893dd2c76e0 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -78,40 +78,80 @@ RSpec.describe 'Profile > Password' do
end
end
- context 'Change passowrd' do
+ context 'Change password' do
+ let(:new_password) { '22233344' }
+
before do
sign_in(user)
visit(edit_profile_password_path)
end
- it 'does not change user passowrd without old one' do
- page.within '.update-password' do
- fill_passwords('22233344', '22233344')
+ shared_examples 'user enters an incorrect current password' do
+ subject do
+ page.within '.update-password' do
+ fill_in 'user_current_password', with: user_current_password
+ fill_passwords(new_password, new_password)
+ end
end
- page.within '.flash-container' do
- expect(page).to have_content 'You must provide a valid current password'
- end
- end
+ it 'handles the invalid password attempt, and prompts the user to try again', :aggregate_failures do
+ expect(Gitlab::AppLogger).to receive(:info)
+ .with(message: 'Invalid current password when attempting to update user password', username: user.username, ip: user.current_sign_in_ip)
+
+ subject
+
+ user.reload
- it 'does not change password with invalid old password' do
- page.within '.update-password' do
- fill_in 'user_current_password', with: 'invalid'
- fill_passwords('password', 'confirmation')
+ expect(user.failed_attempts).to eq(1)
+ expect(user.valid_password?(new_password)).to eq(false)
+ expect(current_path).to eq(edit_profile_password_path)
+
+ page.within '.flash-container' do
+ expect(page).to have_content('You must provide a valid current password')
+ end
end
- page.within '.flash-container' do
- expect(page).to have_content 'You must provide a valid current password'
+ it 'locks the user account when user passes the maximum attempts threshold', :aggregate_failures do
+ user.update!(failed_attempts: User.maximum_attempts.pred)
+
+ subject
+
+ expect(current_path).to eq(new_user_session_path)
+
+ page.within '.flash-container' do
+ expect(page).to have_content('Your account is locked.')
+ end
end
end
- it 'changes user password' do
- page.within '.update-password' do
- fill_in "user_current_password", with: user.password
- fill_passwords('22233344', '22233344')
+ context 'when current password is blank' do
+ let(:user_current_password) { nil }
+
+ it_behaves_like 'user enters an incorrect current password'
+ end
+
+ context 'when current password is incorrect' do
+ let(:user_current_password) {'invalid' }
+
+ it_behaves_like 'user enters an incorrect current password'
+ end
+
+ context 'when the password reset is successful' do
+ subject do
+ page.within '.update-password' do
+ fill_in "user_current_password", with: user.password
+ fill_passwords(new_password, new_password)
+ end
end
- expect(current_path).to eq new_user_session_path
+ it 'changes the password, logs the user out and prompts them to sign in again', :aggregate_failures do
+ expect { subject }.to change { user.reload.valid_password?(new_password) }.to(true)
+ expect(current_path).to eq new_user_session_path
+
+ page.within '.flash-container' do
+ expect(page).to have_content('Password was successfully updated. Please sign in again.')
+ end
+ end
end
end
diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb
new file mode 100644
index 00000000000..e1feca5031a
--- /dev/null
+++ b/spec/features/profiles/two_factor_auths_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Two factor auths' do
+ context 'when signed in' do
+ before do
+ allow(Gitlab).to receive(:com?) { true }
+ end
+
+ context 'when user has two-factor authentication disabled' do
+ let(:user) { create(:user ) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'requires the current password to set up two factor authentication', :js do
+ visit profile_two_factor_auth_path
+
+ register_2fa(user.reload.current_otp, '123')
+
+ expect(page).to have_content('You must provide a valid current password')
+
+ register_2fa(user.reload.current_otp, user.password)
+
+ expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.')
+
+ click_button 'Copy codes'
+ click_link 'Proceed'
+
+ expect(page).to have_content('Status: Enabled')
+ end
+ end
+
+ context 'when user has two-factor authentication enabled' do
+ let(:user) { create(:user, :two_factor) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'requires the current_password to disable two-factor authentication', :js do
+ visit profile_two_factor_auth_path
+
+ fill_in 'current_password', with: '123'
+
+ click_button 'Disable two-factor authentication'
+
+ page.accept_alert
+
+ expect(page).to have_content('You must provide a valid current password')
+
+ fill_in 'current_password', with: user.password
+
+ click_button 'Disable two-factor authentication'
+
+ page.accept_alert
+
+ expect(page).to have_content('Two-factor authentication has been disabled successfully!')
+ expect(page).to have_content('Enable two-factor authentication')
+ end
+
+ it 'requires the current_password to regernate recovery codes', :js do
+ visit profile_two_factor_auth_path
+
+ fill_in 'current_password', with: '123'
+
+ click_button 'Regenerate recovery codes'
+
+ expect(page).to have_content('You must provide a valid current password')
+
+ fill_in 'current_password', with: user.password
+
+ click_button 'Regenerate recovery codes'
+
+ expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.')
+ end
+ end
+
+ def register_2fa(pin, password)
+ fill_in 'pin_code', with: pin
+ fill_in 'current_password', with: password
+
+ click_button 'Register with two-factor app'
+ end
+ end
+end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index afd750d02eb..79c4057a8b9 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -807,6 +807,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
expect(current_path).to eq(profile_two_factor_auth_path)
fill_in 'pin_code', with: user.reload.current_otp
+ fill_in 'current_password', with: user.password
click_button 'Register with two-factor app'
click_button 'Copy codes'
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index e3aeace6383..1072e63b20b 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -7579,23 +7579,6 @@
}
}
],
- "triggers": [
- {
- "id": 123,
- "token": "cdbfasdf44a5958c83654733449e585",
- "project_id": 5,
- "owner_id": 1,
- "created_at": "2017-01-16T15:25:28.637Z",
- "updated_at": "2017-01-16T15:25:28.637Z"
- },
- {
- "id": 456,
- "token": "33a66349b5ad01fc00174af87804e40",
- "project_id": 5,
- "created_at": "2017-01-16T15:25:29.637Z",
- "updated_at": "2017-01-16T15:25:29.637Z"
- }
- ],
"pipeline_schedules": [
{
"id": 1,
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
index 93619f4fb44..2b5bda687b8 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
@@ -1,2 +1,2 @@
{"id":123,"token":"cdbfasdf44a5958c83654733449e585","project_id":5,"owner_id":1,"created_at":"2017-01-16T15:25:28.637Z","updated_at":"2017-01-16T15:25:28.637Z"}
-{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"}
+{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"} \ No newline at end of file
diff --git a/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap b/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap
new file mode 100644
index 00000000000..3fe0e570a54
--- /dev/null
+++ b/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap
@@ -0,0 +1,99 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManageTwoFactorForm Disable button renders the component correctly 1`] = `
+VueWrapper {
+ "_emitted": Object {},
+ "_emittedByOrder": Array [],
+ "isFunctionalComponent": undefined,
+}
+`;
+
+exports[`ManageTwoFactorForm Disable button renders the component correctly 2`] = `
+<form
+ action="#"
+ class="gl-display-inline-block"
+ method="post"
+>
+ <input
+ data-testid="test-2fa-method-field"
+ name="_method"
+ type="hidden"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ />
+
+ <div
+ class="form-group gl-form-group"
+ id="__BVID__15"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="current-password"
+ id="__BVID__15__BV_label_"
+ >
+ Current password
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <input
+ aria-required="true"
+ class="gl-form-input form-control"
+ data-qa-selector="current_password_field"
+ id="current-password"
+ name="current_password"
+ required="required"
+ type="password"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <button
+ class="btn btn-danger gl-mr-3 gl-display-inline-block btn-danger btn-md gl-button"
+ data-confirm="Are you sure? This will invalidate your registered applications and U2F devices."
+ data-form-action="2fa_auth_path"
+ data-form-method="2fa_auth_method"
+ data-testid="test-2fa-disable-button"
+ type="submit"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Disable two-factor authentication
+
+ </span>
+ </button>
+
+ <button
+ class="btn gl-display-inline-block btn-default btn-md gl-button"
+ data-form-action="2fa_codes_path"
+ data-form-method="2fa_codes_method"
+ data-testid="test-2fa-regenerate-codes-button"
+ type="submit"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Regenerate recovery codes
+
+ </span>
+ </button>
+</form>
+`;
diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
new file mode 100644
index 00000000000..384579c6876
--- /dev/null
+++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
@@ -0,0 +1,98 @@
+import { within } from '@testing-library/dom';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import ManageTwoFactorForm, {
+ i18n,
+} from '~/authentication/two_factor_auth/components/manage_two_factor_form.vue';
+
+describe('ManageTwoFactorForm', () => {
+ let wrapper;
+
+ const createComponent = (options = {}) => {
+ wrapper = extendedWrapper(
+ mount(ManageTwoFactorForm, {
+ provide: {
+ webauthnEnabled: options?.webauthnEnabled || false,
+ profileTwoFactorAuthPath: '2fa_auth_path',
+ profileTwoFactorAuthMethod: '2fa_auth_method',
+ codesProfileTwoFactorAuthPath: '2fa_codes_path',
+ codesProfileTwoFactorAuthMethod: '2fa_codes_method',
+ },
+ }),
+ );
+ };
+
+ const queryByText = (text, options) => within(wrapper.element).queryByText(text, options);
+ const queryByLabelText = (text, options) =>
+ within(wrapper.element).queryByLabelText(text, options);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Current password field', () => {
+ it('renders the current password field', () => {
+ expect(queryByLabelText(i18n.currentPassword).tagName).toEqual('INPUT');
+ });
+ });
+
+ describe('Disable button', () => {
+ it('renders the component correctly', () => {
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has the right confirm text', () => {
+ expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual(
+ i18n.confirm,
+ );
+ });
+
+ describe('when webauthnEnabled', () => {
+ beforeEach(() => {
+ createComponent({
+ webauthnEnabled: true,
+ });
+ });
+
+ it('has the right confirm text', () => {
+ expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual(
+ i18n.confirmWebAuthn,
+ );
+ });
+ });
+
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = wrapper.find('form');
+ const disableButton = wrapper.findByTestId('test-2fa-disable-button').element;
+ const methodInput = wrapper.findByTestId('test-2fa-method-field').element;
+
+ form.trigger('submit', { submitter: disableButton });
+
+ await wrapper.vm.$nextTick();
+
+ expect(form.element.getAttribute('action')).toEqual('2fa_auth_path');
+ expect(methodInput.getAttribute('value')).toEqual('2fa_auth_method');
+ });
+ });
+
+ describe('Regenerate recovery codes button', () => {
+ it('renders the button', () => {
+ expect(queryByText(i18n.regenerateRecoveryCodes)).toEqual(expect.any(HTMLElement));
+ });
+
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = wrapper.find('form');
+ const regenerateCodesButton = wrapper.findByTestId('test-2fa-regenerate-codes-button')
+ .element;
+ const methodInput = wrapper.findByTestId('test-2fa-method-field').element;
+
+ form.trigger('submit', { submitter: regenerateCodesButton });
+
+ await wrapper.vm.$nextTick();
+
+ expect(form.element.getAttribute('action')).toEqual('2fa_codes_path');
+ expect(methodInput.getAttribute('value')).toEqual('2fa_codes_method');
+ });
+ });
+});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 211ed064762..94ad7759110 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -574,6 +574,15 @@ describe('GfmAutoComplete', () => {
}),
).toBe('<li><small>grp/proj#5</small> Some Issue</li>');
});
+
+ it('escapes title in the template as it is user input', () => {
+ expect(
+ GfmAutoComplete.Issues.templateFunction({
+ id: 5,
+ title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
+ }),
+ ).toBe('<li><small>5</small> &dollar;{search}&lt;script&gt;oh no &dollar;</li>');
+ });
});
describe('GfmAutoComplete.Members', () => {
@@ -608,16 +617,18 @@ describe('GfmAutoComplete', () => {
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
- it('should add escaped title if title is set', () => {
+ it('escapes title in the template as it is user input', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
- title: 'MyGroup+',
+ title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
- ).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
+ ).toBe(
+ '<li>IMG my-group <small>&dollar;{search}&lt;script&gt;oh no &dollar;</small> <i class="icon"/></li>',
+ );
});
it('should add user availability status if availabilityStatus is set', () => {
@@ -782,6 +793,15 @@ describe('GfmAutoComplete', () => {
${'/unlabel ~'} | ${assignedLabels}
`('$input shows $output.length labels', expectLabels);
});
+
+ it('escapes title in the template as it is user input', () => {
+ const color = '#123456';
+ const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
+
+ expect(GfmAutoComplete.Labels.templateFunction(color, title)).toBe(
+ '<li><span class="dropdown-label-box" style="background: #123456"></span> &dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ );
+ });
});
describe('emoji', () => {
@@ -829,4 +849,15 @@ describe('GfmAutoComplete', () => {
});
});
});
+
+ describe('milestones', () => {
+ it('escapes title in the template as it is user input', () => {
+ const expired = false;
+ const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
+
+ expect(GfmAutoComplete.Milestones.templateFunction(title, expired)).toBe(
+ '<li>&dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ );
+ });
+ });
});
diff --git a/spec/frontend/users_select/index_spec.js b/spec/frontend/users_select/index_spec.js
index 99caaf61c54..0d2aae78944 100644
--- a/spec/frontend/users_select/index_spec.js
+++ b/spec/frontend/users_select/index_spec.js
@@ -1,3 +1,5 @@
+import { escape } from 'lodash';
+import UsersSelect from '~/users_select/index';
import {
createInputsModelExpectation,
createUnassignedExpectation,
@@ -91,5 +93,19 @@ describe('~/users_select/index', () => {
expect(findDropdownItemsModel()).toEqual(expectation);
});
});
+
+ describe('renderApprovalRules', () => {
+ const ruleNames = ['simple-name', '"\'<>&', '"><script>alert(1)<script>'];
+
+ it.each(ruleNames)('escapes rule name correctly for %s', (name) => {
+ const escapedName = escape(name);
+
+ expect(
+ UsersSelect.prototype.renderApprovalRules('reviewer', [{ name }]),
+ ).toMatchInterpolatedText(
+ `<div class="gl-display-flex gl-font-sm"> <span class="gl-text-truncate" title="${escapedName}">${escapedName}</span> </div>`,
+ );
+ });
+ });
});
});
diff --git a/spec/graphql/types/group_invitation_type_spec.rb b/spec/graphql/types/group_invitation_type_spec.rb
index dab2d43fc90..9eedc2db81d 100644
--- a/spec/graphql/types/group_invitation_type_spec.rb
+++ b/spec/graphql/types/group_invitation_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Types::GroupInvitationType do
specify { expect(described_class.graphql_name).to eq('GroupInvitation') }
- specify { expect(described_class).to require_graphql_authorizations(:read_group) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_group) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/graphql/types/project_invitation_type_spec.rb b/spec/graphql/types/project_invitation_type_spec.rb
index 148a763a5fa..5c0b03c2505 100644
--- a/spec/graphql/types/project_invitation_type_spec.rb
+++ b/spec/graphql/types/project_invitation_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Types::ProjectInvitationType do
specify { expect(described_class.graphql_name).to eq('ProjectInvitation') }
- specify { expect(described_class).to require_graphql_authorizations(:read_project) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/helpers/external_link_helper_spec.rb b/spec/helpers/external_link_helper_spec.rb
index f5bb0568824..b746cb04ab3 100644
--- a/spec/helpers/external_link_helper_spec.rb
+++ b/spec/helpers/external_link_helper_spec.rb
@@ -13,8 +13,14 @@ RSpec.describe ExternalLinkHelper do
it 'allows options when creating external link with icon' do
link = external_link('https://gitlab.com', 'https://gitlab.com', { "data-foo": "bar", class: "externalLink" }).to_s
-
expect(link).to start_with('<a target="_blank" rel="noopener noreferrer" data-foo="bar" class="externalLink" href="https://gitlab.com">https://gitlab.com')
expect(link).to include('data-testid="external-link-icon"')
end
+
+ it 'sanitizes and returns external link with icon' do
+ link = external_link('sanitized link content', 'javascript:alert()').to_s
+ expect(link).not_to include('href="javascript:alert()"')
+ expect(link).to start_with('<a target="_blank" rel="noopener noreferrer">sanitized link content')
+ expect(link).to include('data-testid="external-link-icon"')
+ end
end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 4784d0aff26..af2957d72c7 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -35,22 +35,22 @@ RSpec.describe IconsHelper do
it 'returns svg icon html with DEFAULT_ICON_SIZE' do
expect(sprite_icon(icon_name).to_s)
- .to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html without size class' do
expect(sprite_icon(icon_name, size: nil).to_s)
- .to eq "<svg data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes' do
expect(sprite_icon(icon_name, size: 72).to_s)
- .to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes + additional class' do
expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
- .to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
describe 'non existing icon' do
diff --git a/spec/initializers/100_patch_omniauth_oauth2_spec.rb b/spec/initializers/100_patch_omniauth_oauth2_spec.rb
new file mode 100644
index 00000000000..0c436e4ef45
--- /dev/null
+++ b/spec/initializers/100_patch_omniauth_oauth2_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'OmniAuth::Strategies::OAuth2', type: :strategy do
+ let(:strategy) { [OmniAuth::Strategies::OAuth2] }
+
+ it 'verifies the gem version' do
+ current_version = OmniAuth::OAuth2::VERSION
+ expected_version = '1.7.1'
+
+ expect(current_version).to eq(expected_version), <<~EOF
+ New version #{current_version} of the `omniauth-oauth2` gem detected!
+
+ Please check if the monkey patches in `config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb`
+ are still needed, and either update/remove them, or bump the version in this spec.
+
+ EOF
+ end
+
+ context 'when a custom error message is passed from an OAuth2 provider' do
+ let(:message) { 'Please go to https://evil.com' }
+ let(:state) { 'secret' }
+ let(:callback_path) { '/users/auth/oauth2/callback' }
+ let(:params) { { state: state, error: 'evil_key', error_description: message } }
+ let(:error) { last_request.env['omniauth.error'] }
+
+ before do
+ env('rack.session', { 'omniauth.state' => state })
+ end
+
+ it 'returns the custom error message if the state is valid' do
+ get callback_path, **params
+
+ expect(error.message).to eq("evil_key | #{message}")
+ end
+
+ it 'returns the custom `error_reason` message if the `error_description` is blank' do
+ get callback_path, **params.merge(error_description: ' ', error_reason: 'custom reason')
+
+ expect(error.message).to eq('evil_key | custom reason')
+ end
+
+ it 'returns a CSRF error if the state is invalid' do
+ get callback_path, **params.merge(state: 'invalid')
+
+ expect(error.message).to eq('csrf_detected | CSRF detected')
+ end
+
+ it 'returns a CSRF error if the state is missing' do
+ get callback_path, **params.without(:state)
+
+ expect(error.message).to eq('csrf_detected | CSRF detected')
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
index 2c64657d69d..820ebeb6945 100644
--- a/spec/lib/banzai/filter/spaced_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
@@ -63,6 +63,16 @@ RSpec.describe Banzai::Filter::SpacedLinkFilter do
end
end
+ it 'does not process malicious input' do
+ Timeout.timeout(10) do
+ doc = filter('[ (](' * 60_000)
+
+ found_links = doc.css('a')
+
+ expect(found_links.size).to eq(0)
+ end
+ end
+
it 'converts multiple URLs' do
link1 = '[first](slug one)'
link2 = '[second](http://example.com/slug two)'
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index 28e93a8da52..2543eb3a5e9 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -81,32 +81,72 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
expect(subject.find_sessionless_user(:api)).to eq job_token_user
end
- it 'returns lfs_token user if no job_token user found' do
- allow_any_instance_of(described_class)
- .to receive(:find_user_from_lfs_token)
- .and_return(lfs_token_user)
-
- expect(subject.find_sessionless_user(:api)).to eq lfs_token_user
- end
-
- it 'returns basic_auth_access_token user if no lfs_token user found' do
+ it 'returns nil even if basic_auth_access_token is available' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_personal_access_token)
.and_return(basic_auth_access_token_user)
- expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user
+ expect(subject.find_sessionless_user(:api)).to be_nil
end
- it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do
+ it 'returns nil even if find_user_from_lfs_token is available' do
allow_any_instance_of(described_class)
- .to receive(:find_user_from_basic_auth_password)
- .and_return(basic_auth_password_user)
+ .to receive(:find_user_from_lfs_token)
+ .and_return(lfs_token_user)
- expect(subject.find_sessionless_user(:api)).to eq basic_auth_password_user
+ expect(subject.find_sessionless_user(:api)).to be_nil
end
it 'returns nil if no user found' do
- expect(subject.find_sessionless_user(:api)).to be_blank
+ expect(subject.find_sessionless_user(:api)).to be_nil
+ end
+
+ context 'in an API request' do
+ before do
+ env['SCRIPT_NAME'] = '/api/v4/projects'
+ end
+
+ it 'returns basic_auth_access_token user if no job_token_user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_personal_access_token)
+ .and_return(basic_auth_access_token_user)
+
+ expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user
+ end
+ end
+
+ context 'in a Git request' do
+ before do
+ env['SCRIPT_NAME'] = '/group/project.git/info/refs'
+ end
+
+ it 'returns lfs_token user if no job_token user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_lfs_token)
+ .and_return(lfs_token_user)
+
+ expect(subject.find_sessionless_user(nil)).to eq lfs_token_user
+ end
+
+ it 'returns basic_auth_access_token user if no lfs_token user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_personal_access_token)
+ .and_return(basic_auth_access_token_user)
+
+ expect(subject.find_sessionless_user(nil)).to eq basic_auth_access_token_user
+ end
+
+ it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_basic_auth_password)
+ .and_return(basic_auth_password_user)
+
+ expect(subject.find_sessionless_user(nil)).to eq basic_auth_password_user
+ end
+
+ it 'returns nil if no user found' do
+ expect(subject.find_sessionless_user(nil)).to be_blank
+ end
end
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
diff --git a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb
index f906870195a..876c23a91bd 100644
--- a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb
+++ b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb
@@ -3,33 +3,50 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do
- let(:user) { create(:user) }
+ using RSpec::Parameterized::TableSyntax
- subject { described_class.new(user) }
+ subject(:verifier) { described_class.new(user) }
- describe '#two_factor_authentication_required?' do
- describe 'when it is required on application level' do
- it 'returns true' do
- stub_application_setting require_two_factor_authentication: true
+ let(:user) { build_stubbed(:user, otp_grace_period_started_at: Time.zone.now) }
- expect(subject.two_factor_authentication_required?).to be_truthy
- end
- end
+ describe '#two_factor_authentication_enforced?' do
+ subject { verifier.two_factor_authentication_enforced? }
- describe 'when it is required on group level' do
- it 'returns true' do
- allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(true)
+ where(:instance_level_enabled, :group_level_enabled, :grace_period_expired, :should_be_enforced) do
+ false | false | true | false
+ true | false | false | false
+ true | false | true | true
+ false | true | false | false
+ false | true | true | true
+ end
- expect(subject.two_factor_authentication_required?).to be_truthy
+ with_them do
+ before do
+ stub_application_setting(require_two_factor_authentication: instance_level_enabled)
+ allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled)
+ stub_application_setting(two_factor_grace_period: grace_period_expired ? 0 : 1.month.in_hours)
end
+
+ it { is_expected.to eq(should_be_enforced) }
end
+ end
- describe 'when it is not required' do
- it 'returns false when not required on group level' do
- allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(false)
+ describe '#two_factor_authentication_required?' do
+ subject { verifier.two_factor_authentication_required? }
+
+ where(:instance_level_enabled, :group_level_enabled, :should_be_required) do
+ true | false | true
+ false | true | true
+ false | false | false
+ end
- expect(subject.two_factor_authentication_required?).to be_falsey
+ with_them do
+ before do
+ stub_application_setting(require_two_factor_authentication: instance_level_enabled)
+ allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled)
end
+
+ it { is_expected.to eq(should_be_required) }
end
end
@@ -85,25 +102,21 @@ RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do
end
describe '#two_factor_grace_period_expired?' do
- before do
- allow(user).to receive(:otp_grace_period_started_at).and_return(4.hours.ago)
- end
-
it 'returns true if the grace period has expired' do
- allow(subject).to receive(:two_factor_grace_period).and_return(2)
+ stub_application_setting two_factor_grace_period: 0
expect(subject.two_factor_grace_period_expired?).to be_truthy
end
it 'returns false if the grace period has not expired' do
- allow(subject).to receive(:two_factor_grace_period).and_return(6)
+ stub_application_setting two_factor_grace_period: 1.month.in_hours
expect(subject.two_factor_grace_period_expired?).to be_falsey
end
context 'when otp_grace_period_started_at is nil' do
it 'returns false' do
- allow(user).to receive(:otp_grace_period_started_at).and_return(nil)
+ user.otp_grace_period_started_at = nil
expect(subject.two_factor_grace_period_expired?).to be_falsey
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index cc592bb8f24..5ec6e23774a 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -386,7 +386,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
shared_examples 'with an invalid access token' do
it 'fails for a non-member' do
expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
- .to have_attributes(auth_failure )
+ .to have_attributes(auth_failure)
end
context 'when project bot user is blocked' do
@@ -396,7 +396,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'fails for a blocked project bot' do
expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
- .to have_attributes(auth_failure )
+ .to have_attributes(auth_failure)
end
end
end
@@ -466,6 +466,41 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
.to have_attributes(auth_failure)
end
+ context 'when 2fa is enabled globally' do
+ let_it_be(:user) do
+ create(:user, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
+ end
+
+ before do
+ stub_application_setting(require_two_factor_authentication: true)
+ end
+
+ it 'fails if grace period expired' do
+ stub_application_setting(two_factor_grace_period: 0)
+
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ .to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
+ end
+
+ it 'goes through if grace period is not expired yet' do
+ stub_application_setting(two_factor_grace_period: 72)
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
+ end
+ end
+
+ context 'when 2fa is enabled personally' do
+ let(:user) do
+ create(:user, :two_factor, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
+ end
+
+ it 'fails' do
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ .to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
+ end
+ end
+
it 'goes through lfs authentication' do
user = create(
:user,
@@ -757,16 +792,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
describe 'find_with_user_password' do
let!(:user) do
create(:user,
- username: username,
- password: password,
- password_confirmation: password)
+ username: username,
+ password: password,
+ password_confirmation: password)
end
let(:username) { 'John' } # username isn't lowercase, test this
let(:password) { 'my-secret' }
it "finds user by valid login/password" do
- expect( gl_auth.find_with_user_password(username, password) ).to eql user
+ expect(gl_auth.find_with_user_password(username, password)).to eql user
end
it 'finds user by valid email/password with case-insensitive email' do
@@ -779,12 +814,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it "does not find user with invalid password" do
password = 'wrong'
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it "does not find user with invalid login" do
user = 'wrong'
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
include_examples 'user login operation with unique ip limit' do
@@ -796,13 +831,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'finds the user in deactivated state' do
user.deactivate!
- expect( gl_auth.find_with_user_password(username, password) ).to eql user
+ expect(gl_auth.find_with_user_password(username, password)).to eql user
end
it "does not find user in blocked state" do
user.block
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it 'does not find user in locked state' do
@@ -814,13 +849,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it "does not find user in ldap_blocked state" do
user.ldap_block
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it 'does not find user in blocked_pending_approval state' do
user.block_pending_approval
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
context 'with increment_failed_attempts' do
diff --git a/spec/lib/gitlab/fogbugz_import/importer_spec.rb b/spec/lib/gitlab/fogbugz_import/importer_spec.rb
index eb0c4da6ce3..9b58b772d1a 100644
--- a/spec/lib/gitlab/fogbugz_import/importer_spec.rb
+++ b/spec/lib/gitlab/fogbugz_import/importer_spec.rb
@@ -4,23 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::FogbugzImport::Importer do
let(:project) { create(:project_empty_repo) }
- let(:importer) { described_class.new(project) }
- let(:repo) do
- instance_double(Gitlab::FogbugzImport::Repository,
- safe_name: 'vim',
- path: 'vim',
- raw_data: '')
- end
-
- let(:import_data) { { 'repo' => repo } }
- let(:credentials) do
- {
- 'fb_session' => {
- 'uri' => 'https://testing.fogbugz.com',
- 'token' => 'token'
- }
- }
- end
+ let(:fogbugz_project) { { 'ixProject' => project.id, 'sProject' => 'vim' } }
+ let(:import_data) { { 'repo' => fogbugz_project } }
+ let(:base_url) { 'https://testing.fogbugz.com' }
+ let(:token) { 'token' }
+ let(:credentials) { { 'fb_session' => { 'uri' => base_url, 'token' => token } } }
let(:closed_bug) do
{
@@ -46,18 +34,22 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
let(:fogbugz_bugs) { [opened_bug, closed_bug] }
+ subject(:importer) { described_class.new(project) }
+
before do
project.create_import_data(data: import_data, credentials: credentials)
- allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listCategories).and_return([])
- allow_any_instance_of(Gitlab::FogbugzImport::Client).to receive(:cases).and_return(fogbugz_bugs)
+
+ stub_fogbugz('listProjects', projects: { project: [fogbugz_project], count: 1 })
+ stub_fogbugz('listCategories', categories: { category: [], count: 0 })
+ stub_fogbugz('search', cases: { case: fogbugz_bugs, count: fogbugz_bugs.size })
end
it 'imports bugs' do
- expect { importer.execute }.to change { Issue.count }.by(2)
+ expect { subject.execute }.to change { Issue.count }.by(2)
end
it 'imports opened bugs' do
- importer.execute
+ subject.execute
issue = Issue.where(project_id: project.id).find_by_title(opened_bug[:sTitle])
@@ -65,10 +57,54 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
end
it 'imports closed bugs' do
- importer.execute
+ subject.execute
issue = Issue.where(project_id: project.id).find_by_title(closed_bug[:sTitle])
expect(issue.state_id).to eq(Issue.available_states[:closed])
end
+
+ context 'verify url' do
+ context 'when host is localhost' do
+ let(:base_url) { 'https://localhost:3000' }
+
+ it 'does not allow localhost requests' do
+ expect { subject.execute }
+ .to raise_error(
+ ::Gitlab::HTTP::BlockedUrlError,
+ "URL 'https://localhost:3000/api.asp' is blocked: Requests to localhost are not allowed"
+ )
+ end
+ end
+
+ context 'when host is on local network' do
+ let(:base_url) { 'http://192.168.0.1' }
+
+ it 'does not allow localhost requests' do
+ expect { subject.execute }
+ .to raise_error(
+ ::Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://192.168.0.1/api.asp' is blocked: Requests to the local network are not allowed"
+ )
+ end
+ end
+
+ context 'when host is ftp protocol' do
+ let(:base_url) { 'ftp://testing' }
+
+ it 'only accept http and https requests' do
+ expect { subject.execute }
+ .to raise_error(
+ HTTParty::UnsupportedURIScheme,
+ "'ftp://testing/api.asp' Must be HTTP, HTTPS or Generic"
+ )
+ end
+ end
+ end
+
+ def stub_fogbugz(command, response)
+ stub_request(:post, "#{base_url}/api.asp")
+ .with(body: hash_including({ 'cmd' => command, 'token' => token }))
+ .to_return(status: 200, body: response.to_xml(root: :response))
+ end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index bf682e4e4c6..bf2e3c7f5f8 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -435,17 +435,19 @@ RSpec.describe Gitlab::GitAccess do
it 'disallows users with expired password to pull' do
project.add_maintainer(user)
- user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.minutes.ago)
expect { pull_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
- it 'allows ldap users with expired password to pull' do
- project.add_maintainer(user)
- user.update!(password_expires_at: 2.minutes.ago)
- allow(user).to receive(:ldap_user?).and_return(true)
+ context 'with an ldap user' do
+ let(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
- expect { pull_access_check }.not_to raise_error
+ it 'allows ldap users with expired password to pull' do
+ project.add_maintainer(user)
+
+ expect { pull_access_check }.not_to raise_error
+ end
end
context 'when the project repository does not exist' do
@@ -987,24 +989,23 @@ RSpec.describe Gitlab::GitAccess do
end
it 'disallows users with expired password to push' do
- user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.minutes.ago)
expect { push_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
- it 'allows ldap users with expired password to push' do
- user.update!(password_expires_at: 2.minutes.ago)
- allow(user).to receive(:ldap_user?).and_return(true)
+ context 'with an ldap user' do
+ let(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
- expect { push_access_check }.not_to raise_error
- end
+ it 'allows ldap users with expired password to push' do
+ expect { push_access_check }.not_to raise_error
+ end
- it 'disallows blocked ldap users with expired password to push' do
- user.block
- user.update!(password_expires_at: 2.minutes.ago)
- allow(user).to receive(:ldap_user?).and_return(true)
+ it 'disallows blocked ldap users with expired password to push' do
+ user.block
- expect { push_access_check }.to raise_forbidden("Your account has been blocked.")
+ expect { push_access_check }.to raise_forbidden("Your account has been blocked.")
+ end
end
it 'cleans up the files' do
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 82f465c4f9e..518a9337826 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -445,8 +445,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
expect(@project.merge_requests.size).to eq(9)
end
- it 'only restores valid triggers' do
- expect(@project.triggers.size).to eq(1)
+ it 'does not restore triggers' do
+ expect(@project.triggers.size).to eq(0)
end
it 'has the correct number of pipelines and statuses' do
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index a9efa32f986..287be24d11f 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -401,15 +401,6 @@ Ci::Variable:
- encrypted_value
- encrypted_value_salt
- encrypted_value_iv
-Ci::Trigger:
-- id
-- token
-- project_id
-- created_at
-- updated_at
-- owner_id
-- description
-- ref
Ci::PipelineSchedule:
- id
- description
@@ -556,7 +547,6 @@ Project:
- disable_overriding_approvers_per_merge_request
- merge_requests_ff_only_enabled
- issues_template
-- repository_size_limit
- sync_time
- service_desk_enabled
- last_repository_updated_at
diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb
index 0929b90d1f4..83ba5858d81 100644
--- a/spec/lib/gitlab/legacy_github_import/client_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb
@@ -86,6 +86,15 @@ RSpec.describe Gitlab::LegacyGithubImport::Client do
it 'builds a endpoint with the given options' do
expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
end
+
+ context 'and hostname' do
+ subject(:client) { described_class.new(token, host: 'https://167.99.148.217/', api_version: 'v1', hostname: 'try.gitea.io') }
+
+ it 'builds a endpoint with the given options' do
+ expect(client.api.connection_options.dig(:headers, :host)).to eq 'try.gitea.io'
+ expect(client.api.api_endpoint).to eq 'https://167.99.148.217/api/v1/'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index a8472062f03..3bc0bd385a7 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
end
context 'when the user password is expired' do
- let(:actor) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true) }
+ let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be false
@@ -135,12 +135,12 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
end
context 'when the actor is an ldap user' do
- before do
- allow(actor).to receive(:ldap_user?).and_return(true)
- end
+ let(:actor) { create(:omniauth_user, provider: 'ldap') }
context 'when the user is blocked' do
- let(:actor) { create(:user, :blocked) }
+ before do
+ actor.block!
+ end
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be false
@@ -148,7 +148,9 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
end
context 'when the user password is expired' do
- let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
+ before do
+ actor.update!(password_expires_at: 1.minute.ago)
+ end
it 'returns true' do
expect(lfs_token.token_valid?(lfs_token.token)).to be true
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
index a02be83558c..0cbe44eacf4 100644
--- a/spec/lib/gitlab/string_regex_marker_spec.rb
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -23,9 +23,10 @@ RSpec.describe Gitlab::StringRegexMarker do
context 'with multiple occurrences' do
let(:raw) { %{a <b> <c> d} }
let(:rich) { %{a &lt;b&gt; &lt;c&gt; d}.html_safe }
+ let(:regexp) { /<[a-z]>/ }
subject do
- described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:, mode:|
+ described_class.new(raw, rich).mark(regexp) do |text, left:, right:, mode:|
%{<strong>#{text}</strong>}.html_safe
end
end
@@ -34,6 +35,15 @@ RSpec.describe Gitlab::StringRegexMarker do
expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
expect(subject).to be_html_safe
end
+
+ context 'with a Gitlab::UntrustedRegexp' do
+ let(:regexp) { Gitlab::UntrustedRegexp.new('<[a-z]>') }
+
+ it 'marks the matches' do
+ expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
+ expect(subject).to be_html_safe
+ end
+ end
end
end
end
diff --git a/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
new file mode 100644
index 00000000000..0d0f6a3df67
--- /dev/null
+++ b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('cleanup_orphan_project_access_tokens')
+
+RSpec.describe CleanupOrphanProjectAccessTokens, :migration do
+ def create_user(**extra_options)
+ defaults = { state: 'active', projects_limit: 0, email: "#{extra_options[:username]}@example.com" }
+
+ table(:users).create!(defaults.merge(extra_options))
+ end
+
+ def create_membership(**extra_options)
+ defaults = { access_level: 30, notification_level: 0, source_id: 1, source_type: 'Project' }
+
+ table(:members).create!(defaults.merge(extra_options))
+ end
+
+ let!(:regular_user) { create_user(username: 'regular') }
+ let!(:orphan_bot) { create_user(username: 'orphaned_bot', user_type: 6) }
+ let!(:used_bot) do
+ create_user(username: 'used_bot', user_type: 6).tap do |bot|
+ create_membership(user_id: bot.id)
+ end
+ end
+
+ it 'marks all bots without memberships as deactivated' do
+ expect do
+ migrate!
+ regular_user.reload
+ orphan_bot.reload
+ used_bot.reload
+ end.to change {
+ [regular_user.state, orphan_bot.state, used_bot.state]
+ }.from(%w[active active active]).to(%w[active deactivated active])
+ end
+
+ it 'schedules for deletion all bots without memberships' do
+ job_class = 'DeleteUserWorker'.safe_constantize
+
+ if job_class
+ expect(job_class).to receive(:bulk_perform_async).with([[orphan_bot.id, orphan_bot.id, skip_authorization: true]])
+
+ migrate!
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 334f9b4ae30..ca4c38d4663 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -5407,43 +5407,11 @@ RSpec.describe User do
end
describe '#password_expired_if_applicable?' do
- let(:user) { build(:user, password_expires_at: password_expires_at, password_automatically_set: set_automatically?) }
+ let(:user) { build(:user, password_expires_at: password_expires_at) }
subject { user.password_expired_if_applicable? }
- context 'when user is not ldap user' do
- context 'when user has password set automatically' do
- let(:set_automatically?) { true }
-
- context 'when password_expires_at is not set' do
- let(:password_expires_at) {}
-
- it 'returns false' do
- is_expected.to be_falsey
- end
- end
-
- context 'when password_expires_at is in the past' do
- let(:password_expires_at) { 1.minute.ago }
-
- it 'returns true' do
- is_expected.to be_truthy
- end
- end
-
- context 'when password_expires_at is in the future' do
- let(:password_expires_at) { 1.minute.from_now }
-
- it 'returns false' do
- is_expected.to be_falsey
- end
- end
- end
- end
-
- context 'when user has password not set automatically' do
- let(:set_automatically?) { false }
-
+ shared_examples 'password expired not applicable' do
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
@@ -5469,13 +5437,7 @@ RSpec.describe User do
end
end
- context 'when user is ldap user' do
- let(:user) { build(:user, password_expires_at: password_expires_at) }
-
- before do
- allow(user).to receive(:ldap_user?).and_return(true)
- end
-
+ context 'with a regular user' do
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
@@ -5487,8 +5449,8 @@ RSpec.describe User do
context 'when password_expires_at is in the past' do
let(:password_expires_at) { 1.minute.ago }
- it 'returns false' do
- is_expected.to be_falsey
+ it 'returns true' do
+ is_expected.to be_truthy
end
end
@@ -5501,32 +5463,26 @@ RSpec.describe User do
end
end
- context 'when user is a project bot' do
- let(:user) { build(:user, :project_bot, password_expires_at: password_expires_at) }
-
- context 'when password_expires_at is not set' do
- let(:password_expires_at) {}
-
- it 'returns false' do
- is_expected.to be_falsey
- end
+ context 'when user is a bot' do
+ before do
+ allow(user).to receive(:bot?).and_return(true)
end
- context 'when password_expires_at is in the past' do
- let(:password_expires_at) { 1.minute.ago }
+ it_behaves_like 'password expired not applicable'
+ end
- it 'returns false' do
- is_expected.to be_falsey
- end
- end
+ context 'when password_automatically_set is true' do
+ let(:user) { create(:omniauth_user, provider: 'ldap')}
- context 'when password_expires_at is in the future' do
- let(:password_expires_at) { 1.minute.from_now }
+ it_behaves_like 'password expired not applicable'
+ end
- it 'returns false' do
- is_expected.to be_falsey
- end
+ context 'when allow_password_authentication is false' do
+ before do
+ allow(user).to receive(:allow_password_authentication?).and_return(false)
end
+
+ it_behaves_like 'password expired not applicable'
end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 122612df355..ca9a5b1853c 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -249,15 +249,13 @@ RSpec.describe GlobalPolicy do
context 'user with expired password' do
before do
- current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:access_api) }
context 'when user is using ldap' do
- before do
- allow(current_user).to receive(:ldap_user?).and_return(true)
- end
+ let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it { is_expected.to be_allowed(:access_api) }
end
@@ -445,15 +443,13 @@ RSpec.describe GlobalPolicy do
context 'user with expired password' do
before do
- current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:access_git) }
context 'when user is using ldap' do
- before do
- allow(current_user).to receive(:ldap_user?).and_return(true)
- end
+ let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it { is_expected.to be_allowed(:access_git) }
end
@@ -537,15 +533,13 @@ RSpec.describe GlobalPolicy do
context 'user with expired password' do
before do
- current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:use_slash_commands) }
context 'when user is using ldap' do
- before do
- allow(current_user).to receive(:ldap_user?).and_return(true)
- end
+ let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it { is_expected.to be_allowed(:use_slash_commands) }
end
diff --git a/spec/requests/api/import_bitbucket_server_spec.rb b/spec/requests/api/import_bitbucket_server_spec.rb
index 2225f737f36..970416c7444 100644
--- a/spec/requests/api/import_bitbucket_server_spec.rb
+++ b/spec/requests/api/import_bitbucket_server_spec.rb
@@ -28,6 +28,20 @@ RSpec.describe API::ImportBitbucketServer do
Grape::Endpoint.before_each nil
end
+ it 'rejects requests when Bitbucket Server Importer is disabled' do
+ stub_application_setting(import_sources: nil)
+
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug
+ }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
.to receive(:new).with(project_key, repo_slug, anything, repo_slug, user.namespace, user, anything)
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index 76a4548df8a..b23ba0021e0 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -259,22 +259,32 @@ RSpec.describe API::Invitations do
let(:route) { get invitations_url(source, stranger) }
end
- %i[maintainer developer access_requester stranger].each do |type|
+ context "when authenticated as a maintainer" do
+ it 'returns 200' do
+ get invitations_url(source, maintainer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ %i[developer access_requester stranger].each do |type|
context "when authenticated as a #{type}" do
- it 'returns 200' do
+ it 'returns 403' do
user = public_send(type)
get invitations_url(source, user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(0)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
it 'avoids N+1 queries' do
+ invite_member_by_email(source, source_type, email, maintainer)
+
# Establish baseline
get invitations_url(source, maintainer)
@@ -282,7 +292,7 @@ RSpec.describe API::Invitations do
get invitations_url(source, maintainer)
end
- invite_member_by_email(source, source_type, email, maintainer)
+ invite_member_by_email(source, source_type, email2, maintainer)
expect do
get invitations_url(source, maintainer)
@@ -290,7 +300,7 @@ RSpec.describe API::Invitations do
end
it 'does not find confirmed members' do
- get invitations_url(source, developer)
+ get invitations_url(source, maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -300,10 +310,10 @@ RSpec.describe API::Invitations do
end
it 'finds all members with no query string specified' do
- invite_member_by_email(source, source_type, email, developer)
- invite_member_by_email(source, source_type, email2, developer)
+ invite_member_by_email(source, source_type, email, maintainer)
+ invite_member_by_email(source, source_type, email2, maintainer)
- get invitations_url(source, developer), params: { query: '' }
+ get invitations_url(source, maintainer), params: { query: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -314,17 +324,17 @@ RSpec.describe API::Invitations do
end
it 'finds the invitation by invite_email with query string' do
- invite_member_by_email(source, source_type, email, developer)
- invite_member_by_email(source, source_type, email2, developer)
+ invite_member_by_email(source, source_type, email, maintainer)
+ invite_member_by_email(source, source_type, email2, maintainer)
- get invitations_url(source, developer), params: { query: email }
+ get invitations_url(source, maintainer), params: { query: email }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['invite_email']).to eq(email)
- expect(json_response.first['created_by_name']).to eq(developer.name)
+ expect(json_response.first['created_by_name']).to eq(maintainer.name)
expect(json_response.first['user_name']).to eq(nil)
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 80bccdfee0c..be8a6c7bdcf 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1149,6 +1149,16 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:bad_request)
end
+ it 'disallows creating a project with an import_url when git import source is disabled' do
+ stub_application_setting(import_sources: nil)
+
+ project_params = { import_url: 'http://example.com', path: 'path-project-Foo', name: 'Foo Project' }
+ expect { post api('/projects', user), params: project_params }
+ .not_to change { Project.count }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
it 'sets a project as public' do
project = attributes_for(:project, visibility: 'public')
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 527e548ad19..ee1911b0a26 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -53,37 +53,6 @@ RSpec.describe API::Users do
end
end
- describe 'GET /users/:id' do
- context 'when unauthenticated' do
- it 'does not contain the note of the user' do
- get api("/users/#{user.id}")
-
- expect(json_response).not_to have_key('note')
- end
- end
-
- context 'when authenticated' do
- context 'as an admin' do
- it 'contains the note of the user' do
- get api("/users/#{user.id}", admin)
-
- expect(json_response).to have_key('note')
- expect(json_response['note']).to eq(user.note)
- expect(json_response).to have_key('sign_in_count')
- end
- end
-
- context 'as a regular user' do
- it 'does not contain the note of the user' do
- get api("/users/#{user.id}", user)
-
- expect(json_response).not_to have_key('note')
- expect(json_response).not_to have_key('sign_in_count')
- end
- end
- end
- end
-
describe "PUT /users/:id" do
context 'when user is an admin' do
it "updates note of the user" do
@@ -527,6 +496,8 @@ RSpec.describe API::Users do
end
describe "GET /users/:id" do
+ let_it_be(:user2, reload: true) { create(:user, username: 'another_user') }
+
it "returns a user by id" do
get api("/users/#{user.id}", user)
@@ -564,6 +535,64 @@ RSpec.describe API::Users do
expect(json_response.keys).not_to include 'trial'
end
+ it 'returns a 404 if the target user is present but inaccessible' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_user, user2).and_return(false)
+
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns the `created_at` field for public users' do
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include('created_at')
+ end
+
+ it 'does not return the `created_at` field for private users' do
+ get api("/users/#{private_user.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include('created_at')
+ end
+
+ it 'returns the `followers` field for public users' do
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include('followers')
+ end
+
+ it 'does not return the `followers` field for private users' do
+ get api("/users/#{private_user.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include('followers')
+ end
+
+ it 'returns the `following` field for public users' do
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include('following')
+ end
+
+ it 'does not return the `following` field for private users' do
+ get api("/users/#{private_user.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include('following')
+ end
+
+ it 'does not contain the note of the user' do
+ get api("/users/#{user.id}", user)
+
+ expect(json_response).not_to have_key('note')
+ expect(json_response).not_to have_key('sign_in_count')
+ end
+
context 'when job title is present' do
let(:job_title) { 'Fullstack Engineer' }
@@ -580,6 +609,14 @@ RSpec.describe API::Users do
end
context 'when authenticated as admin' do
+ it 'contains the note of the user' do
+ get api("/users/#{user.id}", admin)
+
+ expect(json_response).to have_key('note')
+ expect(json_response['note']).to eq(user.note)
+ expect(json_response).to have_key('sign_in_count')
+ end
+
it 'includes the `is_admin` field' do
get api("/users/#{user.id}", admin)
@@ -640,62 +677,10 @@ RSpec.describe API::Users do
end
context 'for an anonymous user' do
- it "returns a user by id" do
- get api("/users/#{user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response['username']).to eq(user.username)
- end
-
- it "returns a 404 if the target user is present but inaccessible" do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false)
-
- get api("/users/#{user.id}")
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns the `created_at` field for public users" do
- get api("/users/#{user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).to include 'created_at'
- end
-
- it "does not return the `created_at` field for private users" do
- get api("/users/#{private_user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).not_to include 'created_at'
- end
-
- it "returns the `followers` field for public users" do
- get api("/users/#{user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).to include 'followers'
- end
-
- it "does not return the `followers` field for private users" do
- get api("/users/#{private_user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).not_to include 'followers'
- end
-
- it "returns the `following` field for public users" do
+ it 'returns 403' do
get api("/users/#{user.id}")
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).to include 'following'
- end
-
- it "does not return the `following` field for private users" do
- get api("/users/#{private_user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).not_to include 'following'
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -788,6 +773,14 @@ RSpec.describe API::Users do
describe 'GET /users/:id/followers' do
let(:follower) { create(:user) }
+ context 'for an anonymous user' do
+ it 'returns 403' do
+ get api("/users/#{user.id}")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
context 'user has followers' do
it 'lists followers' do
follower.follow(user)
@@ -823,6 +816,14 @@ RSpec.describe API::Users do
describe 'GET /users/:id/following' do
let(:followee) { create(:user) }
+ context 'for an anonymous user' do
+ it 'returns 403' do
+ get api("/users/#{user.id}")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
context 'user has followers' do
it 'lists following user' do
user.follow(followee)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index a16f5abf608..d2528600477 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'Git HTTP requests' do
shared_examples 'operations are not allowed with expired password' do
context "when password is expired" do
it "responds to downloads with status 401 Unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -69,7 +69,7 @@ RSpec.describe 'Git HTTP requests' do
end
it "responds to uploads with status 401 Unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -614,7 +614,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to downloads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -697,7 +697,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to uploads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
@@ -950,7 +950,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when users password is expired' do
it 'rejects pulls with 401 unauthorized' do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, user: 'gitlab-ci-token', password: build.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -1245,7 +1245,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to downloads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -1328,7 +1328,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to uploads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
@@ -1555,7 +1555,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when users password is expired' do
it 'rejects pulls with 401 unauthorized' do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, user: 'gitlab-ci-token', password: build.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 02eb4262690..656ae744ac1 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe 'Git LFS API and storage' do
it_behaves_like 'LFS http 200 blob response'
context 'when user password is expired' do
- let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true)}
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
it_behaves_like 'LFS http 401 response'
end
@@ -344,7 +344,7 @@ RSpec.describe 'Git LFS API and storage' do
end
context 'when user password is expired' do
- let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true)}
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
let(:role) { :reporter}
@@ -958,7 +958,7 @@ RSpec.describe 'Git LFS API and storage' do
it_behaves_like 'LFS http 200 workhorse response'
context 'when user password is expired' do
- let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true) }
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) }
it_behaves_like 'LFS http 401 response'
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 87ef6fa1a18..be942f6ae86 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -933,17 +933,28 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
end
context 'authenticated with lfs token' do
- it 'request is authenticated by token in basic auth' do
- lfs_token = Gitlab::LfsToken.new(user)
- encoded_login = ["#{user.username}:#{lfs_token.token}"].pack('m0')
+ let(:lfs_url) { '/namespace/repo.git/info/lfs/objects/batch' }
+ let(:lfs_token) { Gitlab::LfsToken.new(user) }
+ let(:encoded_login) { ["#{user.username}:#{lfs_token.token}"].pack('m0') }
+ let(:headers) { { 'AUTHORIZATION' => "Basic #{encoded_login}" } }
+ it 'request is authenticated by token in basic auth' do
expect_authenticated_request
- get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" }
+ get lfs_url, headers: headers
+ end
+
+ it 'request is not authenticated with API URL' do
+ expect_unauthenticated_request
+
+ get url, headers: headers
end
end
context 'authenticated with regular login' do
+ let(:encoded_login) { ["#{user.username}:#{user.password}"].pack('m0') }
+ let(:headers) { { 'AUTHORIZATION' => "Basic #{encoded_login}" } }
+
it 'request is authenticated after login' do
login_as(user)
@@ -952,12 +963,30 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
get url
end
- it 'request is authenticated by credentials in basic auth' do
- encoded_login = ["#{user.username}:#{user.password}"].pack('m0')
+ it 'request is not authenticated by credentials in basic auth' do
+ expect_unauthenticated_request
- expect_authenticated_request
+ get url, headers: headers
+ end
+
+ context 'with POST git-upload-pack' do
+ it 'request is authenticated by credentials in basic auth' do
+ expect(::Gitlab::Workhorse).to receive(:verify_api_request!)
+
+ expect_authenticated_request
- get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" }
+ post '/namespace/repo.git/git-upload-pack', headers: headers
+ end
+ end
+
+ context 'with GET info/refs' do
+ it 'request is authenticated by credentials in basic auth' do
+ expect(::Gitlab::Workhorse).to receive(:verify_api_request!)
+
+ expect_authenticated_request
+
+ get '/namespace/repo.git/info/refs?service=git-upload-pack', headers: headers
+ end
end
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 4a76347ea45..27688d8c966 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -450,6 +450,16 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end
end
+ context 'when project has project bots' do
+ let!(:project_bot) { create(:user, :project_bot).tap { |user| project.add_maintainer(user) } }
+
+ it 'deletes bot user as well' do
+ expect do
+ destroy_project(project, user)
+ end.to change { User.find_by(id: project_bot.id) }.to(nil)
+ end
+ end
+
context 'error while destroying', :sidekiq_inline do
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) }
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index a20c1d78912..62b35923bcd 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -323,6 +323,16 @@ RSpec.shared_examples 'handle uploads authorize' do
end
end
+ context 'when id is not passed as a param' do
+ let(:params) { super().without(:id) }
+
+ it 'returns 404 status' do
+ post_authorize
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'when a user can upload a file' do
before do
sign_in(user)
diff --git a/workhorse/internal/artifacts/entry.go b/workhorse/internal/artifacts/entry.go
index 91a0843f1ee..d5b3dfb672c 100644
--- a/workhorse/internal/artifacts/entry.go
+++ b/workhorse/internal/artifacts/entry.go
@@ -14,6 +14,7 @@ import (
"syscall"
"gitlab.com/gitlab-org/labkit/log"
+ "gitlab.com/gitlab-org/labkit/mask"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
@@ -35,7 +36,7 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string)
log.WithContextFields(r.Context(), log.Fields{
"entry": params.Entry,
- "archive": params.Archive,
+ "archive": mask.URL(params.Archive),
"path": r.URL.Path,
}).Print("SendEntry: sending")