diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f5f837d9c..84f3126d78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 13.4.7 (2020-12-07)
+
+### Security (10 changes)
+
+- Validate zoom links to start with https only. !1055
+- Require at least 3 characters when searching for project in the Explore page.
+- Do not show emails of users in confirmation page.
+- Forbid setting a gitlabUserList strategy to a list from another project.
+- Fix mermaid resource consumption in GFM fields.
+- Ensure group and project memberships are not leaked via API for users with private profiles.
+- GraphQL User: do not expose email if set to private.
+- Filter search parameter to prevent data leaks.
+- Do not expose starred projects of users with private profile via API.
+- Do not show starred & contributed projects of users with private profile.
+
+
## 13.4.6 (2020-11-03)
### Fixed (1 change)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 56f3fd065d..b64e52773f 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-13.4.6
\ No newline at end of file
+13.4.7
\ No newline at end of file
diff --git a/VERSION b/VERSION
index 56f3fd065d..b64e52773f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-13.4.6
\ No newline at end of file
+13.4.7
\ No newline at end of file
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index cb0e634505..37a965de86 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -18,7 +18,13 @@ import { __, sprintf } from '~/locale';
//
// This is an arbitrary number; Can be iterated upon when suitable.
-const MAX_CHAR_LIMIT = 5000;
+const MAX_CHAR_LIMIT = 2000;
+// Max # of mermaid blocks that can be rendered in a page.
+const MAX_MERMAID_BLOCK_LIMIT = 50;
+// Keep a map of mermaid blocks we've already rendered.
+const elsProcessingMap = new WeakMap();
+let renderedMermaidBlocks = 0;
+
let mermaidModule = {};
function importMermaidModule() {
@@ -110,13 +116,22 @@ function renderMermaids($els) {
let renderedChars = 0;
$els.each((i, el) => {
+ // Skipping all the elements which we've already queued in requestIdleCallback
+ if (elsProcessingMap.has(el)) {
+ return;
+ }
+
const { source } = fixElementSource(el);
/**
- * Restrict the rendering to a certain amount of character to
- * prevent mermaidjs from hanging up the entire thread and
- * causing a DoS.
+ * Restrict the rendering to a certain amount of character
+ * and mermaid blocks to prevent mermaidjs from hanging
+ * up the entire thread and causing a DoS.
*/
- if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
+ if (
+ (source && source.length > MAX_CHAR_LIMIT) ||
+ renderedChars > MAX_CHAR_LIMIT ||
+ renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT
+ ) {
const html = `
@@ -146,8 +161,13 @@ function renderMermaids($els) {
}
renderedChars += source.length;
+ renderedMermaidBlocks += 1;
- renderMermaidEl(el);
+ const requestId = window.requestIdleCallback(() => {
+ renderMermaidEl(el);
+ });
+
+ elsProcessingMap.set(el, requestId);
});
})
.catch(err => {
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index b3fa089a71..1f6f8e4429 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -8,6 +8,8 @@ class Explore::ProjectsController < Explore::ApplicationController
include SortingHelper
include SortingPreference
+ MIN_SEARCH_LENGTH = 3
+
before_action :set_non_archived_param
before_action :set_sorting
@@ -70,7 +72,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects
load_project_counts
- projects = ProjectsFinder.new(current_user: current_user, params: params).execute
+ projects = ProjectsFinder.new(current_user: current_user, params: params.merge(minimum_search_length: MIN_SEARCH_LENGTH)).execute
projects = preload_associations(projects)
projects = projects.page(params[:page]).without_count
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index dedaf0c903..d041b2f4b7 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -126,7 +126,6 @@ class SearchController < ApplicationController
payload[:metadata] ||= {}
payload[:metadata]['meta.search.group_id'] = params[:group_id]
payload[:metadata]['meta.search.project_id'] = params[:project_id]
- payload[:metadata]['meta.search.search'] = params[:search]
payload[:metadata]['meta.search.scope'] = params[:scope]
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 75a861423e..f4757316e1 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -19,7 +19,7 @@ class UsersController < ApplicationController
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists, :suggests]
before_action :authorize_read_user_profile!,
- only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :starred_projects, :snippets]
+ only: [:calendar, :calendar_activities, :groups, :projects, :contributed, :starred, :snippets]
def show
respond_to do |format|
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 471029c1ef..c3a7301aff 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -18,6 +18,7 @@
# personal: boolean
# search: string
# search_namespaces: boolean
+# minimum_search_length: int
# non_archived: boolean
# archived: 'only' or boolean
# min_access_level: integer
@@ -177,6 +178,9 @@ class ProjectsFinder < UnionFinder
def by_search(items)
params[:search] ||= params[:name]
+
+ return items.none if params[:search].present? && params[:minimum_search_length].present? && params[:search].length < params[:minimum_search_length].to_i
+
items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
end
diff --git a/app/finders/starred_projects_finder.rb b/app/finders/starred_projects_finder.rb
index fcb469d1d1..e209960c47 100644
--- a/app/finders/starred_projects_finder.rb
+++ b/app/finders/starred_projects_finder.rb
@@ -1,11 +1,22 @@
# frozen_string_literal: true
class StarredProjectsFinder < ProjectsFinder
+ include Gitlab::Allowable
+
def initialize(user, params: {}, current_user: nil)
+ @user = user
+
super(
params: params,
current_user: current_user,
project_ids_relation: user.starred_projects.select(:id)
)
end
+
+ def execute
+ # Do not show starred projects if the user has a private profile.
+ return Project.none unless can?(current_user, :read_user_profile, @user)
+
+ super
+ end
end
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 8047708776..4fa62c4961 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -19,7 +19,7 @@ module Types
field :state, Types::UserStateEnum, null: false,
description: 'State of the user'
field :email, GraphQL::STRING_TYPE, null: true,
- description: 'User email'
+ description: 'User email', method: :public_email
field :avatar_url, GraphQL::STRING_TYPE, null: true,
description: "URL of the user's avatar"
field :web_url, GraphQL::STRING_TYPE, null: false,
@@ -30,13 +30,11 @@ module Types
resolver: Resolvers::TodoResolver,
description: 'Todos of the user'
field :group_memberships, Types::GroupMemberType.connection_type, null: true,
- description: 'Group memberships of the user',
- method: :group_members
+ description: 'Group memberships of the user'
field :status, Types::UserStatusType, null: true,
description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
- description: 'Project memberships of the user',
- method: :project_members
+ description: 'Project memberships of the user'
field :starred_projects, Types::ProjectType.connection_type, null: true,
description: 'Projects starred by the user',
resolver: Resolvers::UserStarredProjectsResolver
diff --git a/app/models/operations/feature_flags/user_list.rb b/app/models/operations/feature_flags/user_list.rb
index b9bdcb59d5..75a457567a 100644
--- a/app/models/operations/feature_flags/user_list.rb
+++ b/app/models/operations/feature_flags/user_list.rb
@@ -23,6 +23,11 @@ module Operations
before_destroy :ensure_no_associated_strategies
+ def self.belongs_to?(project_id, user_list_ids)
+ uniq_ids = user_list_ids.uniq
+ where(id: uniq_ids, project_id: project_id).count == uniq_ids.count
+ end
+
private
def ensure_no_associated_strategies
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index f201b36346..0028e6d9ef 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -2,4 +2,18 @@
class UserPresenter < Gitlab::View::Presenter::Delegated
presents :user
+
+ def group_memberships
+ should_be_private? ? GroupMember.none : user.group_members
+ end
+
+ def project_memberships
+ should_be_private? ? ProjectMember.none : user.project_members
+ end
+
+ private
+
+ def should_be_private?
+ !can?(current_user, :read_user_profile, user)
+ end
end
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index 0c0548a17a..a7424c610c 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -24,7 +24,7 @@ module Todos
# if at least reporter, all entities including confidential issues can be accessed
return if user_has_reporter_access?
- remove_confidential_issue_todos
+ remove_confidential_resource_todos
if entity.private?
remove_project_todos
@@ -43,7 +43,7 @@ module Todos
end
# rubocop: disable CodeReuse/ActiveRecord
- def remove_confidential_issue_todos
+ def remove_confidential_resource_todos
Todo.where(
target_id: confidential_issues.select(:id), target_type: Issue.name, user_id: user.id
).delete_all
@@ -147,3 +147,5 @@ module Todos
end
end
end
+
+Todos::Destroy::EntityLeaveService.prepend_if_ee('EE::Todos::Destroy::EntityLeaveService')
diff --git a/app/validators/zoom_url_validator.rb b/app/validators/zoom_url_validator.rb
index dc4ca6b950..e0f8e4e34a 100644
--- a/app/validators/zoom_url_validator.rb
+++ b/app/validators/zoom_url_validator.rb
@@ -5,8 +5,13 @@
# Custom validator for zoom urls
#
class ZoomUrlValidator < ActiveModel::EachValidator
+ ALLOWED_SCHEMES = %w(https).freeze
+
def validate_each(record, attribute, value)
- return if Gitlab::ZoomLinkExtractor.new(value).links.size == 1
+ links_count = Gitlab::ZoomLinkExtractor.new(value).links.size
+ valid = Gitlab::UrlSanitizer.valid?(value, allowed_schemes: ALLOWED_SCHEMES)
+
+ return if links_count == 1 && valid
record.errors.add(:url, 'must contain one valid Zoom URL')
end
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
index f8aa3cf98d..8c0d076c77 100644
--- a/app/views/devise/confirmations/new.html.haml
+++ b/app/views/devise/confirmations/new.html.haml
@@ -6,7 +6,7 @@
= render "devise/shared/error_messages", resource: resource
.form-group
= f.label :email
- = f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.'
+ = f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.', value: nil
.clearfix
= f.submit "Resend", class: 'btn btn-success'
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index d819c4ea55..9b87314411 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -1,2 +1,6 @@
-- is_explore_page = defined?(explore_page) && explore_page
-= render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true)
+- if params[:name].present? && params[:name].size < Explore::ProjectsController::MIN_SEARCH_LENGTH
+ .nothing-here-block
+ %h5= _('Enter at least three characters to search')
+- else
+ - is_explore_page = defined?(explore_page) && explore_page
+ = render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true)
diff --git a/config/application.rb b/config/application.rb
index 4d2f3745b5..436bb288c3 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -135,6 +135,7 @@ module Gitlab
hook
import_url
elasticsearch_url
+ search
otp_attempt
sentry_dsn
trace
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 7b85f910d8..5c6c11f01e 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -46,6 +46,7 @@
- dynamic_application_security_testing
- editor_extension
- epics
+- epic_tracking
- error_tracking
- feature_flags
- foundations
diff --git a/db/post_migrate/20201109114603_schedule_remove_inaccessible_epic_todos.rb b/db/post_migrate/20201109114603_schedule_remove_inaccessible_epic_todos.rb
new file mode 100644
index 0000000000..13d12675a2
--- /dev/null
+++ b/db/post_migrate/20201109114603_schedule_remove_inaccessible_epic_todos.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class ScheduleRemoveInaccessibleEpicTodos < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INTERVAL = 2.minutes
+ BATCH_SIZE = 10
+ MIGRATION = 'RemoveInaccessibleEpicTodos'
+
+ disable_ddl_transaction!
+
+ class Epic < ActiveRecord::Base
+ include EachBatch
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ relation = Epic.where(confidential: true)
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ relation, MIGRATION, INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20201109114603 b/db/schema_migrations/20201109114603
new file mode 100644
index 0000000000..c1df2223db
--- /dev/null
+++ b/db/schema_migrations/20201109114603
@@ -0,0 +1 @@
+ae8034ec52df47ce2ce3397715dd18347e4d297a963c17c7b26321f414dfa632
\ No newline at end of file
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index 8862776ee1..2b373f85af 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -129,7 +129,11 @@ Note the following when promoting a secondary:
```
1. Promote the **secondary** node to the **primary** node.
-
+ CAUTION: **Caution:**
+ If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
+ a point-in-time recovery to the last known state.
+ Data that was created on the primary while the secondary was paused will be lost.
+
To promote the secondary node to primary along with preflight checks:
```shell
@@ -159,11 +163,16 @@ conjunction with multiple servers, as it can only
perform changes on a **secondary** with only a single machine. Instead, you must
do this manually.
+CAUTION: **Caution:**
+If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
+a point-in-time recovery to the last known state.
+Data that was created on the primary while the secondary was paused will be lost.
+
1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
promote to read-write:
```shell
- sudo gitlab-pg-ctl promote
+ sudo gitlab-ctl promote-db
```
In GitLab 12.8 and earlier, see [Message: `sudo: gitlab-pg-ctl: command not found`](../replication/troubleshooting.md#message-sudo-gitlab-pg-ctl-command-not-found).
diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md
index 6fdf213ac7..41f1189f4e 100644
--- a/doc/administration/geo/index.md
+++ b/doc/administration/geo/index.md
@@ -195,6 +195,10 @@ For information on how to update your Geo nodes to the latest GitLab version, se
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
+CAUTION: **Caution:**
+Pausing and resuming of replication is currently only supported for Geo installations using an
+Omnibus GitLab-managed database. External databases are currently not supported.
+
In some circumstances, like during [upgrades](replication/updating_the_geo_nodes.md) or a [planned failover](disaster_recovery/planned_failover.md), it is desirable to pause replication between the primary and secondary.
Pausing and resuming replication is done via a command line tool from the secondary node.
diff --git a/doc/user/todos.md b/doc/user/todos.md
index 1fca3c0ab6..d2a9e1e2df 100644
--- a/doc/user/todos.md
+++ b/doc/user/todos.md
@@ -64,7 +64,7 @@ To-do triggers aren't affected by [GitLab notification email settings](profile/n
NOTE: **Note:**
When a user no longer has access to a resource related to a to-do (such as an
-issue, merge request, project, or group), for security reasons GitLab deletes
+issue, merge request, epic, project, or group), for security reasons GitLab deletes
any related to-do items within the next hour. Deletion is delayed to prevent
data loss, in the case where a user's access is accidentally revoked.
diff --git a/lib/gitlab/background_migration/remove_inaccessible_epic_todos.rb b/lib/gitlab/background_migration/remove_inaccessible_epic_todos.rb
new file mode 100644
index 0000000000..74c48b237c
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_inaccessible_epic_todos.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # rubocop:disable Style/Documentation
+ class RemoveInaccessibleEpicTodos
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::RemoveInaccessibleEpicTodos.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveInaccessibleEpicTodos')
diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb
new file mode 100644
index 0000000000..49a39f257f
--- /dev/null
+++ b/spec/controllers/confirmations_controller_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ConfirmationsController do
+ include DeviseHelpers
+
+ before do
+ set_devise_mapping(context: @request)
+ end
+
+ describe '#show' do
+ render_views
+
+ subject { get :show, params: { confirmation_token: confirmation_token } }
+
+ context 'user is already confirmed' do
+ let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
+ let(:confirmation_token) { user.confirmation_token }
+
+ before do
+ user.confirm
+ subject
+ end
+
+ it 'renders `new`' do
+ expect(response).to render_template(:new)
+ end
+
+ it 'displays an error message' do
+ expect(response.body).to include('Email was already confirmed, please try signing in')
+ end
+
+ it 'does not display the email of the user' do
+ expect(response.body).not_to include(user.email)
+ end
+ end
+
+ context 'user accesses the link after the expiry of confirmation token has passed' do
+ let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
+ let(:confirmation_token) { user.confirmation_token }
+
+ before do
+ allow(Devise).to receive(:confirm_within).and_return(1.day)
+
+ travel_to(3.days.from_now) do
+ subject
+ end
+ end
+
+ it 'renders `new`' do
+ expect(response).to render_template(:new)
+ end
+
+ it 'displays an error message' do
+ expect(response.body).to include('Email needs to be confirmed within 1 day, please request a new one below')
+ end
+
+ it 'does not display the email of the user' do
+ expect(response.body).not_to include(user.email)
+ end
+ end
+
+ context 'with an invalid confirmation token' do
+ let(:confirmation_token) { 'invalid_confirmation_token' }
+
+ before do
+ subject
+ end
+
+ it 'renders `new`' do
+ expect(response).to render_template(:new)
+ end
+
+ it 'displays an error message' do
+ expect(response.body).to include('Confirmation token is invalid')
+ end
+ end
+ end
+end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index f244392bba..1293a1a218 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -272,7 +272,7 @@ RSpec.describe SearchController do
expect(last_payload[:metadata]['meta.search.group_id']).to eq('123')
expect(last_payload[:metadata]['meta.search.project_id']).to eq('456')
- expect(last_payload[:metadata]['meta.search.search']).to eq('hello world')
+ expect(last_payload[:metadata]).not_to have_key('meta.search.search')
expect(last_payload[:metadata]['meta.search.scope']).to eq('issues')
end
end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index bec4b24484..2e57a90131 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -247,32 +247,99 @@ RSpec.describe UsersController do
describe 'GET #contributed' do
let(:project) { create(:project, :public) }
- let(:current_user) { create(:user) }
+
+ subject do
+ get :contributed, params: { username: author.username }, format: format
+ end
before do
- sign_in(current_user)
+ sign_in(user)
project.add_developer(public_user)
project.add_developer(private_user)
+ create(:push_event, project: project, author: author)
+
+ subject
end
- context 'with public profile' do
+ shared_examples_for 'renders contributed projects' do
it 'renders contributed projects' do
- create(:push_event, project: project, author: public_user)
-
- get :contributed, params: { username: public_user.username }
-
expect(assigns[:contributed_projects]).not_to be_empty
+ expect(response).to have_gitlab_http_status(:ok)
end
end
- context 'with private profile' do
- it 'does not render contributed projects' do
- create(:push_event, project: project, author: private_user)
+ %i(html json).each do |format|
+ context "format: #{format}" do
+ let(:format) { format }
- get :contributed, params: { username: private_user.username }
+ context 'with public profile' do
+ let(:author) { public_user }
- expect(assigns[:contributed_projects]).to be_empty
+ it_behaves_like 'renders contributed projects'
+ end
+
+ context 'with private profile' do
+ let(:author) { private_user }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'with a user that has the ability to read private profiles', :enable_admin_mode do
+ let(:user) { create(:admin) }
+
+ it_behaves_like 'renders contributed projects'
+ end
+ end
+ end
+ end
+ end
+
+ describe 'GET #starred' do
+ let(:project) { create(:project, :public) }
+
+ subject do
+ get :starred, params: { username: author.username }, format: format
+ end
+
+ before do
+ author.toggle_star(project)
+
+ sign_in(user)
+ subject
+ end
+
+ shared_examples_for 'renders starred projects' do
+ it 'renders starred projects' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns[:starred_projects]).not_to be_empty
+ end
+ end
+
+ %i(html json).each do |format|
+ context "format: #{format}" do
+ let(:format) { format }
+
+ context 'with public profile' do
+ let(:author) { public_user }
+
+ it_behaves_like 'renders starred projects'
+ end
+
+ context 'with private profile' do
+ let(:author) { private_user }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'with a user that has the ability to read private profiles', :enable_admin_mode do
+ let(:user) { create(:admin) }
+
+ it_behaves_like 'renders starred projects'
+ end
+ end
end
end
end
diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb
index e217638f62..f11a6d1789 100644
--- a/spec/features/explore/user_explores_projects_spec.rb
+++ b/spec/features/explore/user_explores_projects_spec.rb
@@ -34,6 +34,16 @@ RSpec.describe 'User explores projects' do
before do
sign_in(user)
+
+ stub_feature_flags(project_list_filter_bar: false)
+ end
+
+ shared_examples 'minimum search length' do
+ it 'shows a prompt to enter a longer search term', :js do
+ fill_in 'name', with: 'z'
+
+ expect(page).to have_content('Enter at least three characters to search')
+ end
end
context 'when viewing public projects' do
@@ -42,6 +52,7 @@ RSpec.describe 'User explores projects' do
end
include_examples 'shows public and internal projects'
+ include_examples 'minimum search length'
end
context 'when viewing most starred projects' do
@@ -50,6 +61,7 @@ RSpec.describe 'User explores projects' do
end
include_examples 'shows public and internal projects'
+ include_examples 'minimum search length'
end
context 'when viewing trending projects' do
@@ -62,6 +74,7 @@ RSpec.describe 'User explores projects' do
end
include_examples 'shows public projects'
+ include_examples 'minimum search length'
end
end
end
diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index bdb549326f..58314a49c4 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -19,6 +19,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue)
+ wait_for_requests
+ wait_for_mermaid
+
%w[A B C D].each do |label|
expect(page).to have_selector('svg text', text: label)
end
@@ -39,6 +42,7 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue)
wait_for_requests
+ wait_for_mermaid
expected = 'Line 1Line 2'
expect(page.html.scan(expected).count).to be(4)
@@ -65,6 +69,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue)
+ wait_for_requests
+ wait_for_mermaid
+
page.within('.description') do
expect(page).to have_selector('svg')
expect(page).to have_selector('pre.mermaid')
@@ -92,6 +99,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue)
+ wait_for_requests
+ wait_for_mermaid
+
page.within('.description') do
page.find('summary').click
svg = page.find('svg.mermaid')
@@ -118,6 +128,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue)
+ wait_for_requests
+ wait_for_mermaid
+
expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]')
end
@@ -147,6 +160,7 @@ RSpec.describe 'Mermaid rendering', :js do
end
wait_for_requests
+ wait_for_mermaid
find('.js-lazy-render-mermaid').click
@@ -156,4 +170,55 @@ RSpec.describe 'Mermaid rendering', :js do
expect(page).not_to have_selector('.js-lazy-render-mermaid-container')
end
end
+
+ it 'does not render more than 50 mermaid blocks', :js, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' } do
+ graph_edges = "A-->B;B-->A;"
+
+ description = <<~MERMAID
+ ```mermaid
+ graph LR
+ #{graph_edges}
+ ```
+ MERMAID
+
+ description *= 51
+
+ project = create(:project, :public)
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+ wait_for_mermaid
+
+ page.within('.description') do
+ expect(page).to have_selector('svg')
+
+ expect(page).to have_selector('.lazy-alert-shown')
+
+ expect(page).to have_selector('.js-lazy-render-mermaid-container')
+ end
+ end
+end
+
+def wait_for_mermaid
+ run_idle_callback = <<~RUN_IDLE_CALLBACK
+ window.requestIdleCallback(() => {
+ window.__CAPYBARA_IDLE_CALLBACK_EXEC__ = 1;
+ })
+ RUN_IDLE_CALLBACK
+
+ page.evaluate_script(run_idle_callback)
+
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until finished_rendering?
+ end
+end
+
+def finished_rendering?
+ check_idle_callback = <<~CHECK_IDLE_CALLBACK
+ window.__CAPYBARA_IDLE_CALLBACK_EXEC__
+ CHECK_IDLE_CALLBACK
+ page.evaluate_script(check_idle_callback) == 1
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 8ae19757c2..de59852590 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -157,6 +157,29 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([public_project]) }
end
+ describe 'filter by search with minimum search length' do
+ context 'when search term is shorter than minimum length' do
+ let(:params) { { search: 'C', minimum_search_length: 3 } }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when search term is longer than minimum length' do
+ let(:project) { create(:project, :public, group: group, name: 'test_project') }
+ let(:params) { { search: 'test', minimum_search_length: 3 } }
+
+ it { is_expected.to eq([project]) }
+ end
+
+ context 'when minimum length is invalid' do
+ let(:params) { { search: 'C', minimum_search_length: 'x' } }
+
+ it 'ignores the minimum length param' do
+ is_expected.to eq([public_project])
+ end
+ end
+ end
+
describe 'filter by group name' do
let(:params) { { name: group.name, search_namespaces: true } }
diff --git a/spec/finders/starred_projects_finder_spec.rb b/spec/finders/starred_projects_finder_spec.rb
index 15d4ae52dd..f5d3314021 100644
--- a/spec/finders/starred_projects_finder_spec.rb
+++ b/spec/finders/starred_projects_finder_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe StarredProjectsFinder do
let(:project1) { create(:project, :public, :empty_repo) }
let(:project2) { create(:project, :public, :empty_repo) }
- let(:other_project) { create(:project, :public, :empty_repo) }
+ let(:private_project) { create(:project, :private, :empty_repo) }
let(:user) { create(:user) }
let(:other_user) { create(:user) }
@@ -13,6 +13,9 @@ RSpec.describe StarredProjectsFinder do
before do
user.toggle_star(project1)
user.toggle_star(project2)
+
+ private_project.add_maintainer(user)
+ user.toggle_star(private_project)
end
describe '#execute' do
@@ -20,22 +23,56 @@ RSpec.describe StarredProjectsFinder do
subject { finder.execute }
- describe 'as same user' do
- let(:current_user) { user }
+ context 'user has a public profile' do
+ describe 'as same user' do
+ let(:current_user) { user }
- it { is_expected.to contain_exactly(project1, project2) }
+ it { is_expected.to contain_exactly(project1, project2, private_project) }
+ end
+
+ describe 'as other user' do
+ let(:current_user) { other_user }
+
+ it { is_expected.to contain_exactly(project1, project2) }
+ end
+
+ describe 'as no user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to contain_exactly(project1, project2) }
+ end
end
- describe 'as other user' do
- let(:current_user) { other_user }
+ context 'user has a private profile' do
+ before do
+ user.update!(private_profile: true)
+ end
- it { is_expected.to contain_exactly(project1, project2) }
- end
+ describe 'as same user' do
+ let(:current_user) { user }
- describe 'as no user' do
- let(:current_user) { nil }
+ it { is_expected.to contain_exactly(project1, project2, private_project) }
+ end
- it { is_expected.to contain_exactly(project1, project2) }
+ describe 'as other user' do
+ context 'user does not have access to view the private profile' do
+ let(:current_user) { other_user }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'user has access to view the private profile', :enable_admin_mode do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to contain_exactly(project1, project2, private_project) }
+ end
+ end
+
+ describe 'as no user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_empty }
+ end
end
end
end
diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
index 8a1bd3d172..b098058a73 100644
--- a/spec/requests/api/graphql/user/starred_projects_query_spec.rb
+++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
@@ -70,4 +70,31 @@ RSpec.describe 'Getting starredProjects of the user' do
)
end
end
+
+ context 'the user has a private profile' do
+ before do
+ user.update!(private_profile: true)
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'the current user does not have access to view the private profile of the user' do
+ let(:current_user) { create(:user) }
+
+ it 'finds no projects' do
+ expect(starred_projects).to be_empty
+ end
+ end
+
+ context 'the current user has access to view the private profile of the user' do
+ let(:current_user) { create(:admin) }
+
+ it 'finds all projects starred by the user, which the current user has access to' do
+ expect(starred_projects).to contain_exactly(
+ a_hash_including('id' => global_id_of(project_a)),
+ a_hash_including('id' => global_id_of(project_b)),
+ a_hash_including('id' => global_id_of(project_c))
+ )
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index 2f4dc0a916..bfd92f6344 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe 'getting user information' do
'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url,
'status' => presenter.status,
- 'email' => presenter.email
+ 'email' => presenter.public_email
))
end
@@ -210,7 +210,7 @@ RSpec.describe 'getting user information' do
context 'the user is private' do
before do
- user.update(private_profile: true)
+ user.update!(private_profile: true)
post_graphql(query, current_user: current_user)
end
@@ -220,6 +220,50 @@ RSpec.describe 'getting user information' do
it_behaves_like 'a working graphql query'
end
+ context 'we request the groupMemberships' do
+ let_it_be(:membership_a) { create(:group_member, :developer, user: user) }
+ let(:group_memberships) { graphql_data_at(:user, :group_memberships, :nodes) }
+ let(:user_fields) { 'groupMemberships { nodes { id } }' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'cannot be found' do
+ expect(group_memberships).to be_empty
+ end
+
+ context 'the current user is the user' do
+ let(:current_user) { user }
+
+ it 'can be found' do
+ expect(group_memberships).to include(
+ a_hash_including('id' => global_id_of(membership_a))
+ )
+ end
+ end
+ end
+
+ context 'we request the projectMemberships' do
+ let_it_be(:membership_a) { create(:project_member, user: user) }
+ let(:project_memberships) { graphql_data_at(:user, :project_memberships, :nodes) }
+ let(:user_fields) { 'projectMemberships { nodes { id } }' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'cannot be found' do
+ expect(project_memberships).to be_empty
+ end
+
+ context 'the current user is the user' do
+ let(:current_user) { user }
+
+ it 'can be found' do
+ expect(project_memberships).to include(
+ a_hash_including('id' => global_id_of(membership_a))
+ )
+ end
+ end
+ end
+
context 'we request the authoredMergeRequests' do
let(:user_fields) { 'authoredMergeRequests { nodes { id } }' }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 831b0d6e67..205c8655fa 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1255,13 +1255,46 @@ RSpec.describe API::Projects do
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'returns projects filtered by user' do
- get api("/users/#{user3.id}/starred_projects/", user)
+ context 'with a public profile' do
+ it 'returns projects filtered by user' do
+ get api("/users/#{user3.id}/starred_projects/", 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.map { |project| project['id'] }).to contain_exactly(project.id, project2.id, project3.id)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] })
+ .to contain_exactly(project.id, project2.id, project3.id)
+ end
+ end
+
+ context 'with a private profile' do
+ before do
+ user3.update!(private_profile: true)
+ user3.reload
+ end
+
+ context 'user does not have access to view the private profile' do
+ it 'returns no projects' do
+ get api("/users/#{user3.id}/starred_projects/", 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).to be_empty
+ end
+ end
+
+ context 'user has access to view the private profile' do
+ it 'returns projects filtered by user' do
+ get api("/users/#{user3.id}/starred_projects/", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] })
+ .to contain_exactly(project.id, project2.id, project3.id)
+ end
+ end
end
end
diff --git a/spec/validators/zoom_url_validator_spec.rb b/spec/validators/zoom_url_validator_spec.rb
new file mode 100644
index 0000000000..7d5c94bc24
--- /dev/null
+++ b/spec/validators/zoom_url_validator_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ZoomUrlValidator do
+ let(:zoom_meeting) { build(:zoom_meeting) }
+
+ describe 'validations' do
+ context 'when zoom link starts with https' do
+ it 'passes validation' do
+ zoom_meeting.url = 'https://zoom.us/j/123456789'
+
+ expect(zoom_meeting.valid?).to eq(true)
+ expect(zoom_meeting.errors).to be_empty
+ end
+ end
+
+ shared_examples 'zoom link does not start with https' do |url|
+ it 'fails validation' do
+ zoom_meeting.url = url
+ expect(zoom_meeting.valid?).to eq(false)
+
+ expect(zoom_meeting.errors).to be_present
+ expect(zoom_meeting.errors.first[1]).to eq 'must contain one valid Zoom URL'
+ end
+ end
+
+ context 'when zoom link does not start with https' do
+ include_examples 'zoom link does not start with https', 'http://zoom.us/j/123456789'
+
+ context 'when zoom link does not start with a scheme' do
+ include_examples 'zoom link does not start with https', 'testinghttp://zoom.us/j/123456789'
+ end
+ end
+ end
+end