Commit cd76e9bf authored by John T Skarbek's avatar John T Skarbek

Merge remote-tracking branch 'security/master'

parents 1d95630b 1b4cd17b
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 13.6.2 (2020-12-07)
### Security (1 change)
- Cleanup todos for confidential epics that are no longer accessible by the user.
## 13.6.1 (2020-11-23) ## 13.6.1 (2020-11-23)
- No changes. - No changes.
...@@ -184,6 +191,13 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -184,6 +191,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove duplicated BS display properties from member overriding UI. !47126 (Takuya Noguchi) - Remove duplicated BS display properties from member overriding UI. !47126 (Takuya Noguchi)
## 13.5.5 (2020-12-07)
### Security (1 change)
- Cleanup todos for confidential epics that are no longer accessible by the user.
## 13.5.4 (2020-11-13) ## 13.5.4 (2020-11-13)
### Fixed (1 change) ### Fixed (1 change)
...@@ -429,6 +443,13 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -429,6 +443,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove bootstrap class in licensed user count. !45443 - Remove bootstrap class in licensed user count. !45443
## 13.4.7 (2020-12-07)
### Security (1 change)
- Cleanup todos for confidential epics that are no longer accessible by the user.
## 13.4.6 (2020-11-03) ## 13.4.6 (2020-11-03)
### Fixed (1 change) ### Fixed (1 change)
......
...@@ -2,6 +2,22 @@ ...@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.6.2 (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.6.1 (2020-11-23) ## 13.6.1 (2020-11-23)
### Fixed (5 changes) ### Fixed (5 changes)
...@@ -529,6 +545,22 @@ entry. ...@@ -529,6 +545,22 @@ entry.
- Change wording on the project remove fork page. !47878 - Change wording on the project remove fork page. !47878
## 13.5.5 (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.5.4 (2020-11-13) ## 13.5.4 (2020-11-13)
### Fixed (4 changes) ### Fixed (4 changes)
...@@ -1148,6 +1180,22 @@ entry. ...@@ -1148,6 +1180,22 @@ entry.
- Bump cluster applications CI template. !45472 - Bump cluster applications CI template. !45472
## 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) ## 13.4.6 (2020-11-03)
### Fixed (1 change) ### Fixed (1 change)
......
...@@ -18,7 +18,13 @@ import { __, sprintf } from '~/locale'; ...@@ -18,7 +18,13 @@ import { __, sprintf } from '~/locale';
// //
// This is an arbitrary number; Can be iterated upon when suitable. // 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 = {}; let mermaidModule = {};
function importMermaidModule() { function importMermaidModule() {
...@@ -110,13 +116,22 @@ function renderMermaids($els) { ...@@ -110,13 +116,22 @@ function renderMermaids($els) {
let renderedChars = 0; let renderedChars = 0;
$els.each((i, el) => { $els.each((i, el) => {
// Skipping all the elements which we've already queued in requestIdleCallback
if (elsProcessingMap.has(el)) {
return;
}
const { source } = fixElementSource(el); const { source } = fixElementSource(el);
/** /**
* Restrict the rendering to a certain amount of character to * Restrict the rendering to a certain amount of character
* prevent mermaidjs from hanging up the entire thread and * and mermaid blocks to prevent mermaidjs from hanging
* causing a DoS. * 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 = ` const html = `
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert"> <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
<div> <div>
...@@ -146,8 +161,13 @@ function renderMermaids($els) { ...@@ -146,8 +161,13 @@ function renderMermaids($els) {
} }
renderedChars += source.length; renderedChars += source.length;
renderedMermaidBlocks += 1;
const requestId = window.requestIdleCallback(() => {
renderMermaidEl(el);
});
renderMermaidEl(el); elsProcessingMap.set(el, requestId);
}); });
}) })
.catch(err => { .catch(err => {
......
...@@ -8,6 +8,8 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -8,6 +8,8 @@ class Explore::ProjectsController < Explore::ApplicationController
include SortingHelper include SortingHelper
include SortingPreference include SortingPreference
MIN_SEARCH_LENGTH = 3
before_action :set_non_archived_param before_action :set_non_archived_param
before_action :set_sorting before_action :set_sorting
...@@ -72,7 +74,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -72,7 +74,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects def load_projects
load_project_counts 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 = preload_associations(projects)
projects = projects.page(params[:page]).without_count projects = projects.page(params[:page]).without_count
......
...@@ -76,7 +76,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController ...@@ -76,7 +76,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
end end
else else
respond_to do |format| respond_to do |format|
format.json { render_error_json(result[:message]) } format.json { render_error_json(result[:message], result[:http_status]) }
end end
end end
end end
...@@ -158,8 +158,8 @@ class Projects::FeatureFlagsController < Projects::ApplicationController ...@@ -158,8 +158,8 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
render json: feature_flag_json(feature_flag), status: :ok render json: feature_flag_json(feature_flag), status: :ok
end end
def render_error_json(messages) def render_error_json(messages, status = :bad_request)
render json: { message: messages }, render json: { message: messages },
status: :bad_request status: status
end end
end end
...@@ -122,7 +122,6 @@ class SearchController < ApplicationController ...@@ -122,7 +122,6 @@ class SearchController < ApplicationController
payload[:metadata] ||= {} payload[:metadata] ||= {}
payload[:metadata]['meta.search.group_id'] = params[:group_id] payload[:metadata]['meta.search.group_id'] = params[:group_id]
payload[:metadata]['meta.search.project_id'] = params[:project_id] payload[:metadata]['meta.search.project_id'] = params[:project_id]
payload[:metadata]['meta.search.search'] = params[:search]
payload[:metadata]['meta.search.scope'] = params[:scope] payload[:metadata]['meta.search.scope'] = params[:scope]
payload[:metadata]['meta.search.filters.confidential'] = params[:confidential] payload[:metadata]['meta.search.filters.confidential'] = params[:confidential]
payload[:metadata]['meta.search.filters.state'] = params[:state] payload[:metadata]['meta.search.filters.state'] = params[:state]
......
...@@ -19,7 +19,7 @@ class UsersController < ApplicationController ...@@ -19,7 +19,7 @@ class UsersController < ApplicationController
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists, :suggests] before_action :user, except: [:exists, :suggests]
before_action :authorize_read_user_profile!, 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]
feature_category :users feature_category :users
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
# personal: boolean # personal: boolean
# search: string # search: string
# search_namespaces: boolean # search_namespaces: boolean
# minimum_search_length: int
# non_archived: boolean # non_archived: boolean
# archived: 'only' or boolean # archived: 'only' or boolean
# min_access_level: integer # min_access_level: integer
...@@ -182,6 +183,9 @@ class ProjectsFinder < UnionFinder ...@@ -182,6 +183,9 @@ class ProjectsFinder < UnionFinder
def by_search(items) def by_search(items)
params[:search] ||= params[:name] 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?) items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class StarredProjectsFinder < ProjectsFinder class StarredProjectsFinder < ProjectsFinder
include Gitlab::Allowable
def initialize(user, params: {}, current_user: nil) def initialize(user, params: {}, current_user: nil)
@user = user
super( super(
params: params, params: params,
current_user: current_user, current_user: current_user,
project_ids_relation: user.starred_projects.select(:id) project_ids_relation: user.starred_projects.select(:id)
) )
end 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 end
...@@ -19,7 +19,8 @@ module Types ...@@ -19,7 +19,8 @@ module Types
field :state, Types::UserStateEnum, null: false, field :state, Types::UserStateEnum, null: false,
description: 'State of the user' description: 'State of the user'
field :email, GraphQL::STRING_TYPE, null: true, field :email, GraphQL::STRING_TYPE, null: true,
description: 'User email' description: 'User email', method: :public_email,
deprecated: { reason: 'Use public_email', milestone: '13.7' }
field :public_email, GraphQL::STRING_TYPE, null: true, field :public_email, GraphQL::STRING_TYPE, null: true,
description: "User's public email" description: "User's public email"
field :avatar_url, GraphQL::STRING_TYPE, null: true, field :avatar_url, GraphQL::STRING_TYPE, null: true,
...@@ -32,8 +33,7 @@ module Types ...@@ -32,8 +33,7 @@ module Types
resolver: Resolvers::TodoResolver, resolver: Resolvers::TodoResolver,
description: 'Todos of the user' description: 'Todos of the user'
field :group_memberships, Types::GroupMemberType.connection_type, null: true, field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user', description: 'Group memberships of the user'
method: :group_members
field :group_count, GraphQL::INT_TYPE, null: true, field :group_count, GraphQL::INT_TYPE, null: true,
resolver: Resolvers::Users::GroupCountResolver, resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user', description: 'Group count for the user',
...@@ -43,8 +43,7 @@ module Types ...@@ -43,8 +43,7 @@ module Types
field :location, ::GraphQL::STRING_TYPE, null: true, field :location, ::GraphQL::STRING_TYPE, null: true,
description: 'The location of the user.' description: 'The location of the user.'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true, field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
description: 'Project memberships of the user', description: 'Project memberships of the user'
method: :project_members
field :starred_projects, Types::ProjectType.connection_type, null: true, field :starred_projects, Types::ProjectType.connection_type, null: true,
description: 'Projects starred by the user', description: 'Projects starred by the user',
resolver: Resolvers::UserStarredProjectsResolver resolver: Resolvers::UserStarredProjectsResolver
......
...@@ -28,6 +28,11 @@ module Operations ...@@ -28,6 +28,11 @@ module Operations
fuzzy_search(query, [:name], use_minimum_char_limit: false) fuzzy_search(query, [:name], use_minimum_char_limit: false)
end end
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 private
def ensure_no_associated_strategies def ensure_no_associated_strategies
......
...@@ -2,4 +2,18 @@ ...@@ -2,4 +2,18 @@
class UserPresenter < Gitlab::View::Presenter::Delegated class UserPresenter < Gitlab::View::Presenter::Delegated
presents :user 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 end
...@@ -10,6 +10,7 @@ module FeatureFlags ...@@ -10,6 +10,7 @@ module FeatureFlags
def execute(feature_flag) def execute(feature_flag)
return error('Access Denied', 403) unless can_update?(feature_flag) return error('Access Denied', 403) unless can_update?(feature_flag)
return error('Not Found', 404) unless valid_user_list_ids?(feature_flag, user_list_ids(params))
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
feature_flag.assign_attributes(params) feature_flag.assign_attributes(params)
...@@ -87,5 +88,15 @@ module FeatureFlags ...@@ -87,5 +88,15 @@ module FeatureFlags
def can_update?(feature_flag) def can_update?(feature_flag)
Ability.allowed?(current_user, :update_feature_flag, feature_flag) Ability.allowed?(current_user, :update_feature_flag, feature_flag)
end end
def user_list_ids(params)
params.fetch(:strategies_attributes, [])
.select { |s| s[:user_list_id].present? }
.map { |s| s[:user_list_id] }
end
def valid_user_list_ids?(feature_flag, user_list_ids)
user_list_ids.empty? || ::Operations::FeatureFlags::UserList.belongs_to?(feature_flag.project_id, user_list_ids)
end
end end
end end
...@@ -22,7 +22,7 @@ module Todos ...@@ -22,7 +22,7 @@ module Todos
# if at least reporter, all entities including confidential issues can be accessed # if at least reporter, all entities including confidential issues can be accessed
return if user_has_reporter_access? return if user_has_reporter_access?
remove_confidential_issue_todos remove_confidential_resource_todos
if entity.private? if entity.private?
remove_project_todos remove_project_todos
...@@ -40,7 +40,7 @@ module Todos ...@@ -40,7 +40,7 @@ module Todos
end end
end end
def remove_confidential_issue_todos def remove_confidential_resource_todos
Todo Todo
.for_target(confidential_issues.select(:id)) .for_target(confidential_issues.select(:id))
.for_type(Issue.name) .for_type(Issue.name)
...@@ -133,3 +133,5 @@ module Todos ...@@ -133,3 +133,5 @@ module Todos
end end
end end
end end
Todos::Destroy::EntityLeaveService.prepend_if_ee('EE::Todos::Destroy::EntityLeaveService')
...@@ -5,8 +5,13 @@ ...@@ -5,8 +5,13 @@
# Custom validator for zoom urls # Custom validator for zoom urls
# #
class ZoomUrlValidator < ActiveModel::EachValidator class ZoomUrlValidator < ActiveModel::EachValidator
ALLOWED_SCHEMES = %w(https).freeze
def validate_each(record, attribute, value) 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') record.errors.add(:url, 'must contain one valid Zoom URL')
end end
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= render "devise/shared/error_messages", resource: resource = render "devise/shared/error_messages", resource: resource
.form-group .form-group
= f.label :email = 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 .clearfix
= f.submit "Resend", class: 'gl-button btn btn-success' = f.submit "Resend", class: 'gl-button btn btn-success'
......
= render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, 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
= render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true)
...@@ -137,6 +137,7 @@ module Gitlab ...@@ -137,6 +137,7 @@ module Gitlab
encrypted_key encrypted_key
import_url import_url
elasticsearch_url elasticsearch_url
search
otp_attempt otp_attempt
sentry_dsn sentry_dsn
trace trace
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
- dynamic_application_security_testing - dynamic_application_security_testing
- editor_extension - editor_extension
- epics - epics
- epic_tracking
- error_tracking - error_tracking
- feature_flags - feature_flags
- five_minute_production_app - five_minute_production_app
......
# 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
ae8034ec52df47ce2ce3397715dd18347e4d297a963c17c7b26321f414dfa632
\ No newline at end of file
...@@ -23823,9 +23823,9 @@ type User { ...@@ -23823,9 +23823,9 @@ type User {
avatarUrl: String avatarUrl: String
""" """
User email User email. Deprecated in 13.7: Use public_email
""" """
email: String email: String @deprecated(reason: "Use public_email. Deprecated in 13.7")
""" """
Group count for the user. Available only when feature flag `user_group_counts` is enabled Group count for the user. Available only when feature flag `user_group_counts` is enabled
......
...@@ -69304,7 +69304,7 @@ ...@@ -69304,7 +69304,7 @@
}, },
{ {
"name": "email", "name": "email",
"description": "User email", "description": "User email. Deprecated in 13.7: Use public_email",
"args": [ "args": [
], ],
...@@ -69313,8 +69313,8 @@ ...@@ -69313,8 +69313,8 @@
"name": "String", "name": "String",
"ofType": null "ofType": null
}, },
"isDeprecated": false, "isDeprecated": true,
"deprecationReason": null "deprecationReason": "Use public_email. Deprecated in 13.7"
}, },
{ {
"name": "groupCount", "name": "groupCount",
...@@ -3596,7 +3596,7 @@ Autogenerated return type of UpdateSnippet. ...@@ -3596,7 +3596,7 @@ Autogenerated return type of UpdateSnippet.
| `assignedMergeRequests` | MergeRequestConnection | Merge Requests assigned to the user | | `assignedMergeRequests` | MergeRequestConnection | Merge Requests assigned to the user |
| `authoredMergeRequests` | MergeRequestConnection | Merge Requests authored by the user | | `authoredMergeRequests` | MergeRequestConnection | Merge Requests authored by the user |
| `avatarUrl` | String | URL of the user's avatar | | `avatarUrl` | String | URL of the user's avatar |
| `email` | String | User email | | `email` **{warning-solid}** | String | **Deprecated:** Use public_email. Deprecated in 13.7 |
| `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled | | `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled |
| `groupMemberships` | GroupMemberConnection | Group memberships of the user | | `groupMemberships` | GroupMemberConnection | Group memberships of the user |
| `id` | ID! | ID of the user | | `id` | ID! | ID of the user |
......
...@@ -64,7 +64,7 @@ To-do item triggers aren't affected by [GitLab notification email settings](prof ...@@ -64,7 +64,7 @@ To-do item triggers aren't affected by [GitLab notification email settings](prof
NOTE: NOTE:
When a user no longer has access to a resource related to a to-do item (such as When a user no longer has access to a resource related to a to-do item (such as
an issue, merge request, project, or group), for security reasons GitLab an 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 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. prevent data loss, in the case where a user's access is accidentally revoked.
......
# frozen_string_literal: true
module EE
module Todos
module Destroy
module EntityLeaveService
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :remove_confidential_resource_todos
def remove_confidential_resource_todos
super
return unless entity.is_a?(Namespace)
::Todo
.for_target(confidential_epics.select(:id))
.for_type(::Epic.name)
.for_user(user)
.delete_all
end
private
def confidential_epics
::Epic
.in_selected_groups(non_authorized_reporter_groups)
.confidential
end
end
end
end
end
...@@ -34,6 +34,11 @@ module Epics ...@@ -34,6 +34,11 @@ module Epics
end end
todo_service.update_epic(epic, current_user, old_mentioned_users) todo_service.update_epic(epic, current_user, old_mentioned_users)
if epic.previous_changes.include?('confidential') && epic.confidential?
# don't enqueue immediately to prevent todos removal in case of a mistake
::TodosDestroyer::ConfidentialEpicWorker.perform_in(::Todo::WAIT_FOR_DELETE, epic.id)
end
end end
def handle_task_changes(epic) def handle_task_changes(epic)
......
# frozen_string_literal: true
module Todos
module Destroy
# Service class for deleting todos that belong to confidential epics.
# It deletes todos for users that are not at least reporters.
class ConfidentialEpicService < ::Todos::Destroy::BaseService
extend ::Gitlab::Utils::Override
attr_reader :epic
def initialize(epic_id:)
@epic = ::Epic.find_by_id(epic_id)
end
private
override :todos
def todos
epic.todos
end
override :todos_to_remove?
def todos_to_remove?
epic&.confidential?
end
override :authorized_users
def authorized_users
epic.group.members_with_parents.non_guests.select(:user_id)
end
end
end
end
...@@ -581,6 +581,14 @@ ...@@ -581,6 +581,14 @@
:weight: 2 :weight: 2
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: todos_destroyer:todos_destroyer_confidential_epic
:feature_category: :epic_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: adjourned_project_deletion - :name: adjourned_project_deletion
:feature_category: :authentication_and_authorization :feature_category: :authentication_and_authorization
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module TodosDestroyer
class ConfidentialEpicWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
queue_namespace :todos_destroyer
feature_category :epic_tracking
def perform(epic_id)
return unless epic_id
::Todos::Destroy::ConfidentialEpicService.new(epic_id: epic_id).execute
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module EE
module Gitlab
module BackgroundMigration
module RemoveInaccessibleEpicTodos
extend ::Gitlab::Utils::Override
class User < ActiveRecord::Base
end
class Todo < ActiveRecord::Base
belongs_to :epic, foreign_key: :target_id
belongs_to :user
end
class Member < ActiveRecord::Base
include FromUnion
self.inheritance_column = :_type_disabled
end
class GroupGroupLink < ActiveRecord::Base
end
class Epic < ActiveRecord::Base
belongs_to :group
def can_read_confidential?(user)
group.max_member_access_for_user(user) >= ::Gitlab::Access::REPORTER
end
end
class Group < ActiveRecord::Base
self.table_name = 'namespaces'
self.inheritance_column = :_type_disabled
def max_member_access_for_user(user)
max_member_access = members_with_parents.where(user_id: user)
.reorder(access_level: :desc)
.first
&.access_level
max_member_access || ::Gitlab::Access::NO_ACCESS
end
def members_with_parents
group_hierarchy_members = Member.where(source_type: 'Namespace', source_id: source_ids)
Member.from_union([group_hierarchy_members,
members_from_self_and_ancestor_group_shares])
end
# rubocop:disable Metrics/AbcSize
# this is taken from Group model, so instead of doing additional
# refactoring let's keep it close to the original
def members_from_self_and_ancestor_group_shares
group_group_link_table = GroupGroupLink.arel_table
group_member_table = Member.arel_table
group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
cte = ::Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
cte_alias = cte.table.alias(GroupGroupLink.table_name)
# Instead of members.access_level, we need to maximize that access_level at
# the respective group_group_links.group_access.
member_columns = Member.attribute_names.map do |column_name|
if column_name == 'access_level'
smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
'access_level')
else
group_member_table[column_name]
end
end
Member
.with(cte.to_arel)
.select(*member_columns)
.from([group_member_table, cte.alias_to(group_group_link_table)])
.where(group_member_table[:requested_at].eq(nil))
.where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
.where(group_member_table[:source_type].eq('Namespace'))
end
# rubocop:enable Metrics/AbcSize
def source_ids
return id unless parent_id
::Gitlab::ObjectHierarchy
.new(self.class.where(id: id))
.base_and_ancestors
.reorder(nil).select(:id)
end
def smallest_value_arel(args, column_alias)
Arel::Nodes::As.new(
Arel::Nodes::NamedFunction.new('LEAST', args),
Arel::Nodes::SqlLiteral.new(column_alias))
end
end
override :perform
def perform(start_id, stop_id)
confidential_epic_ids = Epic.where(confidential: true).where(id: start_id..stop_id).ids
epic_todos = Todo
.where(target_type: 'Epic', target_id: confidential_epic_ids)
.includes(:epic, :user)
ids_to_delete = not_readable_epic_todo_ids(epic_todos)
logger.info(message: 'Deleting confidential epic todos', todo_ids: ids_to_delete)
Todo.where(id: ids_to_delete).delete_all
end
private
def not_readable_epic_todo_ids(todos)
todos.map do |todo|
next todo.id unless todo.epic
next if todo.epic.can_read_confidential?(todo.user)
todo.id
end.compact
end
def logger
@logger ||= ::Gitlab::BackgroundMigration::Logger.build
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RemoveInaccessibleEpicTodos, schema: 20201109114603 do
include MigrationHelpers::NamespacesHelpers
let(:users) { table(:users) }
let(:todos) { table(:todos) }
let(:epics) { table(:epics) }
let(:members_table) { table(:members) }
let(:group_group_links) { table(:group_group_links) }
let(:author) { users.create!(email: 'author@example.com', projects_limit: 10) }
let(:user) { users.create!(email: 'user@example.com', projects_limit: 10) }
let(:group_root) { create_namespace('root', Gitlab::VisibilityLevel::PUBLIC) }
let(:group_level1) { create_namespace('level1', Gitlab::VisibilityLevel::PUBLIC, parent_id: group_root.id) }
let(:epic_conf1) { epics.create!(iid: 1, title: 'confidential1', title_html: 'confidential1', confidential: true, group_id: group_root.id, author_id: author.id) }
let(:epic_conf2) { epics.create!(iid: 1, title: 'confidential2', title_html: 'confidential2', confidential: true, group_id: group_level1.id, author_id: author.id) }
let(:epic_public1) { epics.create!(iid: 2, title: 'public1', title_html: 'epic_public1', group_id: group_root.id, author_id: author.id) }
let(:epic_public2) { epics.create!(iid: 2, title: 'public1', title_html: 'epic_public2', group_id: group_level1.id, author_id: author.id) }
let!(:todo1) { todos.create!(target_type: 'Epic', target_id: epic_conf1.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
let!(:todo2) { todos.create!(target_type: 'Epic', target_id: epic_conf2.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
let!(:todo3) { todos.create!(target_type: 'Epic', target_id: epic_public1.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
let!(:todo4) { todos.create!(target_type: 'Epic', target_id: epic_public2.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
describe '#perform' do
subject(:perform) { described_class.new.perform(epics.first.id, epics.last.id) }
def expect_todos(preserved:)
expect { subject }.to change { todos.count }.by(preserved.count - 4)
existing_ids = todos.pluck(:id)
expect(existing_ids).to match_array(preserved)
end
context 'when user is not member of related groups' do
it 'deletes only todos referencing confidential epics' do
expect_todos(preserved: [todo3.id, todo4.id])
end
end
context 'when user is only guest member of related groups' do
let!(:member) do
members_table.create!(user_id: user.id, source_id: group_root.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 10, notification_level: 3)
end
it 'deletes todos referencing confidential epics' do
expect_todos(preserved: [todo3.id, todo4.id])
end
end
context 'when user is member of subgroup' do
let!(:member) do
members_table.create!(user_id: user.id, source_id: group_level1.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'deletes only epic todos in the root group' do
expect_todos(preserved: [todo2.id, todo3.id, todo4.id])
end
end
context 'when user is member of root group' do
let!(:member) do
members_table.create!(user_id: user.id, source_id: group_root.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'does not delete any todos' do
expect_todos(preserved: [todo1.id, todo2.id, todo3.id, todo4.id])
end
end
context 'when user is only guest on root group' do
let!(:root_member) do
members_table.create!(user_id: user.id, source_id: group_root.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 10, notification_level: 3)
end
let!(:subgroup_member) do
members_table.create!(user_id: user.id, source_id: group_level1.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'deletes only root confidential epic todo' do
expect_todos(preserved: [todo2.id, todo3.id, todo4.id])
end
end
context 'when root group is shared with other group' do
let!(:other_group) { create_namespace('other_group', Gitlab::VisibilityLevel::PRIVATE) }
let!(:member) do
members_table.create!(user_id: user.id, source_id: other_group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
let!(:group_link) do
group_group_links.create!(shared_group_id: group_root.id,
shared_with_group_id: other_group.id, group_access: 20)
end
it 'does not delete any todos' do
expect_todos(preserved: [todo1.id, todo2.id, todo3.id, todo4.id])
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20201109114603_schedule_remove_inaccessible_epic_todos')
RSpec.describe ScheduleRemoveInaccessibleEpicTodos do
let(:group) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
let(:user) { table(:users).create!(email: 'user@example.com', projects_limit: 10) }
let!(:epic1) { table(:epics).create!(iid: 1, title: 'foo', title_html: 'foo', group_id: group.id, author_id: user.id, confidential: true) }
let!(:epic2) { table(:epics).create!(iid: 2, title: 'foo', title_html: 'foo', group_id: group.id, author_id: user.id) }
let!(:epic3) { table(:epics).create!(iid: 3, title: 'foo', title_html: 'foo', group_id: group.id, author_id: user.id, confidential: true) }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
end
it 'schedules jobs for confidental epic todos' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
2.minutes, epic1.id, epic1.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
4.minutes, epic3.id, epic3.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Todos::Destroy::EntityLeaveService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:epic1) { create(:epic, confidential: true, group: subgroup) }
let_it_be(:epic2) { create(:epic, group: subgroup) }
let!(:todo1) { create(:todo, target: epic1, user: user, group: subgroup) }
let!(:todo2) { create(:todo, target: epic2, user: user, group: subgroup) }
describe '#execute' do
subject { described_class.new(user.id, subgroup.id, 'Group').execute }
shared_examples 'removes only confidential epics todos' do
it 'removes todos targeting confidential epics in the group' do
expect { subject }.to change { Todo.count }.by(-1)
expect(user.reload.todos.ids).to match_array(todo2.id)
end
end
it_behaves_like 'removes only confidential epics todos'
context 'when user is still member of ancestor group' do
before do
group.add_reporter(user)
end
it 'does not remove todos targeting confidential epics in the group' do
expect { subject }.not_to change { Todo.count }
end
end
context 'when user role is downgraded to guest' do
before do
subgroup.add_guest(user)
end
it_behaves_like 'removes only confidential epics todos'
end
end
end
...@@ -216,6 +216,12 @@ RSpec.describe Epics::UpdateService do ...@@ -216,6 +216,12 @@ RSpec.describe Epics::UpdateService do
end end
end end
end end
it 'schedules deletion of todos when epic becomes confidential' do
expect(TodosDestroyer::ConfidentialEpicWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, epic.id)
update_epic(confidential: true)
end
end end
context 'when Epic has tasks' do context 'when Epic has tasks' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Todos::Destroy::ConfidentialEpicService do
let_it_be(:group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:group_member) { create(:user) }
let_it_be(:shared_user) { create(:user) }
let_it_be(:group_link) { create(:group_group_link, shared_group: group) }
let_it_be(:epic_1, reload: true) { create(:epic, :confidential, group: group, author: author) }
let!(:todos) do
[
# todos not to be deleted
create(:todo, user: group_member, target: epic_1, group: group),
create(:todo, user: user, group: group),
create(:todo, user: shared_user, target: epic_1, group: group),
# Todos to be deleted
create(:todo, user: guest, target: epic_1, group: group),
create(:todo, user: user, target: epic_1, group: group)
]
end
describe '#execute' do
before do
group.add_reporter(group_member)
group.add_guest(guest)
group_link.shared_with_group.add_reporter(shared_user)
end
subject { described_class.new(epic_id: epic_1.id).execute }
it 'removes epic todos for users who can not access the confidential epic' do
expect { subject }.to change { Todo.count }.by(-2)
end
context 'when provided epic is not confidential' do
before do
epic_1.update!(confidential: false)
end
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TodosDestroyer::ConfidentialEpicWorker do
let(:service) { double }
it 'calls the Todos::Destroy::ConfidentialEpicService with epic_id parameter' do
expect(::Todos::Destroy::ConfidentialEpicService).to receive(:new).with(epic_id: 100).and_return(service)
expect(service).to receive(:execute)
described_class.new.perform(100)
end
end
# 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')
# 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
...@@ -1419,6 +1419,40 @@ RSpec.describe Projects::FeatureFlagsController do ...@@ -1419,6 +1419,40 @@ RSpec.describe Projects::FeatureFlagsController do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'returns not found when trying to update a gitlabUserList strategy with a user list from another project' do
user_list = create(:operations_feature_flag_user_list, project: project, name: 'My List', user_xids: 'user1,user2')
strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list)
other_project = create(:project)
other_user_list = create(:operations_feature_flag_user_list, project: other_project, name: 'Other List', user_xids: 'some,one')
put_request(new_version_flag, strategies_attributes: [{
id: strategy.id,
user_list_id: other_user_list.id
}])
expect(response).to have_gitlab_http_status(:not_found)
expect(strategy.reload.user_list).to eq(user_list)
end
it 'allows setting multiple gitlabUserList strategies to the same user list' do
user_list_a = create(:operations_feature_flag_user_list, project: project, name: 'My List A', user_xids: 'user1,user2')
user_list_b = create(:operations_feature_flag_user_list, project: project, name: 'My List B', user_xids: 'user3,user4')
strategy_a = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list_a)
strategy_b = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list_a)
put_request(new_version_flag, strategies_attributes: [{
id: strategy_a.id,
user_list_id: user_list_b.id
}, {
id: strategy_b.id,
user_list_id: user_list_b.id
}])
expect(response).to have_gitlab_http_status(:ok)
expect(strategy_a.reload.user_list).to eq(user_list_b)
expect(strategy_b.reload.user_list).to eq(user_list_b)
end
it 'updates an existing strategy' do it 'updates an existing strategy' do
strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'default', parameters: {}) strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'default', parameters: {})
......
...@@ -272,7 +272,7 @@ RSpec.describe SearchController do ...@@ -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.group_id']).to eq('123')
expect(last_payload[:metadata]['meta.search.project_id']).to eq('456') 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') expect(last_payload[:metadata]['meta.search.scope']).to eq('issues')
expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true') expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true')
expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true') expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true')
......
...@@ -354,32 +354,99 @@ RSpec.describe UsersController do ...@@ -354,32 +354,99 @@ RSpec.describe UsersController do
describe 'GET #contributed' do describe 'GET #contributed' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:current_user) { create(:user) }
subject do
get :contributed, params: { username: author.username }, format: format
end
before do before do
sign_in(current_user) sign_in(user)
project.add_developer(public_user) project.add_developer(public_user)
project.add_developer(private_user) project.add_developer(private_user)
create(:push_event, project: project, author: author)
subject
end end
context 'with public profile' do shared_examples_for 'renders contributed projects' do
it 'renders contributed projects' do it 'renders contributed projects' do
create(:push_event, project: project, author: public_user) expect(assigns[:contributed_projects]).not_to be_empty
expect(response).to have_gitlab_http_status(:ok)
end
end
get :contributed, params: { username: public_user.username } %i(html json).each do |format|
context "format: #{format}" do
let(:format) { format }
expect(assigns[:contributed_projects]).not_to be_empty context 'with public profile' do
let(:author) { public_user }
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
end end
context 'with private profile' do %i(html json).each do |format|
it 'does not render contributed projects' do context "format: #{format}" do
create(:push_event, project: project, author: private_user) 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
get :contributed, params: { username: private_user.username } context 'with a user that has the ability to read private profiles', :enable_admin_mode do
let(:user) { create(:admin) }
expect(assigns[:contributed_projects]).to be_empty it_behaves_like 'renders starred projects'
end
end
end end
end end
end end
......
...@@ -47,6 +47,14 @@ RSpec.describe 'User explores projects' do ...@@ -47,6 +47,14 @@ RSpec.describe 'User explores projects' do
end end
end 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 context 'when viewing public projects' do
before do before do
visit(explore_projects_path) visit(explore_projects_path)
...@@ -54,6 +62,7 @@ RSpec.describe 'User explores projects' do ...@@ -54,6 +62,7 @@ RSpec.describe 'User explores projects' do
include_examples 'shows public and internal projects' include_examples 'shows public and internal projects'
include_examples 'empty search results' include_examples 'empty search results'
include_examples 'minimum search length'
end end
context 'when viewing most starred projects' do context 'when viewing most starred projects' do
...@@ -63,6 +72,7 @@ RSpec.describe 'User explores projects' do ...@@ -63,6 +72,7 @@ RSpec.describe 'User explores projects' do
include_examples 'shows public and internal projects' include_examples 'shows public and internal projects'
include_examples 'empty search results' include_examples 'empty search results'
include_examples 'minimum search length'
end end
context 'when viewing trending projects' do context 'when viewing trending projects' do
...@@ -76,6 +86,7 @@ RSpec.describe 'User explores projects' do ...@@ -76,6 +86,7 @@ RSpec.describe 'User explores projects' do
include_examples 'shows public projects' include_examples 'shows public projects'
include_examples 'empty search results' include_examples 'empty search results'
include_examples 'minimum search length'
end end
end end
end end
......
...@@ -19,6 +19,9 @@ RSpec.describe 'Mermaid rendering', :js do ...@@ -19,6 +19,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
%w[A B C D].each do |label| %w[A B C D].each do |label|
expect(page).to have_selector('svg text', text: label) expect(page).to have_selector('svg text', text: label)
end end
...@@ -39,6 +42,7 @@ RSpec.describe 'Mermaid rendering', :js do ...@@ -39,6 +42,7 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests wait_for_requests
wait_for_mermaid
expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>' expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
expect(page.html.scan(expected).count).to be(4) expect(page.html.scan(expected).count).to be(4)
...@@ -65,6 +69,9 @@ RSpec.describe 'Mermaid rendering', :js do ...@@ -65,6 +69,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
page.within('.description') do page.within('.description') do
expect(page).to have_selector('svg') expect(page).to have_selector('svg')
expect(page).to have_selector('pre.mermaid') expect(page).to have_selector('pre.mermaid')
...@@ -92,6 +99,9 @@ RSpec.describe 'Mermaid rendering', :js do ...@@ -92,6 +99,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
page.within('.description') do page.within('.description') do
page.find('summary').click page.find('summary').click
svg = page.find('svg.mermaid') svg = page.find('svg.mermaid')
...@@ -118,6 +128,9 @@ RSpec.describe 'Mermaid rendering', :js do ...@@ -118,6 +128,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]') expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]')
end end
...@@ -147,6 +160,7 @@ RSpec.describe 'Mermaid rendering', :js do ...@@ -147,6 +160,7 @@ RSpec.describe 'Mermaid rendering', :js do
end end
wait_for_requests wait_for_requests
wait_for_mermaid
find('.js-lazy-render-mermaid').click find('.js-lazy-render-mermaid').click
...@@ -156,4 +170,55 @@ RSpec.describe 'Mermaid rendering', :js do ...@@ -156,4 +170,55 @@ RSpec.describe 'Mermaid rendering', :js do
expect(page).not_to have_selector('.js-lazy-render-mermaid-container') expect(page).not_to have_selector('.js-lazy-render-mermaid-container')
end end
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 end
...@@ -161,6 +161,29 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -161,6 +161,29 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end 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 describe 'filter by group name' do
let(:params) { { name: group.name, search_namespaces: true } } let(:params) { { name: group.name, search_namespaces: true } }
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe StarredProjectsFinder do RSpec.describe StarredProjectsFinder do
let(:project1) { create(:project, :public, :empty_repo) } let(:project1) { create(:project, :public, :empty_repo) }
let(:project2) { 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(:user) { create(:user) }
let(:other_user) { create(:user) } let(:other_user) { create(:user) }
...@@ -13,6 +13,9 @@ RSpec.describe StarredProjectsFinder do ...@@ -13,6 +13,9 @@ RSpec.describe StarredProjectsFinder do
before do before do
user.toggle_star(project1) user.toggle_star(project1)
user.toggle_star(project2) user.toggle_star(project2)
private_project.add_maintainer(user)
user.toggle_star(private_project)
end end
describe '#execute' do describe '#execute' do
...@@ -20,22 +23,56 @@ RSpec.describe StarredProjectsFinder do ...@@ -20,22 +23,56 @@ RSpec.describe StarredProjectsFinder do
subject { finder.execute } subject { finder.execute }
describe 'as same user' do context 'user has a public profile' do
let(:current_user) { user } 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 end
describe 'as other user' do
let(:current_user) { other_user }
describe 'as other user' do it { is_expected.to contain_exactly(project1, project2) }
let(:current_user) { other_user } end
it { is_expected.to contain_exactly(project1, project2) } describe 'as no user' do
let(:current_user) { nil }
it { is_expected.to contain_exactly(project1, project2) }
end
end end
describe 'as no user' do context 'user has a private profile' do
let(:current_user) { nil } before do
user.update!(private_profile: true)
end
describe 'as same user' do
let(:current_user) { user }
it { is_expected.to contain_exactly(project1, project2, private_project) }
end
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 contain_exactly(project1, project2) } it { is_expected.to be_empty }
end
end end
end end
end end
...@@ -70,4 +70,31 @@ RSpec.describe 'Getting starredProjects of the user' do ...@@ -70,4 +70,31 @@ RSpec.describe 'Getting starredProjects of the user' do
) )
end end
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 end
...@@ -82,7 +82,7 @@ RSpec.describe 'getting user information' do ...@@ -82,7 +82,7 @@ RSpec.describe 'getting user information' do
'username' => presenter.username, 'username' => presenter.username,
'webUrl' => presenter.web_url, 'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url, 'avatarUrl' => presenter.avatar_url,
'email' => presenter.email, 'email' => presenter.public_email,
'publicEmail' => presenter.public_email 'publicEmail' => presenter.public_email
)) ))
...@@ -251,7 +251,7 @@ RSpec.describe 'getting user information' do ...@@ -251,7 +251,7 @@ RSpec.describe 'getting user information' do
context 'the user is private' do context 'the user is private' do
before do before do
user.update(private_profile: true) user.update!(private_profile: true)
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
...@@ -261,6 +261,50 @@ RSpec.describe 'getting user information' do ...@@ -261,6 +261,50 @@ RSpec.describe 'getting user information' do
it_behaves_like 'a working graphql query' it_behaves_like 'a working graphql query'
end end
context 'we request the groupMemberships' do
let_it_be(:membership_a) { create(:group_member, 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 context 'we request the authoredMergeRequests' do
let(:user_fields) { 'authoredMergeRequests { nodes { id } }' } let(:user_fields) { 'authoredMergeRequests { nodes { id } }' }
......
...@@ -1255,13 +1255,46 @@ RSpec.describe API::Projects do ...@@ -1255,13 +1255,46 @@ RSpec.describe API::Projects do
expect(json_response['message']).to eq('404 User Not Found') expect(json_response['message']).to eq('404 User Not Found')
end end
it 'returns projects filtered by user' do context 'with a public profile' do
get api("/users/#{user3.id}/starred_projects/", user) 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 have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, project2.id, project3.id) 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
end end
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment