summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js3
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue9
-rw-r--r--app/assets/javascripts/notes/constants.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue33
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss12
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/concerns/issuable_actions.rb7
-rw-r--r--app/controllers/concerns/issuable_collections.rb10
-rw-r--r--app/controllers/concerns/project_unauthorized.rb19
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/finders/issuable_finder.rb30
-rw-r--r--app/finders/issues_finder.rb14
-rw-r--r--app/helpers/application_settings_helper.rb45
-rw-r--r--app/helpers/boards_helper.rb2
-rw-r--r--app/helpers/form_helper.rb39
-rw-r--r--app/helpers/issuables_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb10
-rw-r--r--app/mailers/emails/merge_requests.rb6
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/application_setting.rb34
-rw-r--r--app/models/concerns/deprecated_assignee.rb86
-rw-r--r--app/models/concerns/issuable.rb41
-rw-r--r--app/models/issue.rb32
-rw-r--r--app/models/merge_request.rb51
-rw-r--r--app/models/project.rb9
-rw-r--r--app/policies/base_policy.rb9
-rw-r--r--app/policies/project_policy.rb28
-rw-r--r--app/serializers/issuable_sidebar_extras_entity.rb2
-rw-r--r--app/serializers/issue_sidebar_extras_entity.rb1
-rw-r--r--app/serializers/merge_request_assignee_entity.rb7
-rw-r--r--app/serializers/merge_request_basic_entity.rb3
-rw-r--r--app/serializers/merge_request_serializer.rb4
-rw-r--r--app/serializers/merge_request_sidebar_basic_entity.rb11
-rw-r--r--app/serializers/merge_request_sidebar_extras_entity.rb7
-rw-r--r--app/services/application_settings/update_service.rb8
-rw-r--r--app/services/concerns/validates_classification_label.rb27
-rw-r--r--app/services/issuable_base_service.rb18
-rw-r--r--app/services/issues/base_service.rb22
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb6
-rw-r--r--app/services/merge_requests/update_service.rb18
-rw-r--r--app/services/notification_recipient_service.rb24
-rw-r--r--app/services/notification_service.rb20
-rw-r--r--app/services/projects/create_service.rb4
-rw-r--r--app/services/projects/update_service.rb3
-rw-r--r--app/services/system_note_service.rb10
-rw-r--r--app/services/todo_service.rb16
-rw-r--r--app/validators/x509_certificate_credentials_validator.rb86
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml51
-rw-r--r--app/views/admin/application_settings/show.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/nav/_classification_level_banner.html.haml5
-rw-r--r--app/views/notify/_reassigned_issuable_email.html.haml10
-rw-r--r--app/views/notify/closed_merge_request_email.text.haml2
-rw-r--r--app/views/notify/issue_due_email.html.haml2
-rw-r--r--app/views/notify/issue_due_email.text.erb2
-rw-r--r--app/views/notify/merge_request_status_email.text.haml2
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml2
-rw-r--r--app/views/notify/new_issue_email.html.haml2
-rw-r--r--app/views/notify/new_issue_email.text.erb2
-rw-r--r--app/views/notify/new_mention_in_issue_email.text.erb2
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.text.erb2
-rw-r--r--app/views/notify/new_merge_request_email.html.haml4
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/notify/reassigned_issue_email.html.haml11
-rw-r--r--app/views/notify/reassigned_merge_request_email.html.haml11
-rw-r--r--app/views/notify/reassigned_merge_request_email.text.erb4
-rw-r--r--app/views/projects/_classification_policy_settings.html.haml8
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml4
-rw-r--r--app/views/shared/boards/components/sidebar/_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/_assignees.html.haml6
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml56
-rw-r--r--app/views/shared/issuable/form/_merge_request_assignee.html.haml31
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml7
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml (renamed from app/views/shared/issuable/form/_metadata_issue_assignee.html.haml)6
-rw-r--r--changelogs/unreleased/57131-external_auth_to_core.yml5
-rw-r--r--changelogs/unreleased/do-not-reopen-merged-mr.yml5
-rw-r--r--db/fixtures/development/10_merge_requests.rb2
-rw-r--r--db/migrate/20171211131502_add_external_classification_authorization_settings_to_appliction_settings.rb29
-rw-r--r--db/migrate/20171218140451_add_external_authorization_service_classification_label_to_projects.rb11
-rw-r--r--db/migrate/20180314100728_add_external_authorization_service_timeout_to_application_settings.rb18
-rw-r--r--db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb16
-rw-r--r--db/schema.rb10
-rw-r--r--doc/gitlab-basics/fork-project.md6
-rw-r--r--doc/user/project/pages/getting_started_part_three.md2
-rw-r--r--lib/api/entities.rb9
-rw-r--r--lib/api/helpers/projects_helpers.rb5
-rw-r--r--lib/api/merge_requests.rb4
-rw-r--r--lib/api/settings.rb4
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb2
-rw-r--r--lib/gitlab/external_authorization.rb40
-rw-r--r--lib/gitlab/external_authorization/access.rb55
-rw-r--r--lib/gitlab/external_authorization/cache.rb62
-rw-r--r--lib/gitlab/external_authorization/client.rb63
-rw-r--r--lib/gitlab/external_authorization/config.rb47
-rw-r--r--lib/gitlab/external_authorization/logger.rb21
-rw-r--r--lib/gitlab/external_authorization/response.rb38
-rw-r--r--lib/gitlab/hook_data/issuable_builder.rb6
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb5
-rw-r--r--locale/gitlab.pot90
-rw-r--r--qa/qa/page/merge_request/new.rb2
-rwxr-xr-xscripts/review_apps/review-apps.sh101
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb22
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb26
-rw-r--r--spec/controllers/concerns/project_unauthorized_spec.rb51
-rw-r--r--spec/controllers/dashboard/groups_controller_spec.rb50
-rw-r--r--spec/controllers/dashboard/labels_controller_spec.rb6
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb6
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb24
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb6
-rw-r--r--spec/controllers/groups/avatars_controller_spec.rb10
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb8
-rw-r--r--spec/controllers/groups/children_controller_spec.rb12
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb86
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb6
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb6
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb15
-rw-r--r--spec/controllers/groups/variables_controller_spec.rb34
-rw-r--r--spec/controllers/groups_controller_spec.rb96
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb11
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb6
-rw-r--r--spec/controllers/projects_controller_spec.rb32
-rw-r--r--spec/controllers/search_controller_spec.rb39
-rw-r--r--spec/controllers/users_controller_spec.rb12
-rw-r--r--spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb38
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb4
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb4
-rw-r--r--spec/features/groups/group_page_with_external_authorization_service_spec.rb58
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/issues/form_spec.rb4
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb4
-rw-r--r--spec/features/merge_request/user_creates_mr_spec.rb15
-rw-r--r--spec/features/merge_request/user_edits_mr_spec.rb18
-rw-r--r--spec/features/merge_requests/user_filters_by_assignees_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb2
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb4
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb3
-rw-r--r--spec/features/projects/classification_label_on_project_pages_spec.rb22
-rw-r--r--spec/features/projects/forks/fork_list_spec.rb35
-rw-r--r--spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb128
-rw-r--r--spec/features/projects/settings/external_authorization_service_settings_spec.rb21
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb4
-rw-r--r--spec/features/users/show_spec.rb22
-rw-r--r--spec/finders/issues_finder_spec.rb71
-rw-r--r--spec/finders/labels_finder_spec.rb7
-rw-r--r--spec/finders/merge_requests_finder_spec.rb56
-rw-r--r--spec/finders/snippets_finder_spec.rb32
-rw-r--r--spec/finders/todos_finder_spec.rb7
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request.json5
-rw-r--r--spec/fixtures/passphrase_x509_certificate.crt27
-rw-r--r--spec/fixtures/passphrase_x509_certificate_pk.key54
-rw-r--r--spec/fixtures/x509_certificate.crt27
-rw-r--r--spec/fixtures/x509_certificate_pk.key51
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js85
-rw-r--r--spec/lib/gitlab/external_authorization/access_spec.rb142
-rw-r--r--spec/lib/gitlab/external_authorization/cache_spec.rb48
-rw-r--r--spec/lib/gitlab/external_authorization/client_spec.rb97
-rw-r--r--spec/lib/gitlab/external_authorization/logger_spec.rb45
-rw-r--r--spec/lib/gitlab/external_authorization/response_spec.rb52
-rw-r--r--spec/lib/gitlab/external_authorization_spec.rb54
-rw-r--r--spec/lib/gitlab/hook_data/issuable_builder_spec.rb6
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml5
-rw-r--r--spec/lib/gitlab/issuable_metadata_spec.rb4
-rw-r--r--spec/mailers/notify_spec.rb18
-rw-r--r--spec/models/application_setting_spec.rb48
-rw-r--r--spec/models/ci/pipeline_spec.rb8
-rw-r--r--spec/models/concerns/deprecated_assignee_spec.rb160
-rw-r--r--spec/models/concerns/issuable_spec.rb7
-rw-r--r--spec/models/concerns/protected_ref_access_spec.rb12
-rw-r--r--spec/models/event_spec.rb2
-rw-r--r--spec/models/issue_spec.rb45
-rw-r--r--spec/models/merge_request_spec.rb60
-rw-r--r--spec/models/project_spec.rb20
-rw-r--r--spec/models/user_spec.rb6
-rw-r--r--spec/policies/base_policy_spec.rb23
-rw-r--r--spec/policies/issue_policy_spec.rb19
-rw-r--r--spec/policies/merge_request_policy_spec.rb19
-rw-r--r--spec/policies/project_policy_spec.rb53
-rw-r--r--spec/requests/api/events_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb124
-rw-r--r--spec/requests/api/projects_spec.rb49
-rw-r--r--spec/requests/api/settings_spec.rb33
-rw-r--r--spec/serializers/group_child_entity_spec.rb19
-rw-r--r--spec/services/application_settings/update_service_spec.rb35
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb16
-rw-r--r--spec/services/issuable/destroy_service_spec.rb2
-rw-r--r--spec/services/members/destroy_service_spec.rb4
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb30
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb2
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb2
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb42
-rw-r--r--spec/services/notification_service_spec.rb96
-rw-r--r--spec/services/projects/create_service_spec.rb37
-rw-r--r--spec/services/projects/update_service_spec.rb41
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb6
-rw-r--r--spec/services/todo_service_spec.rb110
-rw-r--r--spec/services/users/destroy_service_spec.rb4
-rw-r--r--spec/support/external_authorization_service_helpers.rb33
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb6
-rw-r--r--spec/support/shared_contexts/merge_request_create.rb26
-rw-r--r--spec/support/shared_contexts/merge_request_edit.rb28
-rw-r--r--spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/finders/assignees_filter_spec.rb49
-rw-r--r--spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb30
-rw-r--r--spec/validators/x509_certificate_credentials_validator_spec.rb87
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb6
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb13
227 files changed, 4318 insertions, 901 deletions
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index b844e4c5e5b..ccbe591a63e 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -81,9 +81,6 @@ export default {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
- // For Merge Requests
- assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
- // For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 1d6cb9485f7..b30d7fa9b73 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -115,8 +115,11 @@ export default {
author() {
return this.getUserData;
},
- canUpdateIssue() {
- return this.getNoteableData.current_user.can_update;
+ canToggleIssueState() {
+ return (
+ this.getNoteableData.current_user.can_update &&
+ this.getNoteableData.state !== constants.MERGED
+ );
},
endpoint() {
return this.getNoteableData.create_note_path;
@@ -415,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</div>
<loading-button
- v-if="canUpdateIssue"
+ v-if="canToggleIssueState"
:loading="isToggleStateButtonLoading"
:container-class="[
actionButtonClassNames,
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index fba3db8542c..bdfb6b8f105 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -7,6 +7,7 @@ export const COMMENT = 'comment';
export const OPENED = 'opened';
export const REOPENED = 'reopened';
export const CLOSED = 'closed';
+export const MERGED = 'merged';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index d1a396182b3..ce378e24289 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -74,8 +74,7 @@ export default {
}
if (!this.users.length) {
- const emptyTooltipLabel =
- this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
+ const emptyTooltipLabel = __('Assignee(s)');
names.push(emptyTooltipLabel);
}
@@ -90,6 +89,27 @@ export default {
return counter;
},
+ mergeNotAllowedTooltipMessage() {
+ const assigneesCount = this.users.length;
+
+ if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
+ return null;
+ }
+
+ const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
+ const canMergeCount = assigneesCount - cannotMergeCount;
+
+ if (canMergeCount === assigneesCount) {
+ // Everyone can merge
+ return null;
+ } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
+ return 'No one can merge';
+ } else if (assigneesCount === 1) {
+ return 'Cannot merge';
+ }
+
+ return `${canMergeCount}/${assigneesCount} can merge`;
+ },
},
methods: {
assignSelf() {
@@ -154,6 +174,15 @@ export default {
</button>
</div>
<div class="value hide-collapsed">
+ <span
+ v-if="mergeNotAllowedTooltipMessage"
+ v-tooltip
+ :title="mergeNotAllowedTooltipMessage"
+ data-placement="left"
+ class="float-right cannot-be-merged"
+ >
+ <i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
+ </span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value">
No assignee
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 5a5601f2fa3..628dffc39f1 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -25,6 +25,18 @@ $item-weight-max-width: 48px;
flex-grow: 1;
}
+ .issue-token-state-icon-open {
+ color: $green-500;
+ }
+
+ .issue-token-state-icon-closed {
+ color: $blue-500;
+ }
+
+ .merge-request-status.closed {
+ color: $red-500;
+ }
+
.issue-token-state-icon-open,
.issue-token-state-icon-closed,
.confidential-icon,
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 86b58c1b1b2..709940ba6c8 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -498,6 +498,16 @@
flex: 1;
}
+ .issuable-meta {
+ .author-link {
+ display: inline-block;
+ }
+
+ .issuable-comments {
+ height: 18px;
+ }
+ }
+
.merge-request-title {
margin-bottom: 2px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 792c618fd40..7778b4aab3d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -67,6 +67,10 @@
}
}
+.classification-label {
+ background-color: $red-500;
+}
+
.toggle-wrapper {
margin-top: 5px;
}
@@ -1158,6 +1162,8 @@ pre.light-well {
.cannot-be-merged:hover {
color: $red-500;
margin-top: 2px;
+ position: relative;
+ z-index: 2;
}
.private-forks-notice .private-fork-icon {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index ab792cf7403..b681949ab36 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -124,7 +124,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def visible_application_setting_attributes
- ApplicationSettingsHelper.visible_attributes + [
+ [
+ *::ApplicationSettingsHelper.visible_attributes,
+ *::ApplicationSettingsHelper.external_authorization_service_attributes,
:domain_blacklist_file,
disabled_oauth_sign_in_sources: [],
import_sources: [],
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 85aeecbf90b..065d2d3a4ec 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -192,12 +192,7 @@ module IssuableActions
def bulk_update_params
permitted_keys_array = permitted_keys.dup
-
- if resource_name == 'issue'
- permitted_keys_array << { assignee_ids: [] }
- else
- permitted_keys_array.unshift(:assignee_id)
- end
+ permitted_keys_array << { assignee_ids: [] }
params.require(:update).permit(permitted_keys_array)
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 6d6e0cc6c7f..91e875dca54 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -190,15 +190,15 @@ module IssuableCollections
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def preload_for_collection
+ common_attributes = [:author, :assignees, :labels, :milestone]
@preload_for_collection ||= case collection_type
when 'Issue'
- [:project, :author, :assignees, :labels, :milestone, project: :namespace]
+ common_attributes + [:project, project: :namespace]
when 'MergeRequest'
- [
- :target_project, :author, :assignee, :labels, :milestone,
- source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
- ]
+ common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits]
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb
index f59440dbc59..d42363b8b17 100644
--- a/app/controllers/concerns/project_unauthorized.rb
+++ b/app/controllers/concerns/project_unauthorized.rb
@@ -1,10 +1,21 @@
# frozen_string_literal: true
module ProjectUnauthorized
- extend ActiveSupport::Concern
-
- # EE would override this
def project_unauthorized_proc
- # no-op
+ lambda do |project|
+ if project
+ label = project.external_authorization_classification_label
+ rejection_reason = nil
+
+ unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, label)
+ rejection_reason = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label)
+ rejection_reason ||= _('External authorization denied access to this project')
+ end
+
+ if rejection_reason
+ access_denied!(rejection_reason)
+ end
+ end
+ end
end
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 6045ee4e171..eb469d2d714 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -20,7 +20,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
:allow_collaboration,
- :assignee_id,
:description,
:force_remove_source_branch,
:lock_version,
@@ -35,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:title,
:discussion_locked,
label_ids: [],
+ assignee_ids: [],
update_task: [:index, :checked, :line_number, :line_source]
]
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 89dc43a48a1..62b97fc2590 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -345,6 +345,7 @@ class ProjectsController < Projects::ApplicationController
:container_registry_enabled,
:default_branch,
:description,
+ :external_authorization_classification_label,
:import_url,
:issues_tracker,
:issues_tracker_id,
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 64c88505a16..88ec77426d5 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -439,22 +439,6 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def by_assignee(items)
- if filter_by_no_assignee?
- items.where(assignee_id: nil)
- elsif filter_by_any_assignee?
- items.where('assignee_id IS NOT NULL')
- elsif assignee
- items.where(assignee_id: assignee.id)
- elsif assignee_id? || assignee_username? # assignee not found
- items.none
- else
- items
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def filter_by_no_assignee?
# Assignee_id takes precedence over assignee_username
[NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
@@ -478,6 +462,20 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_assignee(items)
+ if filter_by_no_assignee?
+ items.unassigned
+ elsif filter_by_any_assignee?
+ items.assigned
+ elsif assignee
+ items.assigned_to(assignee)
+ elsif assignee_id? || assignee_username? # assignee not found
+ items.none
+ else
+ items
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
if milestones?
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index cb44575d6f1..e6a82f55856 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -144,18 +144,4 @@ class IssuesFinder < IssuableFinder
current_user.blank?
end
-
- def by_assignee(items)
- if filter_by_no_assignee?
- items.unassigned
- elsif filter_by_any_assignee?
- items.assigned
- elsif assignee
- items.assigned_to(assignee)
- elsif assignee_id? || assignee_username? # assignee not found
- items.none
- else
- items
- end
- end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index e275e4278a4..5995ef57e26 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -119,6 +119,39 @@ module ApplicationSettingsHelper
options_for_select(options, selected)
end
+ def external_authorization_description
+ _("If enabled, access to projects will be validated on an external service"\
+ " using their classification label.")
+ end
+
+ def external_authorization_timeout_help_text
+ _("Time in seconds GitLab will wait for a response from the external "\
+ "service. When the service does not respond in time, access will be "\
+ "denied.")
+ end
+
+ def external_authorization_url_help_text
+ _("When leaving the URL blank, classification labels can still be "\
+ "specified without disabling cross project features or performing "\
+ "external authorization checks.")
+ end
+
+ def external_authorization_client_certificate_help_text
+ _("The X509 Certificate to use when mutual TLS is required to communicate "\
+ "with the external authorization service. If left blank, the server "\
+ "certificate is still validated when accessing over HTTPS.")
+ end
+
+ def external_authorization_client_key_help_text
+ _("The private key to use when a client certificate is provided. This value "\
+ "is encrypted at rest.")
+ end
+
+ def external_authorization_client_pass_help_text
+ _("The passphrase required to decrypt the private key. This is optional "\
+ "and the value is encrypted at rest.")
+ end
+
def visible_attributes
[
:admin_notification_email,
@@ -238,6 +271,18 @@ module ApplicationSettingsHelper
]
end
+ def external_authorization_service_attributes
+ [
+ :external_auth_client_cert,
+ :external_auth_client_key,
+ :external_auth_client_key_pass,
+ :external_authorization_service_default_label,
+ :external_authorization_service_enabled,
+ :external_authorization_service_timeout,
+ :external_authorization_service_url
+ ]
+ end
+
def expanded_by_default?
Rails.env.test?
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index be1e7016a1e..1640f4fc93f 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -69,7 +69,7 @@ module BoardsHelper
end
def board_sidebar_user_data
- dropdown_options = issue_assignees_dropdown_options
+ dropdown_options = assignees_dropdown_options('issue')
{
toggle: 'dropdown',
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 8b3d270e873..f7c7f37cc38 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -17,8 +17,8 @@ module FormHelper
end
end
- def issue_assignees_dropdown_options
- {
+ def assignees_dropdown_options(issuable_type)
+ dropdown_data = {
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee',
filter: true,
@@ -28,8 +28,8 @@ module FormHelper
first_user: current_user&.username,
null_user: true,
current_user: true,
- project_id: @project&.id,
- field_name: 'issue[assignee_ids][]',
+ project_id: (@target_project || @project)&.id,
+ field_name: "#{issuable_type}[assignee_ids][]",
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
@@ -39,5 +39,36 @@ module FormHelper
current_user_info: UserSerializer.new.represent(current_user)
}
}
+
+ type = issuable_type.to_s
+
+ if type == 'issue' && issue_supports_multiple_assignees? ||
+ type == 'merge_request' && merge_request_supports_multiple_assignees?
+ dropdown_data = multiple_assignees_dropdown_options(dropdown_data)
+ end
+
+ dropdown_data
+ end
+
+ # Overwritten
+ def issue_supports_multiple_assignees?
+ false
+ end
+
+ # Overwritten
+ def merge_request_supports_multiple_assignees?
+ false
+ end
+
+ private
+
+ def multiple_assignees_dropdown_options(options)
+ new_options = options.dup
+
+ new_options[:title] = 'Select assignee(s)'
+ new_options[:data][:'dropdown-header'] = 'Assignee(s)'
+ new_options[:data].delete(:'max-select')
+
+ new_options
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 52c49498e9b..9a12db258d5 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -15,11 +15,14 @@ module IssuablesHelper
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end
- def sidebar_assignee_tooltip_label(issuable)
- if issuable.assignee
- issuable.assignee.name
+ def assignees_label(issuable, include_value: true)
+ label = 'Assignee'.pluralize(issuable.assignees.count)
+
+ if include_value
+ sanitized_list = sanitize_name(issuable.assignee_list)
+ "#{label}: #{sanitized_list}"
else
- issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
+ label
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 009dd70c2c9..2ac90eb8d9f 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -303,6 +303,16 @@ module ProjectsHelper
@path.present?
end
+ def external_classification_label_help_message
+ default_label = ::Gitlab::CurrentSettings.current_application_settings
+ .external_authorization_service_default_label
+
+ s_(
+ "ExternalAuthorizationService|When no classification label is set the "\
+ "default label `%{default_label}` will be used."
+ ) % { default_label: default_label }
+ end
+
private
def get_project_nav_tabs(project, current_user)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 9ba8f92fcbf..63148831a24 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -24,10 +24,12 @@ module Emails
end
# rubocop: disable CodeReuse/ActiveRecord
- def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
+ def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_ids, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ @previous_assignees = []
+ @previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
+
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index efa1233b434..0b740809f30 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -4,6 +4,7 @@ class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper
include EmailsHelper
+ include IssuablesHelper
include Emails::Issues
include Emails::MergeRequests
@@ -24,6 +25,7 @@ class Notify < BaseMailer
helper MembersHelper
helper AvatarsHelper
helper GitlabRoutingHelper
+ helper IssuablesHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 7ec8505b33a..d28a12413bf 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -213,6 +213,40 @@ class ApplicationSetting < ApplicationRecord
validate :terms_exist, if: :enforce_terms?
+ validates :external_authorization_service_default_label,
+ presence: true,
+ if: :external_authorization_service_enabled
+
+ validates :external_authorization_service_url,
+ url: true, allow_blank: true,
+ if: :external_authorization_service_enabled
+
+ validates :external_authorization_service_timeout,
+ numericality: { greater_than: 0, less_than_or_equal_to: 10 },
+ if: :external_authorization_service_enabled
+
+ validates :external_auth_client_key,
+ presence: true,
+ if: -> (setting) { setting.external_auth_client_cert.present? }
+
+ validates_with X509CertificateCredentialsValidator,
+ certificate: :external_auth_client_cert,
+ pkey: :external_auth_client_key,
+ pass: :external_auth_client_key_pass,
+ if: -> (setting) { setting.external_auth_client_cert.present? }
+
+ attr_encrypted :external_auth_client_key,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: true
+
+ attr_encrypted :external_auth_client_key_pass,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: true
+
before_validation :ensure_uuid!
before_validation :strip_sentry_values
diff --git a/app/models/concerns/deprecated_assignee.rb b/app/models/concerns/deprecated_assignee.rb
new file mode 100644
index 00000000000..7f12ce39c96
--- /dev/null
+++ b/app/models/concerns/deprecated_assignee.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+# This module handles backward compatibility for import/export of Merge Requests after
+# multiple assignees feature was introduced. Also, it handles the scenarios where
+# the #26496 background migration hasn't finished yet.
+# Ideally, most of this code should be removed at #59457.
+module DeprecatedAssignee
+ extend ActiveSupport::Concern
+
+ def assignee_ids=(ids)
+ nullify_deprecated_assignee
+ super
+ end
+
+ def assignees=(users)
+ nullify_deprecated_assignee
+ super
+ end
+
+ def assignee_id=(id)
+ self.assignee_ids = Array(id)
+ end
+
+ def assignee=(user)
+ self.assignees = Array(user)
+ end
+
+ def assignee
+ assignees.first
+ end
+
+ def assignee_id
+ assignee_ids.first
+ end
+
+ def assignee_ids
+ if Gitlab::Database.read_only? && pending_assignees_population?
+ return Array(deprecated_assignee_id)
+ end
+
+ update_assignees_relation
+ super
+ end
+
+ def assignees
+ if Gitlab::Database.read_only? && pending_assignees_population?
+ return User.where(id: deprecated_assignee_id)
+ end
+
+ update_assignees_relation
+ super
+ end
+
+ private
+
+ # This will make the background migration process quicker (#26496) as it'll have less
+ # assignee_id rows to look through.
+ def nullify_deprecated_assignee
+ return unless persisted? && Gitlab::Database.read_only?
+
+ update_column(:assignee_id, nil)
+ end
+
+ # This code should be removed in the clean-up phase of the
+ # background migration (#59457).
+ def pending_assignees_population?
+ persisted? && deprecated_assignee_id && merge_request_assignees.empty?
+ end
+
+ # If there's an assignee_id and no relation, it means the background
+ # migration at #26496 didn't reach this merge request yet.
+ # This code should be removed in the clean-up phase of the
+ # background migration (#59457).
+ def update_assignees_relation
+ if pending_assignees_population?
+ transaction do
+ merge_request_assignees.create!(user_id: deprecated_assignee_id, merge_request_id: id)
+ update_column(:assignee_id, nil)
+ end
+ end
+ end
+
+ def deprecated_assignee_id
+ read_attribute(:assignee_id)
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 17f94b4bd9b..3232c51bfbd 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -67,13 +67,6 @@ module Issuable
allow_nil: true,
prefix: true
- delegate :name,
- :email,
- :public_email,
- to: :assignee,
- allow_nil: true,
- prefix: true
-
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
validate :milestone_is_valid
@@ -88,6 +81,19 @@ module Issuable
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
+ # rubocop:disable GitlabSecurity/SqlInjection
+ # The `to_ability_name` method is not an user input.
+ scope :assigned, -> do
+ where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
+ end
+ scope :unassigned, -> do
+ where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
+ end
+ scope :assigned_to, ->(u) do
+ where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE user_id = ? AND #{to_ability_name}_id = #{to_ability_name}s.id)", u.id)
+ end
+ # rubocop:enable GitlabSecurity/SqlInjection
+
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') }
@@ -104,6 +110,7 @@ module Issuable
participant :author
participant :notes_with_associations
+ participant :assignees
strip_attributes :title
@@ -270,6 +277,10 @@ module Issuable
end
end
+ def assignee_or_author?(user)
+ author_id == user.id || assignees.exists?(user.id)
+ end
+
def today?
Date.today == created_at.to_date
end
@@ -314,11 +325,7 @@ module Issuable
end
if old_assignees != assignees
- if self.is_a?(Issue)
- changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
- else
- changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
- end
+ changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
end
if self.respond_to?(:total_time_spent)
@@ -355,10 +362,18 @@ module Issuable
def card_attributes
{
'Author' => author.try(:name),
- 'Assignee' => assignee.try(:name)
+ 'Assignee' => assignee_list
}
end
+ def assignee_list
+ assignees.map(&:name).to_sentence
+ end
+
+ def assignee_username_list
+ assignees.map(&:username).to_sentence
+ end
+
def notes_with_associations
# If A has_many Bs, and B has_many Cs, and you do
# `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 261935fd054..eb5544f2a12 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -49,10 +49,6 @@ class Issue < ApplicationRecord
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
- scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
- scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
- scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
-
scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
@@ -75,8 +71,6 @@ class Issue < ApplicationRecord
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
- participant :assignees
-
state_machine :state, initial: :opened do
event :close do
transition [:opened] => :closed
@@ -155,22 +149,6 @@ class Issue < ApplicationRecord
Gitlab::HookData::IssueBuilder.new(self).build
end
- # Returns a Hash of attributes to be used for Twitter card metadata
- def card_attributes
- {
- 'Author' => author.try(:name),
- 'Assignee' => assignee_list
- }
- end
-
- def assignee_or_author?(user)
- author_id == user.id || assignees.exists?(user.id)
- end
-
- def assignee_list
- assignees.map(&:name).to_sentence
- end
-
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -230,7 +208,13 @@ class Issue < ApplicationRecord
def visible_to_user?(user = nil)
return false unless project && project.feature_available?(:issues, user)
- user ? readable_by?(user) : publicly_visible?
+ return publicly_visible? unless user
+
+ return false unless readable_by?(user)
+
+ user.full_private_access? ||
+ ::Gitlab::ExternalAuthorization.access_allowed?(
+ user, project.external_authorization_classification_label)
end
def check_for_spam?
@@ -298,7 +282,7 @@ class Issue < ApplicationRecord
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
- project.public? && !confidential?
+ project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
end
def expire_etag_cache
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 458c57c1dc6..0a39a720766 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -16,6 +16,7 @@ class MergeRequest < ApplicationRecord
include LabelEventable
include ReactiveCaching
include FromUnion
+ include DeprecatedAssignee
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
@@ -69,8 +70,7 @@ class MergeRequest < ApplicationRecord
has_many :suggestions, through: :notes
has_many :merge_request_assignees
- # Will be deprecated at https://gitlab.com/gitlab-org/gitlab-ce/issues/59457
- belongs_to :assignee, class_name: "User"
+ has_many :assignees, class_name: "User", through: :merge_request_assignees
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
@@ -79,10 +79,6 @@ class MergeRequest < ApplicationRecord
after_update :reload_diff_if_branch_changed
after_save :ensure_metrics
- # Required until the codebase starts using this relation for single or multiple assignees.
- # TODO: Remove at gitlab-ee#2004 implementation.
- after_save :refresh_merge_request_assignees, if: :assignee_id_changed?
-
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
@@ -188,19 +184,14 @@ class MergeRequest < ApplicationRecord
end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
- scope :assigned, -> { where("assignee_id IS NOT NULL") }
- scope :unassigned, -> { where("assignee_id IS NULL") }
- scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :with_api_entity_associations, -> {
- preload(:author, :assignee, :notes, :labels, :milestone, :timelogs,
+ preload(:assignees, :author, :notes, :labels, :milestone, :timelogs,
latest_merge_request_diff: [:merge_request_diff_commits],
metrics: [:latest_closed_by, :merged_by],
target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
}
- participant :assignee
-
after_save :keep_around_commit
alias_attribute :project, :target_project
@@ -337,31 +328,6 @@ class MergeRequest < ApplicationRecord
Gitlab::HookData::MergeRequestBuilder.new(self).build
end
- # Returns a Hash of attributes to be used for Twitter card metadata
- def card_attributes
- {
- 'Author' => author.try(:name),
- 'Assignee' => assignee.try(:name)
- }
- end
-
- # These method are needed for compatibility with issues to not mess view and other code
- def assignees
- Array(assignee)
- end
-
- def assignee_ids
- Array(assignee_id)
- end
-
- def assignee_ids=(ids)
- write_attribute(:assignee_id, ids.last)
- end
-
- def assignee_or_author?(user)
- author_id == user.id || assignee_id == user.id
- end
-
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -682,15 +648,6 @@ class MergeRequest < ApplicationRecord
merge_request_diff || create_merge_request_diff
end
- def refresh_merge_request_assignees
- transaction do
- # Using it instead relation.delete_all in order to avoid adding a
- # dependent: :delete_all (we already have foreign key cascade deletion).
- MergeRequestAssignee.where(merge_request_id: self).delete_all
- merge_request_assignees.create(user_id: assignee_id) if assignee_id
- end
- end
-
def create_merge_request_diff
fetch_ref!
@@ -1208,7 +1165,7 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
- variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee
+ variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.any?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
variables.concat(source_project_variables)
diff --git a/app/models/project.rb b/app/models/project.rb
index e2869fc2ad5..3e9bb6aedf1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -674,6 +674,10 @@ class Project < ApplicationRecord
{ scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end
+ def multiple_mr_assignees_enabled?
+ Feature.enabled?(:multiple_merge_request_assignees, self)
+ end
+
def daily_statistics_enabled?
Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
end
@@ -2062,6 +2066,11 @@ class Project < ApplicationRecord
fetch_branch_allows_collaboration(user, branch_name)
end
+ def external_authorization_classification_label
+ super || ::Gitlab::CurrentSettings.current_application_settings
+ .external_authorization_service_default_label
+ end
+
def licensed_features
[]
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 72de04203a6..5dd2279ef99 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -22,6 +22,13 @@ class BasePolicy < DeclarativePolicy::Base
Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
- # This is prevented in some cases in `gitlab-ee`
+ condition(:external_authorization_enabled, scope: :global, score: 0) do
+ ::Gitlab::ExternalAuthorization.perform_check?
+ end
+
+ rule { external_authorization_enabled & ~full_private_access }.policy do
+ prevent :read_cross_project
+ end
+
rule { default }.enable :read_cross_project
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 26d7d6e84c4..ba38af9c529 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -89,6 +89,15 @@ class ProjectPolicy < BasePolicy
::Gitlab::CurrentSettings.current_application_settings.mirror_available
end
+ with_scope :subject
+ condition(:classification_label_authorized, score: 32) do
+ ::Gitlab::ExternalAuthorization.access_allowed?(
+ @user,
+ @subject.external_authorization_classification_label,
+ @subject.full_path
+ )
+ end
+
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
@@ -417,6 +426,25 @@ class ProjectPolicy < BasePolicy
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
+ rule { ~can?(:read_cross_project) & ~classification_label_authorized }.policy do
+ # Preventing access here still allows the projects to be listed. Listing
+ # projects doesn't check the `:read_project` ability. But instead counts
+ # on the `project_authorizations` table.
+ #
+ # All other actions should explicitly check read project, which would
+ # trigger the `classification_label_authorized` condition.
+ #
+ # `:read_project_for_iids` is not prevented by this condition, as it is
+ # used for cross-project reference checks.
+ prevent :guest_access
+ prevent :public_access
+ prevent :public_user_access
+ prevent :reporter_access
+ prevent :developer_access
+ prevent :maintainer_access
+ prevent :owner_access
+ end
+
private
def team_member?
diff --git a/app/serializers/issuable_sidebar_extras_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb
index d60253564e1..fb35b7522c5 100644
--- a/app/serializers/issuable_sidebar_extras_entity.rb
+++ b/app/serializers/issuable_sidebar_extras_entity.rb
@@ -11,4 +11,6 @@ class IssuableSidebarExtrasEntity < Grape::Entity
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
+
+ expose :assignees, using: API::Entities::UserBasic
end
diff --git a/app/serializers/issue_sidebar_extras_entity.rb b/app/serializers/issue_sidebar_extras_entity.rb
index 7b6e860140b..dee891a50b7 100644
--- a/app/serializers/issue_sidebar_extras_entity.rb
+++ b/app/serializers/issue_sidebar_extras_entity.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
- expose :assignees, using: API::Entities::UserBasic
end
diff --git a/app/serializers/merge_request_assignee_entity.rb b/app/serializers/merge_request_assignee_entity.rb
new file mode 100644
index 00000000000..6849c62e759
--- /dev/null
+++ b/app/serializers/merge_request_assignee_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class MergeRequestAssigneeEntity < ::API::Entities::UserBasic
+ expose :can_merge do |assignee, options|
+ options[:merge_request]&.can_be_merged_by?(assignee)
+ end
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 178e72f4f0a..973e971b4c0 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class MergeRequestBasicEntity < Grape::Entity
- expose :assignee_id
expose :merge_status
expose :merge_error
expose :state
@@ -9,7 +8,7 @@ class MergeRequestBasicEntity < Grape::Entity
expose :rebase_in_progress?, as: :rebase_in_progress
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
- expose :assignee, using: API::Entities::UserBasic
+ expose :assignees, using: API::Entities::UserBasic
expose :task_status, :task_status_short
expose :lock_version, :lock_version
end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 4cf84336aa4..6f589351670 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -8,9 +8,9 @@ class MergeRequestSerializer < BaseSerializer
entity =
case opts[:serializer]
when 'sidebar'
- MergeRequestSidebarBasicEntity
+ IssuableSidebarBasicEntity
when 'sidebar_extras'
- IssuableSidebarExtrasEntity
+ MergeRequestSidebarExtrasEntity
when 'basic'
MergeRequestBasicEntity
else
diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb
deleted file mode 100644
index 0ae7298a7c1..00000000000
--- a/app/serializers/merge_request_sidebar_basic_entity.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
- expose :assignee, if: lambda { |issuable| issuable.assignee } do
- expose :assignee, merge: true, using: API::Entities::UserBasic
-
- expose :can_merge do |issuable|
- issuable.can_be_merged_by?(issuable.assignee)
- end
- end
-end
diff --git a/app/serializers/merge_request_sidebar_extras_entity.rb b/app/serializers/merge_request_sidebar_extras_entity.rb
new file mode 100644
index 00000000000..7276509c363
--- /dev/null
+++ b/app/serializers/merge_request_sidebar_extras_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
+ expose :assignees do |merge_request|
+ MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
+ end
+end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 9146eb96533..7eeaf8aade1 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -2,9 +2,17 @@
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
+ include ValidatesClassificationLabel
+
attr_reader :params, :application_setting
def execute
+ validate_classification_label(application_setting, :external_authorization_service_default_label)
+
+ if application_setting.errors.any?
+ return false
+ end
+
update_terms(@params.delete(:terms))
if params.key?(:performance_bar_allowed_group_path)
diff --git a/app/services/concerns/validates_classification_label.rb b/app/services/concerns/validates_classification_label.rb
new file mode 100644
index 00000000000..ebcf5c24ff8
--- /dev/null
+++ b/app/services/concerns/validates_classification_label.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ValidatesClassificationLabel
+ def validate_classification_label(record, attribute_name)
+ return unless ::Gitlab::ExternalAuthorization.enabled?
+ return unless classification_label_change?(record, attribute_name)
+
+ new_label = params[attribute_name].presence
+ new_label ||= ::Gitlab::CurrentSettings.current_application_settings
+ .external_authorization_service_default_label
+
+ unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, new_label)
+ reason = rejection_reason_for_label(new_label)
+ message = s_('ClassificationLabelUnavailable|is unavailable: %{reason}') % { reason: reason }
+ record.errors.add(attribute_name, message)
+ end
+ end
+
+ def rejection_reason_for_label(label)
+ reason_from_service = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence
+ reason_from_service || _("Access to '%{classification_label}' not allowed") % { classification_label: label }
+ end
+
+ def classification_label_change?(record, attribute_name)
+ params.key?(attribute_name) || record.new_record?
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 7a4ccf0d178..26132f1824a 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -34,14 +34,20 @@ class IssuableBaseService < BaseService
end
def filter_assignee(issuable)
- return unless params[:assignee_id].present?
+ return if params[:assignee_ids].blank?
- assignee_id = params[:assignee_id]
+ unless issuable.allows_multiple_assignees?
+ params[:assignee_ids] = params[:assignee_ids].first(1)
+ end
+
+ assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
- if assignee_id.to_s == IssuableFinder::NONE
- params[:assignee_id] = ""
+ if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
+ params[:assignee_ids] = []
+ elsif assignee_ids.any?
+ params[:assignee_ids] = assignee_ids
else
- params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
+ params.delete(:assignee_ids)
end
end
@@ -352,7 +358,7 @@ class IssuableBaseService < BaseService
end
def has_changes?(issuable, old_labels: [], old_assignees: [])
- valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
+ valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index ef08adf4f92..48ed5afbc2a 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -20,7 +20,7 @@ module Issues
private
def create_assignee_note(issue, old_assignees)
- SystemNoteService.change_issue_assignees(
+ SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees)
end
@@ -31,26 +31,6 @@ module Issues
issue.project.execute_services(issue_data, hooks_scope)
end
- # rubocop: disable CodeReuse/ActiveRecord
- def filter_assignee(issuable)
- return if params[:assignee_ids].blank?
-
- unless issuable.allows_multiple_assignees?
- params[:assignee_ids] = params[:assignee_ids].take(1)
- end
-
- assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
-
- if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
- params[:assignee_ids] = []
- elsif assignee_ids.any?
- params[:assignee_ids] = assignee_ids
- else
- params.delete(:assignee_ids)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index cec5b5734c0..cb2337d29d4 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -39,7 +39,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_issue(issue, current_user, old_assignees)
+ todo_service.reassigned_issuable(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 8a9e5ebb014..b8334a87f6d 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -49,9 +49,9 @@ module MergeRequests
MergeRequestMetricsService.new(merge_request.metrics)
end
- def create_assignee_note(merge_request)
- SystemNoteService.change_assignee(
- merge_request, merge_request.project, current_user, merge_request.assignee)
+ def create_assignee_note(merge_request, old_assignees)
+ SystemNoteService.change_issuable_assignees(
+ merge_request, merge_request.project, current_user, old_assignees)
end
def create_pipeline_for(merge_request, user)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 8112c2a4299..faaa4d66726 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -24,13 +24,13 @@ module MergeRequests
update_task_event(merge_request) || update(merge_request)
end
- # rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
old_mentioned_users = old_associations.fetch(:mentioned_users, [])
+ old_assignees = old_associations.fetch(:assignees, [])
- if has_changes?(merge_request, old_labels: old_labels)
+ if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
@@ -45,15 +45,10 @@ module MergeRequests
merge_request.target_branch)
end
- if merge_request.previous_changes.include?('assignee_id')
- reassigned_merge_request_args = [merge_request, current_user]
-
- old_assignee_id = merge_request.previous_changes['assignee_id'].first
- reassigned_merge_request_args << User.find(old_assignee_id) if old_assignee_id
-
- create_assignee_note(merge_request)
- notification_service.async.reassigned_merge_request(*reassigned_merge_request_args)
- todo_service.reassigned_merge_request(merge_request, current_user)
+ if merge_request.assignees != old_assignees
+ create_assignee_note(merge_request, old_assignees)
+ notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
+ todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
if merge_request.previous_changes.include?('target_branch') ||
@@ -81,7 +76,6 @@ module MergeRequests
)
end
end
- # rubocop:enable Metrics/AbcSize
def handle_task_changes(merge_request)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 56f11b31110..760962346fb 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -247,15 +247,15 @@ module NotificationRecipientService
attr_reader :target
attr_reader :current_user
attr_reader :action
- attr_reader :previous_assignee
+ attr_reader :previous_assignees
attr_reader :skip_current_user
- def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
+ def initialize(target, current_user, action:, custom_action: nil, previous_assignees: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
@custom_action = custom_action
- @previous_assignee = previous_assignee
+ @previous_assignees = previous_assignees
@skip_current_user = skip_current_user
end
@@ -270,11 +270,7 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee
case custom_action
- when :reassign_merge_request
- add_recipients(previous_assignee, :mention, nil)
- add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
- when :reassign_issue
- previous_assignees = Array(previous_assignee)
+ when :reassign_merge_request, :reassign_issue
add_recipients(previous_assignees, :mention, nil)
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end
@@ -287,17 +283,11 @@ module NotificationRecipientService
# receive them, too.
add_mentions(current_user, target: target)
- # Add the assigned users, if any
- assignees = case custom_action
- when :new_issue
- target.assignees
- else
- target.assignee
- end
-
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
- add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
+ if target.is_a?(Issuable)
+ add_recipients(target.assignees, :participating, NotificationReason::ASSIGNED)
+ end
add_labels_subscribers
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 1a65561dd70..8d3b569498f 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -95,8 +95,8 @@ class NotificationService
# When we reassign an issue we should send an email to:
#
- # * issue old assignee if their notification level is not Disabled
- # * issue new assignee if their notification level is not Disabled
+ # * issue old assignees if their notification level is not Disabled
+ # * issue new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user, previous_assignees = [])
@@ -104,7 +104,7 @@ class NotificationService
issue,
current_user,
action: "reassign",
- previous_assignee: previous_assignees
+ previous_assignees: previous_assignees
)
previous_assignee_ids = previous_assignees.map(&:id)
@@ -140,7 +140,7 @@ class NotificationService
# When create a merge request we should send an email to:
#
# * mr author
- # * mr assignee if their notification level is not Disabled
+ # * mr assignees if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
# * users with custom level checked with "new merge request"
@@ -184,23 +184,25 @@ class NotificationService
# When we reassign a merge_request we should send an email to:
#
- # * merge_request old assignee if their notification level is not Disabled
- # * merge_request assignee if their notification level is not Disabled
+ # * merge_request old assignees if their notification level is not Disabled
+ # * merge_request new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
- def reassigned_merge_request(merge_request, current_user, previous_assignee = nil)
+ def reassigned_merge_request(merge_request, current_user, previous_assignees = [])
recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
action: "reassign",
- previous_assignee: previous_assignee
+ previous_assignees: previous_assignees
)
+ previous_assignee_ids = previous_assignees.map(&:id)
+
recipients.each do |recipient|
mailer.reassigned_merge_request_email(
recipient.user.id,
merge_request.id,
- previous_assignee&.id,
+ previous_assignee_ids,
current_user.id,
recipient.reason
).deliver_later
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index d03137b63b2..3723c5ef7d7 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -2,6 +2,8 @@
module Projects
class CreateService < BaseService
+ include ValidatesClassificationLabel
+
def initialize(user, params)
@current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
@@ -45,6 +47,8 @@ module Projects
relations_block&.call(@project)
yield(@project) if block_given?
+ validate_classification_label(@project, :external_authorization_classification_label)
+
# If the block added errors, don't try to save the project
return @project if @project.errors.any?
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 6856009b395..bc36bb8659d 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -3,6 +3,7 @@
module Projects
class UpdateService < BaseService
include UpdateVisibilityLevel
+ include ValidatesClassificationLabel
ValidationError = Class.new(StandardError)
@@ -14,6 +15,8 @@ module Projects
yield if block_given?
+ validate_classification_label(project, :external_authorization_classification_label)
+
# If the block added errors, don't try to save the project
return update_failed! if project.errors.any?
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index acbbb0da929..a39ff76b798 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -69,7 +69,7 @@ module SystemNoteService
# Called when the assignees of an Issue is changed or removed
#
- # issue - Issue object
+ # issuable - Issuable object (responds to assignees)
# project - Project owning noteable
# author - User performing the change
# assignees - Users being assigned, or nil
@@ -85,9 +85,9 @@ module SystemNoteService
# "assigned to @user1 and @user2"
#
# Returns the created Note object
- def change_issue_assignees(issue, project, author, old_assignees)
- unassigned_users = old_assignees - issue.assignees
- added_users = issue.assignees.to_a - old_assignees
+ def change_issuable_assignees(issuable, project, author, old_assignees)
+ unassigned_users = old_assignees - issuable.assignees
+ added_users = issuable.assignees.to_a - old_assignees
text_parts = []
text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
@@ -95,7 +95,7 @@ module SystemNoteService
body = text_parts.join(' and ')
- create_note(NoteSummary.new(issue, project, author, body, action: 'assignee'))
+ create_note(NoteSummary.new(issuable, project, author, body, action: 'assignee'))
end
# Called when the milestone of a Noteable is changed
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index f357dc37fe7..0ea230a44a1 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -49,12 +49,12 @@ class TodoService
todo_users.each(&:update_todos_count_cache)
end
- # When we reassign an issue we should:
+ # When we reassign an issuable we should:
#
- # * create a pending todo for new assignee if issue is assigned
+ # * create a pending todo for new assignee if issuable is assigned
#
- def reassigned_issue(issue, current_user, old_assignees = [])
- create_assignment_todo(issue, current_user, old_assignees)
+ def reassigned_issuable(issuable, current_user, old_assignees = [])
+ create_assignment_todo(issuable, current_user, old_assignees)
end
# When create a merge request we should:
@@ -82,14 +82,6 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
- # When we reassign a merge request we should:
- #
- # * creates a pending todo for new assignee if merge request is assigned
- #
- def reassigned_merge_request(merge_request, current_user)
- create_assignment_todo(merge_request, current_user)
- end
-
# When merge a merge request we should:
#
# * mark all pending todos related to the target for the current user as done
diff --git a/app/validators/x509_certificate_credentials_validator.rb b/app/validators/x509_certificate_credentials_validator.rb
new file mode 100644
index 00000000000..d2f18e956c3
--- /dev/null
+++ b/app/validators/x509_certificate_credentials_validator.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+# X509CertificateCredentialsValidator
+#
+# Custom validator to check if certificate-attribute was signed using the
+# private key stored in an attrebute.
+#
+# This can be used as an `ActiveModel::Validator` as follows:
+#
+# validates_with X509CertificateCredentialsValidator,
+# certificate: :client_certificate,
+# pkey: :decrypted_private_key,
+# pass: :decrypted_passphrase
+#
+#
+# Required attributes:
+# - certificate: The name of the accessor that returns the certificate to check
+# - pkey: The name of the accessor that returns the private key
+# Optional:
+# - pass: The name of the accessor that returns the passphrase to decrypt the
+# private key
+class X509CertificateCredentialsValidator < ActiveModel::Validator
+ def initialize(*args)
+ super
+
+ # We can't validate if we don't have a private key or certificate attributes
+ # in which case this validator is useless.
+ if options[:pkey].nil? || options[:certificate].nil?
+ raise 'Provide at least `certificate` and `pkey` attribute names'
+ end
+ end
+
+ def validate(record)
+ unless certificate = read_certificate(record)
+ record.errors.add(options[:certificate], _('is not a valid X509 certificate.'))
+ end
+
+ unless private_key = read_private_key(record)
+ record.errors.add(options[:pkey], _('could not read private key, is the passphrase correct?'))
+ end
+
+ return if private_key.nil? || certificate.nil?
+
+ unless certificate.public_key.fingerprint == private_key.public_key.fingerprint
+ record.errors.add(options[:pkey], _('private key does not match certificate.'))
+ end
+ end
+
+ private
+
+ def read_private_key(record)
+ OpenSSL::PKey.read(pkey(record).to_s, pass(record).to_s)
+ rescue OpenSSL::PKey::PKeyError, ArgumentError
+ # When the primary key could not be read, an ArgumentError is raised.
+ # This hapens when the passed key is not valid or the passphrase is incorrect
+ nil
+ end
+
+ def read_certificate(record)
+ OpenSSL::X509::Certificate.new(certificate(record).to_s)
+ rescue OpenSSL::X509::CertificateError
+ nil
+ end
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ #
+ # Allowing `#public_send` here because we don't want the validator to really
+ # care about the names of the attributes or where they come from.
+ #
+ # The credentials are mostly stored encrypted so we need to go through the
+ # accessors to get the values, `read_attribute` bypasses those.
+ def certificate(record)
+ record.public_send(options[:certificate])
+ end
+
+ def pkey(record)
+ record.public_send(options[:pkey])
+ end
+
+ def pass(record)
+ return unless options[:pass]
+
+ record.public_send(options[:pass])
+ end
+ # rubocop:enable GitlabSecurity/PublicSend
+end
diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
new file mode 100644
index 00000000000..01f6c7afe61
--- /dev/null
+++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
@@ -0,0 +1,51 @@
+%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('External authentication')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('External Classification Policy Authorization')
+ .settings-content
+
+ = form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :external_authorization_service_enabled, class: 'form-check-input'
+ = f.label :external_authorization_service_enabled, class: 'form-check-label' do
+ = _('Enable classification control using an external service')
+ %span.form-text.text-muted
+ = external_authorization_description
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/external_authorization')
+ .form-group
+ = f.label :external_authorization_service_url, _('Service URL'), class: 'label-bold'
+ = f.text_field :external_authorization_service_url, class: 'form-control'
+ %span.form-text.text-muted
+ = external_authorization_url_help_text
+ .form-group
+ = f.label :external_authorization_service_timeout, _('External authorization request timeout'), class: 'label-bold'
+ = f.number_field :external_authorization_service_timeout, class: 'form-control', min: 0.001, max: 10, step: 0.001
+ %span.form-text.text-muted
+ = external_authorization_timeout_help_text
+ = f.label :external_auth_client_cert, _('Client authentication certificate'), class: 'label-bold'
+ = f.text_area :external_auth_client_cert, class: 'form-control'
+ %span.form-text.text-muted
+ = external_authorization_client_certificate_help_text
+ .form-group
+ = f.label :external_auth_client_key, _('Client authentication key'), class: 'label-bold'
+ = f.text_area :external_auth_client_key, class: 'form-control'
+ %span.form-text.text-muted
+ = external_authorization_client_key_help_text
+ .form-group
+ = f.label :external_auth_client_key_pass, _('Client authentication key password'), class: 'label-bold'
+ = f.password_field :external_auth_client_key_pass, class: 'form-control'
+ %span.form-text.text-muted
+ = external_authorization_client_pass_help_text
+ .form-group
+ = f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold'
+ = f.text_field :external_authorization_service_default_label, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index fc9dd29b8ca..31f18ba0d56 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -68,7 +68,7 @@
.settings-content
= render 'terms'
-= render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default?
+= render 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default?
%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1b2a4cd6780..26a1f1e119c 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -7,6 +7,7 @@
.alert-wrapper
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
+ = render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/ping_consent"
- unless @hide_breadcrumbs
diff --git a/app/views/layouts/nav/_classification_level_banner.html.haml b/app/views/layouts/nav/_classification_level_banner.html.haml
new file mode 100644
index 00000000000..cc4caf079b8
--- /dev/null
+++ b/app/views/layouts/nav/_classification_level_banner.html.haml
@@ -0,0 +1,5 @@
+- if ::Gitlab::ExternalAuthorization.enabled? && @project
+ = content_for :header_content do
+ %span.badge.color-label.classification-label.has-tooltip{ title: s_('ExternalAuthorizationService|Classification label') }
+ = sprite_icon('lock-open', size: 8, css_class: 'inline')
+ = @project.external_authorization_classification_label
diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml
new file mode 100644
index 00000000000..4ab40ff2659
--- /dev/null
+++ b/app/views/notify/_reassigned_issuable_email.html.haml
@@ -0,0 +1,10 @@
+%p
+ Assignee changed
+ - if previous_assignees.any?
+ from
+ %strong= sanitize_name(previous_assignees.map(&:name).to_sentence)
+ to
+ - if issuable.assignees.any?
+ %strong= sanitize_name(issuable.assignee_list)
+ - else
+ %strong Unassigned
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index 1094d584a1c..6e84f9fb355 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml
index e81144b8fcb..08bc98ca05c 100644
--- a/app/views/notify/issue_due_email.html.haml
+++ b/app/views/notify/issue_due_email.html.haml
@@ -3,7 +3,7 @@
- if @issue.assignees.any?
%p
- Assignee: #{@issue.assignee_list}
+ = assignees_label(@issue)
%p
This issue is due on: #{@issue.due_date.to_s(:medium)}
diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb
index 3c7a57a8a2e..ae50b703fe3 100644
--- a/app/views/notify/issue_due_email.text.erb
+++ b/app/views/notify/issue_due_email.text.erb
@@ -2,6 +2,6 @@ The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %>
-Assignee: <%= @issue.assignee_list %>
+<%= assignees_label(@issue) %>
<%= @issue.description %>
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index b9b9e0c3ad7..e3b24bbd405 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index 0c7bf1bb044..e9708a297d7 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 045a43cbc84..d623e701a30 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index e6cdaf85c0d..8aa7939dd0b 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -4,7 +4,7 @@
- if @issue.assignees.any?
%p
- Assignee: #{@issue.assignee_list}
+ = assignees_label(@issue)
- if @issue.description
%div
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index 58a2bcbe5eb..ff258711b48 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -2,6 +2,6 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
-Assignee: <%= @issue.assignee_list %>
+<%= assignees_label(@issue) %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
index 173091e4a80..8e95063b40f 100644
--- a/app/views/notify/new_mention_in_issue_email.text.erb
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -2,6 +2,6 @@ You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
-Assignee: <%= sanitize_name(@issue.assignee_list) %>
+<%= assignees_label(@issue) %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
index 96a4f3f9eac..3c78e257a88 100644
--- a/app/views/notify/new_mention_in_merge_request_email.text.erb
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -4,6 +4,6 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= sanitize_name(@merge_request.author_name) %>
-Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
+= assignees_label(@merge_request)
<%= @merge_request.description %>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index db23447dd39..77d2e65d285 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -5,9 +5,9 @@
%p.details
!= merge_path_description(@merge_request, '&rarr;')
-- if @merge_request.assignee_id.present?
+- if @merge_request.assignees.any?
%p
- Assignee: #{sanitize_name(@merge_request.assignee_name)}
+ = assignees_label(@merge_request)
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index 754f4bca1cd..e6c42f1cf5f 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -4,7 +4,7 @@ New Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %>
-Assignee: <%= @merge_request.assignee_name %>
+<%= assignees_label(@merge_request) %>
<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
<%= @merge_request.description %>
diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml
index 6d25488a7e2..6b088927623 100644
--- a/app/views/notify/reassigned_issue_email.html.haml
+++ b/app/views/notify/reassigned_issue_email.html.haml
@@ -1,10 +1 @@
-%p
- Assignee changed
- - if @previous_assignees.any?
- from
- %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence)
- to
- - if @issue.assignees.any?
- %strong= @issue.assignee_list
- - else
- %strong Unassigned
+= render 'reassigned_issuable_email', issuable: @issue, previous_assignees: @previous_assignees
diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml
index e4f19bc3200..0aefca6b14a 100644
--- a/app/views/notify/reassigned_merge_request_email.html.haml
+++ b/app/views/notify/reassigned_merge_request_email.html.haml
@@ -1,10 +1 @@
-%p
- Assignee changed
- - if @previous_assignee
- from
- %strong= sanitize_name(@previous_assignee.name)
- to
- - if @merge_request.assignee_id
- %strong= sanitize_name(@merge_request.assignee_name)
- - else
- %strong Unassigned
+= render 'reassigned_issuable_email', issuable: @merge_request, previous_assignees: @previous_assignees
diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb
index 96c770b5219..82ec7aa0fa4 100644
--- a/app/views/notify/reassigned_merge_request_email.text.erb
+++ b/app/views/notify/reassigned_merge_request_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
-Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%>
- to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %>
+Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
+ to <%= "#{@merge_request.assignees.any? ? @merge_request.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/projects/_classification_policy_settings.html.haml b/app/views/projects/_classification_policy_settings.html.haml
new file mode 100644
index 00000000000..57c7a718d53
--- /dev/null
+++ b/app/views/projects/_classification_policy_settings.html.haml
@@ -0,0 +1,8 @@
+- if ::Gitlab::ExternalAuthorization.enabled?
+ .form-group
+ = f.label :external_authorization_classification_label, class: 'label-bold' do
+ = s_('ExternalAuthorizationService|Classification Label')
+ %span.light (optional)
+ = f.text_field :external_authorization_classification_label, class: "form-control"
+ %span.form-text.text-muted
+ = external_classification_label_help_message
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 98017bea0c9..abf2fb7dc57 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -32,7 +32,7 @@
%span.light (optional)
= f.text_area :description, class: "form-control", rows: 3, maxlength: 250
- = render_if_exists 'projects/classification_policy_settings', f: f
+ = render 'projects/classification_policy_settings', f: f
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index ce7c7091c93..377b2a6d8d9 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -46,7 +46,7 @@
CLOSED
- if issue.assignees.any?
%li
- = render 'shared/issuable/assignees', project: @project, issue: issue
+ = render 'shared/issuable/assignees', project: @project, issuable: issue
= render 'shared/issuable_meta_data', issuable: issue
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index b8e0b66e277..47c8e3d73f5 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -53,9 +53,9 @@
%li.issuable-pipeline-broken.d-none.d-sm-inline-block
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
= icon('exclamation-triangle')
- - if merge_request.assignee
+ - if merge_request.assignees.any?
%li
- = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name'))
+ = render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
= render_if_exists 'projects/merge_requests/approvals_count', merge_request: merge_request
= render 'shared/issuable_meta_data', issuable: merge_request
diff --git a/app/views/shared/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml
index 1374da9d82c..af6a519a967 100644
--- a/app/views/shared/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml
@@ -19,7 +19,7 @@
":data-name" => "assignee.name",
":data-username" => "assignee.username" }
.dropdown
- - dropdown_options = issue_assignees_dropdown_options
+ - dropdown_options = assignees_dropdown_options('issue')
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
":data-issuable-id" => "issue.iid" }
= dropdown_options[:title]
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index ef3d44a9241..24734ed66cf 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -1,9 +1,9 @@
- max_render = 4
-- assignees_rendering_overflow = issue.assignees.size > max_render
+- assignees_rendering_overflow = issuable.assignees.size > max_render
- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
-- more_assignees_count = issue.assignees.size - render_count
+- more_assignees_count = issuable.assignees.size - render_count
-- issue.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
+- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, assignee, name: false, title: "Assigned to :name")
- if more_assignees_count.positive?
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 909eb738f95..a05a13814ac 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -21,10 +21,7 @@
.title
Assignee
.filter-item
- - if type == :issues
- - field_name = "update[assignee_ids][]"
- - else
- - field_name = "update[assignee_id]"
+ - field_name = "update[assignee_ids][]"
= dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.block
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 1a59055f652..ab01094ed6e 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,42 +1,10 @@
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
-- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
-- if issuable_type == "issue"
- #js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
- .title.hide-collapsed
- = _('Assignee')
- = icon('spinner spin')
-- else
- - assignee = assignees.first
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: (issuable_sidebar.dig(:assignee, :name) || _('Assignee')) }
- - if issuable_sidebar[:assignee]
- = link_to_member(@project, assignee, size: 24)
- - else
- = icon('user', 'aria-hidden': 'true')
+#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- - if !signed_in
- %a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
- = sidebar_gutter_toggle_icon
- .value.hide-collapsed
- - if issuable_sidebar[:assignee]
- = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
- - unless issuable_sidebar[:assignee][:can_merge]
- %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
- = icon('exclamation-triangle', 'aria-hidden': 'true')
- %span.username
- @#{issuable_sidebar[:assignee][:username]}
- - else
- %span.assign-yourself.no-value
- = _('No assignee')
- - if can_edit_issuable
- \-
- %a.js-assign-yourself{ href: '#' }
- = _('assign yourself')
+ = icon('spinner spin')
.selectbox.hide-collapsed
- if assignees.none?
@@ -59,17 +27,15 @@
ability_name: issuable_type,
null_user: true,
display: 'static' } }
- - title = _('Select assignee')
- - if issuable_type == "issue"
- - dropdown_options = issue_assignees_dropdown_options
- - title = dropdown_options[:title]
- - options[:toggle_class] += ' js-multiselect js-save-user-data'
- - data = { field_name: "#{issuable_type}[assignee_ids][]" }
- - data[:multi_select] = true
- - data['dropdown-title'] = title
- - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- - options[:data].merge!(data)
+ - dropdown_options = assignees_dropdown_options(issuable_type)
+ - title = dropdown_options[:title]
+ - options[:toggle_class] += ' js-multiselect js-save-user-data'
+ - data = { field_name: "#{issuable_type}[assignee_ids][]" }
+ - data[:multi_select] = true
+ - data['dropdown-title'] = title
+ - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
+ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
+ - options[:data].merge!(data)
= dropdown_tag(title, options: options)
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
deleted file mode 100644
index 05c03dedd91..00000000000
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-- merge_request = issuable
-.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- - if merge_request.assignee
- = link_to_member(@project, merge_request.assignee, size: 24)
- - else
- = icon('user', 'aria-hidden': 'true')
- .title.hide-collapsed
- Assignee
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- .value.hide-collapsed
- - if merge_request.assignee
- = link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
- - unless merge_request.can_be_merged_by?(merge_request.assignee)
- %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
- = icon('exclamation-triangle', 'aria-hidden': 'true')
- %span.username
- = merge_request.assignee.to_reference
- - else
- %span.assign-yourself.no-value
- No assignee
- - if can_edit_issuable
- \-
- %a.js-assign-yourself{ href: '#' }
- assign yourself
-
- .selectbox.hide-collapsed
- = f.hidden_field 'assignee_id', value: merge_request.assignee_id, id: 'issue_assignee_id'
- = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project&.id, author_id: merge_request.author_id, field_name: 'merge_request[assignee_id]', issue_update: issuable_json_path(merge_request), ability_name: 'merge_request', null_user: true } })
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index e370dff9526..1e03440a5dc 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -8,11 +8,8 @@
%hr
.row
%div{ class: (has_due_date ? "col-lg-6" : "col-12") }
- .form-group.row.issue-assignee
- - if issuable.is_a?(Issue)
- = render "shared/issuable/form/metadata_issue_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- - else
- = render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date
+ .form-group.row.merge-request-assignee
+ = render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date
.form-group.row.issue-milestone
= form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
index 6d4f9ccd66f..5336159e762 100644
--- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
@@ -1,4 +1,4 @@
-= form.label :assignee_ids, "Assignee", class: "col-form-label #{"col-md-2 col-lg-4" if has_due_date}"
+= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
@@ -7,5 +7,5 @@
- if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
- = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options)
- = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
+ = dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
+ = link_to 'Assign to me', '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
diff --git a/changelogs/unreleased/57131-external_auth_to_core.yml b/changelogs/unreleased/57131-external_auth_to_core.yml
new file mode 100644
index 00000000000..aacd3916c82
--- /dev/null
+++ b/changelogs/unreleased/57131-external_auth_to_core.yml
@@ -0,0 +1,5 @@
+---
+title: Move "Authorize project access with external service" to Core
+merge_request: 26823
+author:
+type: changed
diff --git a/changelogs/unreleased/do-not-reopen-merged-mr.yml b/changelogs/unreleased/do-not-reopen-merged-mr.yml
new file mode 100644
index 00000000000..14d1455cca4
--- /dev/null
+++ b/changelogs/unreleased/do-not-reopen-merged-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Remove a "reopen merge request button" on a "merged" merge request
+merge_request: 26965
+author: Hiroyuki Sato
+type: fixed
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index 1952f84ed62..43b69470d2c 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -21,7 +21,7 @@ Gitlab::Seeder.quiet do
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentences(3).join(" "),
milestone: project.milestones.sample,
- assignee: project.team.users.sample,
+ assignees: [project.team.users.sample],
label_ids: label_ids
}
diff --git a/db/migrate/20171211131502_add_external_classification_authorization_settings_to_appliction_settings.rb b/db/migrate/20171211131502_add_external_classification_authorization_settings_to_appliction_settings.rb
new file mode 100644
index 00000000000..a7dec8732fb
--- /dev/null
+++ b/db/migrate/20171211131502_add_external_classification_authorization_settings_to_appliction_settings.rb
@@ -0,0 +1,29 @@
+class AddExternalClassificationAuthorizationSettingsToApplictionSettings < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings,
+ :external_authorization_service_enabled,
+ :boolean,
+ default: false
+ add_column :application_settings,
+ :external_authorization_service_url,
+ :string
+ add_column :application_settings,
+ :external_authorization_service_default_label,
+ :string
+ end
+
+ def down
+ remove_column :application_settings,
+ :external_authorization_service_default_label
+ remove_column :application_settings,
+ :external_authorization_service_url
+ remove_column :application_settings,
+ :external_authorization_service_enabled
+ end
+end
diff --git a/db/migrate/20171218140451_add_external_authorization_service_classification_label_to_projects.rb b/db/migrate/20171218140451_add_external_authorization_service_classification_label_to_projects.rb
new file mode 100644
index 00000000000..7b83580f025
--- /dev/null
+++ b/db/migrate/20171218140451_add_external_authorization_service_classification_label_to_projects.rb
@@ -0,0 +1,11 @@
+class AddExternalAuthorizationServiceClassificationLabelToProjects < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :projects,
+ :external_authorization_classification_label,
+ :string
+ end
+end
diff --git a/db/migrate/20180314100728_add_external_authorization_service_timeout_to_application_settings.rb b/db/migrate/20180314100728_add_external_authorization_service_timeout_to_application_settings.rb
new file mode 100644
index 00000000000..c3c6aa0ddf8
--- /dev/null
+++ b/db/migrate/20180314100728_add_external_authorization_service_timeout_to_application_settings.rb
@@ -0,0 +1,18 @@
+class AddExternalAuthorizationServiceTimeoutToApplicationSettings < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ # We can use the regular `add_column` with a default since `application_settings`
+ # is a small table.
+ add_column :application_settings,
+ :external_authorization_service_timeout,
+ :float,
+ default: 0.5
+ end
+
+ def down
+ remove_column :application_settings, :external_authorization_service_timeout
+ end
+end
diff --git a/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb
new file mode 100644
index 00000000000..ee3d1078f5e
--- /dev/null
+++ b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb
@@ -0,0 +1,16 @@
+class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration[4.2]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings,
+ :external_auth_client_cert, :text
+ add_column :application_settings,
+ :encrypted_external_auth_client_key, :text
+ add_column :application_settings,
+ :encrypted_external_auth_client_key_iv, :string
+ add_column :application_settings,
+ :encrypted_external_auth_client_key_pass, :string
+ add_column :application_settings,
+ :encrypted_external_auth_client_key_pass_iv, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ca5b04e810a..c044fcc90c6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -178,6 +178,15 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.integer "local_markdown_version", default: 0, null: false
t.integer "first_day_of_week", default: 0, null: false
t.integer "default_project_creation", default: 2, null: false
+ t.boolean "external_authorization_service_enabled", default: false, null: false
+ t.string "external_authorization_service_url"
+ t.string "external_authorization_service_default_label"
+ t.float "external_authorization_service_timeout", default: 0.5
+ t.text "external_auth_client_cert"
+ t.text "encrypted_external_auth_client_key"
+ t.string "encrypted_external_auth_client_key_iv"
+ t.string "encrypted_external_auth_client_key_pass"
+ t.string "encrypted_external_auth_client_key_pass_iv"
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -1756,6 +1765,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.string "runners_token_encrypted"
t.string "bfg_object_map"
t.boolean "detected_repository_languages"
+ t.string "external_authorization_classification_label"
t.index ["ci_id"], name: "index_projects_on_ci_id", using: :btree
t.index ["created_at"], name: "index_projects_on_created_at", using: :btree
t.index ["creator_id"], name: "index_projects_on_creator_id", using: :btree
diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md
index 6c232fe6086..a128a7c7dd3 100644
--- a/doc/gitlab-basics/fork-project.md
+++ b/doc/gitlab-basics/fork-project.md
@@ -1,8 +1,8 @@
# How to fork a project
-A fork is a copy of an original repository that you can put in another namespace
-where you can experiment and apply changes that you can later decide if
-publishing or not, without affecting your original project.
+A fork is a copy of an original repository that you put in another namespace
+where you can experiment and apply changes that you can later decide whether or
+not to share, without affecting the original project.
It takes just a few steps to fork a project in GitLab.
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 756b8b698c7..fa7ab19ece6 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -149,7 +149,7 @@ verify your domain's ownership with a TXT record:
Once you've set the DNS record, you'll need navigate to your project's
**Setting > Pages** and click **+ New domain** to add your custom domain to
GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssltls-certificates)
-to make your website accessible under HTTPS or leave it blank. If don't add a certificate,
+to make your website accessible under HTTPS or leave it blank. If you don't add a certificate,
your site will be accessible only via HTTP:
![Add new domain](img/add_certificate_to_pages.png)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index f9773086f69..4bdac278add 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -277,6 +277,7 @@ module API
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
}
+ expose :external_authorization_classification_label
# rubocop: disable CodeReuse/ActiveRecord
def self.preload_relation(projects_relation, options = {})
@@ -663,7 +664,11 @@ module API
expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
- expose :author, :assignee, using: Entities::UserBasic
+ expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
+ merge_request.assignee
+ end
+ expose :author, :assignees, using: Entities::UserBasic
+
expose :source_project_id, :target_project_id
expose :labels do |merge_request|
# Avoids an N+1 query since labels are preloaded
@@ -1116,6 +1121,8 @@ module API
expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
+ expose(*::ApplicationSettingsHelper.external_authorization_service_attributes)
+
# support legacy names, can be removed in v5
expose :password_authentication_enabled_for_web, as: :password_authentication_enabled
expose :password_authentication_enabled_for_web, as: :signin_enabled
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 7b858dc2e72..aaf32dafca4 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -29,13 +29,13 @@ module API
optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
+ optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
end
if Gitlab.ee?
params :optional_project_params_ee do
optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
optional :approvals_before_merge, type: Integer, desc: 'How many approvers should approve merge request by default'
- optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
optional :mirror, type: Boolean, desc: 'Enables pull mirroring in a project'
optional :mirror_trigger_builds, type: Boolean, desc: 'Pull mirroring triggers builds'
end
@@ -72,7 +72,8 @@ module API
:tag_list,
:visibility,
:wiki_enabled,
- :avatar
+ :avatar,
+ :external_authorization_classification_label
]
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index e4b21b7d1c4..1cc0ecc6df8 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -20,6 +20,7 @@ module API
def self.update_params_at_least_one_of
%i[
assignee_id
+ assignee_ids
description
labels
milestone_id
@@ -184,6 +185,7 @@ module API
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
+ optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
@@ -231,6 +233,7 @@ module API
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch)
+ mr_params = convert_parameters_from_legacy_format(mr_params)
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
@@ -334,6 +337,7 @@ module API
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+ mr_params = convert_parameters_from_legacy_format(mr_params)
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index d96cdc31212..b064747e5fc 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -168,7 +168,9 @@ module API
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
end
- optional_attributes = ::ApplicationSettingsHelper.visible_attributes << :performance_bar_allowed_group_id
+ optional_attributes = [*::ApplicationSettingsHelper.visible_attributes,
+ *::ApplicationSettingsHelper.external_authorization_service_attributes,
+ :performance_bar_allowed_group_id]
if Gitlab.ee?
optional_attributes += EE::ApplicationSettingsHelper.possible_licensed_attributes
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index e8147ac591a..d7bf450465e 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -10,7 +10,7 @@ module Banzai
nodes,
MergeRequest.includes(
:author,
- :assignee,
+ :assignees,
{
# These associations are primarily used for checking permissions.
# Eager loading these ensures we don't end up running dozens of
diff --git a/lib/gitlab/external_authorization.rb b/lib/gitlab/external_authorization.rb
new file mode 100644
index 00000000000..25f8b7b3628
--- /dev/null
+++ b/lib/gitlab/external_authorization.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ExternalAuthorization
+ extend ExternalAuthorization::Config
+
+ RequestFailed = Class.new(StandardError)
+
+ def self.access_allowed?(user, label, project_path = nil)
+ return true unless perform_check?
+ return false unless user
+
+ access_for_user_to_label(user, label, project_path).has_access?
+ end
+
+ def self.rejection_reason(user, label)
+ return unless enabled?
+ return unless user
+
+ access_for_user_to_label(user, label, nil).reason
+ end
+
+ def self.access_for_user_to_label(user, label, project_path)
+ if RequestStore.active?
+ RequestStore.fetch("external_authorisation:user-#{user.id}:label-#{label}") do
+ load_access(user, label, project_path)
+ end
+ else
+ load_access(user, label, project_path)
+ end
+ end
+
+ def self.load_access(user, label, project_path)
+ access = ::Gitlab::ExternalAuthorization::Access.new(user, label).load!
+ ::Gitlab::ExternalAuthorization::Logger.log_access(access, project_path)
+
+ access
+ end
+ end
+end
diff --git a/lib/gitlab/external_authorization/access.rb b/lib/gitlab/external_authorization/access.rb
new file mode 100644
index 00000000000..e111c41fcc2
--- /dev/null
+++ b/lib/gitlab/external_authorization/access.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ExternalAuthorization
+ class Access
+ attr_reader :user,
+ :reason,
+ :loaded_at,
+ :label,
+ :load_type
+
+ def initialize(user, label)
+ @user, @label = user, label
+ end
+
+ def loaded?
+ loaded_at && (loaded_at > ExternalAuthorization::Cache::VALIDITY_TIME.ago)
+ end
+
+ def has_access?
+ @access
+ end
+
+ def load!
+ load_from_cache
+ load_from_service unless loaded?
+ self
+ end
+
+ private
+
+ def load_from_cache
+ @load_type = :cache
+ @access, @reason, @loaded_at = cache.load
+ end
+
+ def load_from_service
+ @load_type = :request
+ response = Client.new(@user, @label).request_access
+ @access = response.successful?
+ @reason = response.reason
+ @loaded_at = Time.now
+ cache.store(@access, @reason, @loaded_at) if response.valid?
+ rescue ::Gitlab::ExternalAuthorization::RequestFailed => e
+ @access = false
+ @reason = e.message
+ @loaded_at = Time.now
+ end
+
+ def cache
+ @cache ||= ExternalAuthorization::Cache.new(@user, @label)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/external_authorization/cache.rb b/lib/gitlab/external_authorization/cache.rb
new file mode 100644
index 00000000000..acdc028b4dc
--- /dev/null
+++ b/lib/gitlab/external_authorization/cache.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ExternalAuthorization
+ class Cache
+ VALIDITY_TIME = 6.hours
+
+ def initialize(user, label)
+ @user, @label = user, label
+ end
+
+ def load
+ @access, @reason, @refreshed_at = ::Gitlab::Redis::Cache.with do |redis|
+ redis.hmget(cache_key, :access, :reason, :refreshed_at)
+ end
+
+ [access, reason, refreshed_at]
+ end
+
+ def store(new_access, new_reason, new_refreshed_at)
+ ::Gitlab::Redis::Cache.with do |redis|
+ redis.pipelined do
+ redis.mapped_hmset(
+ cache_key,
+ {
+ access: new_access.to_s,
+ reason: new_reason.to_s,
+ refreshed_at: new_refreshed_at.to_s
+ }
+ )
+
+ redis.expire(cache_key, VALIDITY_TIME)
+ end
+ end
+ end
+
+ private
+
+ def access
+ ::Gitlab::Utils.to_boolean(@access)
+ end
+
+ def reason
+ # `nil` if the cached value was an empty string
+ return unless @reason.present?
+
+ @reason
+ end
+
+ def refreshed_at
+ # Don't try to parse a time if there was no cache
+ return unless @refreshed_at.present?
+
+ Time.parse(@refreshed_at)
+ end
+
+ def cache_key
+ "external_authorization:user-#{@user.id}:label-#{@label}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/external_authorization/client.rb b/lib/gitlab/external_authorization/client.rb
new file mode 100644
index 00000000000..60aab2e7044
--- /dev/null
+++ b/lib/gitlab/external_authorization/client.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+Excon.defaults[:ssl_verify_peer] = false
+
+module Gitlab
+ module ExternalAuthorization
+ class Client
+ include ExternalAuthorization::Config
+
+ REQUEST_HEADERS = {
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json'
+ }.freeze
+
+ def initialize(user, label)
+ @user, @label = user, label
+ end
+
+ def request_access
+ response = Excon.post(
+ service_url,
+ post_params
+ )
+ ::Gitlab::ExternalAuthorization::Response.new(response)
+ rescue Excon::Error => e
+ raise ::Gitlab::ExternalAuthorization::RequestFailed.new(e)
+ end
+
+ private
+
+ def post_params
+ params = { headers: REQUEST_HEADERS,
+ body: body.to_json,
+ connect_timeout: timeout,
+ read_timeout: timeout,
+ write_timeout: timeout }
+
+ if has_tls?
+ params[:client_cert_data] = client_cert
+ params[:client_key_data] = client_key
+ params[:client_key_pass] = client_key_pass
+ end
+
+ params
+ end
+
+ def body
+ @body ||= begin
+ body = {
+ user_identifier: @user.email,
+ project_classification_label: @label
+ }
+
+ if @user.ldap_identity
+ body[:user_ldap_dn] = @user.ldap_identity.extern_uid
+ end
+
+ body
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/external_authorization/config.rb b/lib/gitlab/external_authorization/config.rb
new file mode 100644
index 00000000000..8654a8c1e2e
--- /dev/null
+++ b/lib/gitlab/external_authorization/config.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ExternalAuthorization
+ module Config
+ extend self
+
+ def timeout
+ application_settings.external_authorization_service_timeout
+ end
+
+ def service_url
+ application_settings.external_authorization_service_url
+ end
+
+ def enabled?
+ application_settings.external_authorization_service_enabled
+ end
+
+ def perform_check?
+ enabled? && service_url.present?
+ end
+
+ def client_cert
+ application_settings.external_auth_client_cert
+ end
+
+ def client_key
+ application_settings.external_auth_client_key
+ end
+
+ def client_key_pass
+ application_settings.external_auth_client_key_pass
+ end
+
+ def has_tls?
+ client_cert.present? && client_key.present?
+ end
+
+ private
+
+ def application_settings
+ ::Gitlab::CurrentSettings.current_application_settings
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/external_authorization/logger.rb b/lib/gitlab/external_authorization/logger.rb
new file mode 100644
index 00000000000..61246cd870e
--- /dev/null
+++ b/lib/gitlab/external_authorization/logger.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ExternalAuthorization
+ class Logger < ::Gitlab::Logger
+ def self.log_access(access, project_path)
+ status = access.has_access? ? "GRANTED" : "DENIED"
+ message = ["#{status} #{access.user.email} access to '#{access.label}'"]
+
+ message << "(#{project_path})" if project_path.present?
+ message << "- #{access.load_type} #{access.loaded_at}" if access.load_type == :cache
+
+ info(message.join(' '))
+ end
+
+ def self.file_name_noext
+ 'external-policy-access-control'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/external_authorization/response.rb b/lib/gitlab/external_authorization/response.rb
new file mode 100644
index 00000000000..4f3fe5882db
--- /dev/null
+++ b/lib/gitlab/external_authorization/response.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ExternalAuthorization
+ class Response
+ include ::Gitlab::Utils::StrongMemoize
+
+ def initialize(excon_response)
+ @excon_response = excon_response
+ end
+
+ def valid?
+ @excon_response && [200, 401, 403].include?(@excon_response.status)
+ end
+
+ def successful?
+ valid? && @excon_response.status == 200
+ end
+
+ def reason
+ parsed_response['reason'] if parsed_response
+ end
+
+ private
+
+ def parsed_response
+ strong_memoize(:parsed_response) { parse_response! }
+ end
+
+ def parse_response!
+ JSON.parse(@excon_response.body)
+ rescue JSON::JSONError
+ # The JSON response is optional, so don't fail when it's missing
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
index 0803df65632..b8da6731081 100644
--- a/lib/gitlab/hook_data/issuable_builder.rb
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -20,11 +20,7 @@ module Gitlab
repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
}
- if issuable.is_a?(Issue)
- hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
- else
- hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee
- end
+ hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
hook_data
end
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
index d77b1d04644..a8e993e087e 100644
--- a/lib/gitlab/hook_data/merge_request_builder.rb
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -34,7 +34,6 @@ module Gitlab
end
SAFE_HOOK_RELATIONS = %i[
- assignee
labels
total_time_spent
].freeze
@@ -51,7 +50,9 @@ module Gitlab
work_in_progress: merge_request.work_in_progress?,
total_time_spent: merge_request.total_time_spent,
human_total_time_spent: merge_request.human_total_time_spent,
- human_time_estimate: merge_request.human_time_estimate
+ human_time_estimate: merge_request.human_time_estimate,
+ assignee_ids: merge_request.assignee_ids,
+ assignee_id: merge_request.assignee_ids.first # This key is deprecated
}
merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index aa116a44254..0e04b67f5b8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -423,6 +423,9 @@ msgstr ""
msgid "Access forbidden. Check your access level."
msgstr ""
+msgid "Access to '%{classification_label}' not allowed"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -987,9 +990,6 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
-msgid "Assigned to :name"
-msgstr ""
-
msgid "Assigned to me"
msgstr ""
@@ -1707,6 +1707,9 @@ msgstr ""
msgid "CiVariable|Validation failed"
msgstr ""
+msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
+msgstr ""
+
msgid "Clear"
msgstr ""
@@ -1737,6 +1740,15 @@ msgstr ""
msgid "Click to expand text"
msgstr ""
+msgid "Client authentication certificate"
+msgstr ""
+
+msgid "Client authentication key"
+msgstr ""
+
+msgid "Client authentication key password"
+msgstr ""
+
msgid "Clients"
msgstr ""
@@ -2783,6 +2795,9 @@ msgstr ""
msgid "Default artifacts expiration"
msgstr ""
+msgid "Default classification label"
+msgstr ""
+
msgid "Default first day of the week"
msgstr ""
@@ -3199,6 +3214,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
+msgid "Enable classification control using an external service"
+msgstr ""
+
msgid "Enable error tracking"
msgstr ""
@@ -3628,12 +3646,33 @@ msgstr ""
msgid "Explore public groups"
msgstr ""
+msgid "External Classification Policy Authorization"
+msgstr ""
+
msgid "External URL"
msgstr ""
msgid "External Wiki"
msgstr ""
+msgid "External authentication"
+msgstr ""
+
+msgid "External authorization denied access to this project"
+msgstr ""
+
+msgid "External authorization request timeout"
+msgstr ""
+
+msgid "ExternalAuthorizationService|Classification Label"
+msgstr ""
+
+msgid "ExternalAuthorizationService|Classification label"
+msgstr ""
+
+msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
+msgstr ""
+
msgid "Facebook"
msgstr ""
@@ -4349,6 +4388,9 @@ msgstr ""
msgid "If enabled"
msgstr ""
+msgid "If enabled, access to projects will be validated on an external service using their classification label."
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
msgstr ""
@@ -5474,9 +5516,6 @@ msgstr ""
msgid "No activities found"
msgstr ""
-msgid "No assignee"
-msgstr ""
-
msgid "No branches found"
msgstr ""
@@ -5564,9 +5603,6 @@ msgstr ""
msgid "None"
msgstr ""
-msgid "Not allowed to merge"
-msgstr ""
-
msgid "Not available"
msgstr ""
@@ -7277,9 +7313,6 @@ msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
-msgid "Select assignee"
-msgstr ""
-
msgid "Select branch/tag"
msgstr ""
@@ -7394,6 +7427,9 @@ msgstr ""
msgid "Service Templates"
msgstr ""
+msgid "Service URL"
+msgstr ""
+
msgid "Session duration (minutes)"
msgstr ""
@@ -8136,6 +8172,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
+msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
+msgstr ""
+
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
msgstr ""
@@ -8217,6 +8256,9 @@ msgstr ""
msgid "The name %{entryName} is already taken in this directory."
msgstr ""
+msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
+msgstr ""
+
msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
@@ -8229,6 +8271,9 @@ msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
+msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
+msgstr ""
+
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr ""
@@ -8592,6 +8637,9 @@ msgstr ""
msgid "Time estimate"
msgstr ""
+msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -8860,9 +8908,6 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
-msgid "Toggle sidebar"
-msgstr ""
-
msgid "ToggleButton|Toggle Status: OFF"
msgstr ""
@@ -9367,6 +9412,9 @@ msgstr ""
msgid "When fast-forward merge is not possible, the user is given the option to rebase."
msgstr ""
+msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks."
+msgstr ""
+
msgid "When this merge request is accepted"
msgid_plural "When these merge requests are accepted"
msgstr[0] ""
@@ -9870,9 +9918,6 @@ msgstr ""
msgid "among other things"
msgstr ""
-msgid "assign yourself"
-msgstr ""
-
msgid "attach a new file"
msgstr ""
@@ -9894,6 +9939,9 @@ msgstr ""
msgid "connecting"
msgstr ""
+msgid "could not read private key, is the passphrase correct?"
+msgstr ""
+
msgid "customize"
msgstr ""
@@ -9982,6 +10030,9 @@ msgstr ""
msgid "index"
msgstr ""
+msgid "is not a valid X509 certificate."
+msgstr ""
+
msgid "issue boards"
msgstr ""
@@ -10264,6 +10315,9 @@ msgstr ""
msgid "private"
msgstr ""
+msgid "private key does not match certificate."
+msgstr ""
+
msgid "processing"
msgstr ""
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index 20d9c336367..56fbf59b9bc 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -26,7 +26,7 @@ module QA
element :issuable_label
end
- view 'app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml' do
+ view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do
element :assign_to_me_link
end
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index e324f972640..22e47ce2c0a 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -38,13 +38,13 @@ function previousDeployFailed() {
local deploy="${1}"
echoinfo "Checking for previous deployment of ${deploy}" true
- helm status ${deploy} >/dev/null 2>&1
+ helm status "${deploy}" >/dev/null 2>&1
local status=$?
# if `status` is `0`, deployment exists, has a status
if [ $status -eq 0 ]; then
echoinfo "Previous deployment found, checking status..."
- deployment_status=$(helm status ${deploy} | grep ^STATUS | cut -d' ' -f2)
+ deployment_status=$(helm status "${deploy}" | grep ^STATUS | cut -d' ' -f2)
echoinfo "Previous deployment state: ${deployment_status}"
if [[ "$deployment_status" == "FAILED" || "$deployment_status" == "PENDING_UPGRADE" || "$deployment_status" == "PENDING_INSTALL" ]]; then
status=0;
@@ -64,13 +64,8 @@ function delete() {
return
fi
- local track="${1-stable}"
local name="$CI_ENVIRONMENT_SLUG"
- if [[ "$track" != "stable" ]]; then
- name="$name-$track"
- fi
-
echoinfo "Deleting release '$name'..." true
helm delete --purge "$name" || true
@@ -97,7 +92,8 @@ function get_pod() {
echoinfo "Running '${get_pod_cmd}'" true
while true; do
- local pod_name="$(eval $get_pod_cmd)"
+ local pod_name
+ pod_name="$(eval "${get_pod_cmd}")"
[[ "${pod_name}" == "" ]] || break
echoinfo "Waiting till '${app_name}' pod is ready";
@@ -113,7 +109,8 @@ function perform_review_app_deployment() {
ensure_namespace
install_tiller
install_external_dns
- time deploy
+ time deploy || true
+ wait_for_review_app_to_be_accessible
add_license
}
@@ -156,7 +153,8 @@ function install_tiller() {
function install_external_dns() {
local release_name="dns-gitlab-review-app"
- local domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}')
+ local domain
+ domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}')
echoinfo "Installing external DNS for domain ${domain}..." true
if ! deployExists "${KUBE_NAMESPACE}" "${release_name}" || previousDeployFailed "${release_name}" ; then
@@ -182,17 +180,17 @@ function create_secret() {
echoinfo "Creating the ${CI_ENVIRONMENT_SLUG}-gitlab-initial-root-password secret in the ${KUBE_NAMESPACE} namespace..." true
kubectl create secret generic -n "$KUBE_NAMESPACE" \
- $CI_ENVIRONMENT_SLUG-gitlab-initial-root-password \
- --from-literal=password=$REVIEW_APPS_ROOT_PASSWORD \
+ "${CI_ENVIRONMENT_SLUG}-gitlab-initial-root-password" \
+ --from-literal="password=${REVIEW_APPS_ROOT_PASSWORD}" \
--dry-run -o json | kubectl apply -f -
}
function download_gitlab_chart() {
echoinfo "Downloading the GitLab chart..." true
- curl -o gitlab.tar.bz2 https://gitlab.com/charts/gitlab/-/archive/$GITLAB_HELM_CHART_REF/gitlab-$GITLAB_HELM_CHART_REF.tar.bz2
+ curl -o gitlab.tar.bz2 "https://gitlab.com/charts/gitlab/-/archive/${GITLAB_HELM_CHART_REF}/gitlab-${GITLAB_HELM_CHART_REF}.tar.bz2"
tar -xjf gitlab.tar.bz2
- cd gitlab-$GITLAB_HELM_CHART_REF
+ cd "gitlab-${GITLAB_HELM_CHART_REF}"
echoinfo "Adding the gitlab repo to Helm..."
helm repo add gitlab https://charts.gitlab.io
@@ -202,18 +200,9 @@ function download_gitlab_chart() {
}
function deploy() {
- local track="${1-stable}"
local name="$CI_ENVIRONMENT_SLUG"
-
- if [[ "$track" != "stable" ]]; then
- name="$name-$track"
- fi
echoinfo "Deploying ${name}..." true
- replicas="1"
- service_enabled="false"
- postgres_enabled="$POSTGRES_ENABLED"
-
IMAGE_REPOSITORY="registry.gitlab.com/gitlab-org/build/cng-mirror"
IMAGE_VERSION="${CI_PROJECT_NAME#gitlab-}"
gitlab_migrations_image_repository="${IMAGE_REPOSITORY}/gitlab-rails-${IMAGE_VERSION}"
@@ -224,24 +213,6 @@ function deploy() {
gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell"
gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-${IMAGE_VERSION}"
- # canary uses stable db
- [[ "$track" == "canary" ]] && postgres_enabled="false"
-
- env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' )
- env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' )
-
- if [[ "$track" == "stable" ]]; then
- # for stable track get number of replicas from `PRODUCTION_REPLICAS`
- eval new_replicas=\$${env_slug}_REPLICAS
- service_enabled="true"
- else
- # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
- eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
- fi
- if [[ -n "$new_replicas" ]]; then
- replicas="$new_replicas"
- fi
-
# Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade`
if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed "$CI_ENVIRONMENT_SLUG" ; then
echo "Deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG"
@@ -282,9 +253,9 @@ HELM_CMD=$(cat << EOF
--set gitlab.unicorn.image.tag="$CI_COMMIT_REF_SLUG" \
--set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \
--set gitlab.task-runner.image.tag="$CI_COMMIT_REF_SLUG" \
- --set gitlab.gitaly.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly" \
+ --set gitlab.gitaly.image.repository="$gitlab_gitaly_image_repository" \
--set gitlab.gitaly.image.tag="v$GITALY_VERSION" \
- --set gitlab.gitlab-shell.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell" \
+ --set gitlab.gitlab-shell.image.repository="$gitlab_shell_image_repository" \
--set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \
--set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \
--set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_SLUG" \
@@ -302,6 +273,31 @@ EOF
eval $HELM_CMD
}
+function wait_for_review_app_to_be_accessible() {
+ # In case the Review App isn't completely available yet. Keep trying for 5 minutes.
+ local interval=5
+ local elapsed_seconds=0
+ local max_seconds=$((5 * 60))
+ while true; do
+ local review_app_http_code
+ review_app_http_code=$(curl --silent --output /dev/null --max-time 5 --write-out "%{http_code}" "${CI_ENVIRONMENT_URL}/users/sign_in")
+ if [[ "${review_app_http_code}" -eq "200" ]] || [[ "${elapsed_seconds}" -gt "${max_seconds}" ]]; then
+ break
+ fi
+
+ printf "."
+ let "elapsed_seconds+=interval"
+ sleep ${interval}
+ done
+
+ if [[ "${review_app_http_code}" == "200" ]]; then
+ echoinfo "The Review App at ${CI_ENVIRONMENT_URL} is ready!"
+ else
+ echoerr "The Review App at ${CI_ENVIRONMENT_URL} isn't ready after 5 minutes of polling..."
+ exit 1
+ fi
+}
+
function add_license() {
if [ -z "${REVIEW_APPS_EE_LICENSE}" ]; then echo "License not found" && return; fi
@@ -311,10 +307,10 @@ function add_license() {
echoinfo "Installing license..." true
echo "${REVIEW_APPS_EE_LICENSE}" > /tmp/license.gitlab
- kubectl -n "$KUBE_NAMESPACE" cp /tmp/license.gitlab ${task_runner_pod}:/tmp/license.gitlab
+ kubectl -n "$KUBE_NAMESPACE" cp /tmp/license.gitlab "${task_runner_pod}":/tmp/license.gitlab
rm /tmp/license.gitlab
- kubectl -n "$KUBE_NAMESPACE" exec -it ${task_runner_pod} -- /srv/gitlab/bin/rails runner -e production \
+ kubectl -n "$KUBE_NAMESPACE" exec -it "${task_runner_pod}" -- /srv/gitlab/bin/rails runner -e production \
'
content = File.read("/tmp/license.gitlab").strip;
FileUtils.rm_f("/tmp/license.gitlab");
@@ -344,7 +340,8 @@ function get_job_id() {
local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}"
echoinfo "GET ${url}"
- local job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last")
+ local job_id
+ job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last")
[[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break
let "page++"
@@ -360,20 +357,23 @@ function get_job_id() {
function play_job() {
local job_name="${1}"
- local job_id=$(get_job_id "${job_name}" "scope=manual");
+ local job_id
+ job_id=$(get_job_id "${job_name}" "scope=manual");
if [ -z "${job_id}" ]; then return; fi
local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play"
echoinfo "POST ${url}"
- local job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url")
+ local job_url
+ job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url")
echoinfo "Manual job '${job_name}' started at: ${job_url}"
}
function wait_for_job_to_be_done() {
local job_name="${1}"
local query_string="${2}"
- local job_id=$(get_job_id "${job_name}" "${query_string}");
+ local job_id
+ job_id=$(get_job_id "${job_name}" "${query_string}")
if [ -z "${job_id}" ]; then return; fi
echoinfo "Waiting for the '${job_name}' job to finish..."
@@ -385,7 +385,8 @@ function wait_for_job_to_be_done() {
local interval=30
local elapsed_seconds=0
while true; do
- local job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".status" | sed -e s/\"//g)
+ local job_status
+ job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".status" | sed -e s/\"//g)
[[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break
printf "."
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 1a7be4c9a85..f3450a8289f 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -92,6 +92,28 @@ describe Admin::ApplicationSettingsController do
expect(response).to redirect_to(admin_application_settings_path)
expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
end
+
+ context 'external policy classification settings' do
+ let(:settings) do
+ {
+ external_authorization_service_enabled: true,
+ external_authorization_service_url: 'https://custom.service/',
+ external_authorization_service_default_label: 'default',
+ external_authorization_service_timeout: 3,
+ external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'),
+ external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'),
+ external_auth_client_key_pass: "5iveL!fe"
+ }
+ end
+
+ it 'updates settings when the feature is available' do
+ put :update, params: { application_setting: settings }
+
+ settings.each do |attribute, value|
+ expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
+ end
+ end
+ end
end
describe 'PUT #reset_registration_token' do
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 5eb05f01b8d..309cac47928 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Boards::IssuesController do
+ include ExternalAuthorizationServiceHelpers
+
let(:project) { create(:project, :private) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
@@ -136,6 +138,30 @@ describe Boards::IssuesController do
end
end
+ context 'with external authorization' do
+ before do
+ sign_in(user)
+ enable_external_authorization_service_check
+ end
+
+ it 'returns a 403 for group boards' do
+ group = create(:group)
+ group_board = create(:board, group: group)
+
+ list_issues(user: user, board: group_board)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'is successful for project boards' do
+ project_board = create(:board, project: project)
+
+ list_issues(user: user, board: project_board)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
def list_issues(user:, board:, list: nil)
sign_in(user)
diff --git a/spec/controllers/concerns/project_unauthorized_spec.rb b/spec/controllers/concerns/project_unauthorized_spec.rb
new file mode 100644
index 00000000000..90b59b027cf
--- /dev/null
+++ b/spec/controllers/concerns/project_unauthorized_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe ProjectUnauthorized do
+ include ExternalAuthorizationServiceHelpers
+ let(:user) { create(:user) }
+
+ before do
+ sign_in user
+ end
+
+ render_views
+
+ describe '#project_unauthorized_proc' do
+ controller(::Projects::ApplicationController) do
+ def show
+ head :ok
+ end
+ end
+
+ let(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'renders a 200 when the service allows access to the project' do
+ external_service_allow_access(user, project)
+
+ get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'renders a 403 when the service denies access to the project' do
+ external_service_deny_access(user, project)
+
+ get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param }
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(response.body).to match("External authorization denied access to this project")
+ end
+
+ it 'renders a 404 when the user cannot see the project at all' do
+ other_project = create(:project, :private)
+
+ get :show, params: { namespace_id: other_project.namespace.to_param, id: other_project.to_param }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb
index c8d99f79277..775b3ca40b2 100644
--- a/spec/controllers/dashboard/groups_controller_spec.rb
+++ b/spec/controllers/dashboard/groups_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Dashboard::GroupsController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
before do
@@ -11,33 +13,43 @@ describe Dashboard::GroupsController do
expect(described_class).to include(GroupTree)
end
- it 'only includes projects the user is a member of' do
- member_of_group = create(:group)
- member_of_group.add_developer(user)
- create(:group, :public)
+ describe '#index' do
+ it 'only includes projects the user is a member of' do
+ member_of_group = create(:group)
+ member_of_group.add_developer(user)
+ create(:group, :public)
- get :index
+ get :index
- expect(assigns(:groups)).to contain_exactly(member_of_group)
- end
+ expect(assigns(:groups)).to contain_exactly(member_of_group)
+ end
- context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do
- let!(:top_level_result) { create(:group, name: 'chef-top') }
- let!(:top_level_a) { create(:group, name: 'top-a') }
- let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) }
- let!(:other_group) { create(:group, name: 'other') }
+ context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do
+ let!(:top_level_result) { create(:group, name: 'chef-top') }
+ let!(:top_level_a) { create(:group, name: 'top-a') }
+ let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) }
+ let!(:other_group) { create(:group, name: 'other') }
- before do
- top_level_result.add_maintainer(user)
- top_level_a.add_maintainer(user)
+ before do
+ top_level_result.add_maintainer(user)
+ top_level_a.add_maintainer(user)
+ end
+
+ it 'renders only groups the user is a member of when searching hierarchy correctly' do
+ get :index, params: { filter: 'chef' }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ all_groups = [top_level_result, top_level_a, sub_level_result_a]
+ expect(assigns(:groups)).to contain_exactly(*all_groups)
+ end
end
- it 'renders only groups the user is a member of when searching hierarchy correctly' do
- get :index, params: { filter: 'chef' }, format: :json
+ it 'works when the external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ get :index
expect(response).to have_gitlab_http_status(200)
- all_groups = [top_level_result, top_level_a, sub_level_result_a]
- expect(assigns(:groups)).to contain_exactly(*all_groups)
end
end
end
diff --git a/spec/controllers/dashboard/labels_controller_spec.rb b/spec/controllers/dashboard/labels_controller_spec.rb
index a3bfb2f3a87..01de896f9f4 100644
--- a/spec/controllers/dashboard/labels_controller_spec.rb
+++ b/spec/controllers/dashboard/labels_controller_spec.rb
@@ -13,13 +13,17 @@ describe Dashboard::LabelsController do
describe "#index" do
let!(:unrelated_label) { create(:label, project: create(:project, :public)) }
+ subject { get :index, format: :json }
+
it 'returns global labels for projects the user has a relationship with' do
- get :index, format: :json
+ subject
expect(json_response).to be_kind_of(Array)
expect(json_response.size).to eq(1)
expect(json_response[0]["id"]).to be_nil
expect(json_response[0]["title"]).to eq(label.title)
end
+
+ it_behaves_like 'disabled when using an external authorization service'
end
end
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index 828de0e7ca5..1614739db05 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -81,5 +81,11 @@ describe Dashboard::MilestonesController do
expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>")
expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>")
end
+
+ context 'external authorization' do
+ subject { get :index }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
end
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index 649441f4917..c17cb49e460 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -1,7 +1,29 @@
require 'spec_helper'
describe Dashboard::ProjectsController do
- it_behaves_like 'authenticates sessionless user', :index, :atom
+ include ExternalAuthorizationServiceHelpers
+
+ describe '#index' do
+ context 'user not logged in' do
+ it_behaves_like 'authenticates sessionless user', :index, :atom
+ end
+
+ context 'user logged in' do
+ before do
+ sign_in create(:user)
+ end
+
+ context 'external authorization' do
+ it 'works when the external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ get :index
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+ end
context 'json requests' do
render_views
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index d88beaff0e1..abbf0b52306 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -105,6 +105,12 @@ describe Dashboard::TodosController do
end
end
end
+
+ context 'external authorization' do
+ subject { get :index }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe 'PATCH #restore' do
diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb
index 772d1d0c1dd..6ececa6f372 100644
--- a/spec/controllers/groups/avatars_controller_spec.rb
+++ b/spec/controllers/groups/avatars_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Groups::AvatarsController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
@@ -15,4 +17,12 @@ describe Groups::AvatarsController do
expect(@group.avatar.present?).to be_falsey
expect(@group).to be_valid
end
+
+ it 'works when external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ delete :destroy, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
end
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index 27ee37b3817..0ca5ce51750 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -82,6 +82,10 @@ describe Groups::BoardsController do
end
end
+ it_behaves_like 'disabled when using an external authorization service' do
+ subject { list_boards }
+ end
+
def list_boards(format: :html)
get :index, params: { group_id: group }, format: format
end
@@ -160,6 +164,10 @@ describe Groups::BoardsController do
end
end
+ it_behaves_like 'disabled when using an external authorization service' do
+ subject { read_board board: board }
+ end
+
def read_board(board:, format: :html)
get :show, params: {
group_id: group,
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index e1b97013408..4085c8f95a9 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Groups::ChildrenController do
+ include ExternalAuthorizationServiceHelpers
+
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
let!(:group_member) { create(:group_member, group: group, user: user) }
@@ -317,5 +319,15 @@ describe Groups::ChildrenController do
end
end
end
+
+ context 'external authorization' do
+ it 'works when external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ get :index, params: { group_id: group }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 3a801fabafc..96a58d6d87c 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -1,8 +1,11 @@
require 'spec_helper'
describe Groups::GroupMembersController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
+ let(:membership) { create(:group_member, group: group) }
describe 'GET index' do
it 'renders index with 200 status code' do
@@ -263,4 +266,87 @@ describe Groups::GroupMembersController do
end
end
end
+
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'is successful' do
+ get :index, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'POST #create' do
+ it 'is successful' do
+ post :create, params: { group_id: group, users: user, access_level: Gitlab::Access::GUEST }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'PUT #update' do
+ it 'is successful' do
+ put :update,
+ params: {
+ group_member: { access_level: Gitlab::Access::GUEST },
+ group_id: group,
+ id: membership
+ },
+ format: :js
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ it 'is successful' do
+ delete :destroy, params: { group_id: group, id: membership }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #destroy' do
+ it 'is successful' do
+ sign_in(create(:user))
+
+ post :request_access, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #approve_request_access' do
+ it 'is successful' do
+ access_request = create(:group_member, :access_request, group: group)
+ post :approve_access_request, params: { group_id: group, id: access_request }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'DELETE #leave' do
+ it 'is successful' do
+ group.add_owner(create(:user))
+
+ delete :leave, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #resend_invite' do
+ it 'is successful' do
+ post :resend_invite, params: { group_id: group, id: membership }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index fa664a29066..9af47114838 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -37,6 +37,12 @@ describe Groups::LabelsController do
expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title])
end
end
+
+ context 'external authorization' do
+ subject { get :index, params: { group_id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe 'POST #toggle_subscription' do
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 043cf28514b..d70946cbc8f 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -80,6 +80,12 @@ describe Groups::MilestonesController do
expect(response.content_type).to eq 'application/json'
end
end
+
+ context 'external authorization' do
+ subject { get :index, params: { group_id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe '#show' do
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index 3290ed8b088..b998f64ef72 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Groups::Settings::CiCdController do
+ include ExternalAuthorizationServiceHelpers
+
let(:group) { create(:group) }
let(:user) { create(:user) }
@@ -33,6 +35,19 @@ describe Groups::Settings::CiCdController do
expect(response).to have_gitlab_http_status(404)
end
end
+
+ context 'external authorization' do
+ before do
+ enable_external_authorization_service_check
+ group.add_owner(user)
+ end
+
+ it 'renders show with 200 status code' do
+ get :show, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
describe 'PUT #reset_registration_token' do
diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb
index 29ec3588316..40f05167350 100644
--- a/spec/controllers/groups/variables_controller_spec.rb
+++ b/spec/controllers/groups/variables_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Groups::VariablesController do
+ include ExternalAuthorizationServiceHelpers
+
let(:group) { create(:group) }
let(:user) { create(:user) }
@@ -34,4 +36,36 @@ describe Groups::VariablesController do
include_examples 'PATCH #update updates variables'
end
+
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe 'GET #show' do
+ let!(:variable) { create(:ci_group_variable, group: group) }
+
+ it 'is successful' do
+ get :show, params: { group_id: group }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'PATCH #update' do
+ let!(:variable) { create(:ci_group_variable, group: group) }
+ let(:owner) { group }
+
+ it 'is successful' do
+ patch :update,
+ params: {
+ group_id: group,
+ variables_attributes: [{ id: variable.id, key: 'hello' }]
+ },
+ format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 4a28a27da79..431627cf85a 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe GroupsController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:group) { create(:group, :public) }
@@ -665,4 +667,98 @@ describe GroupsController do
end
end
end
+
+ describe 'external authorization' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ context 'with external authorization service enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe 'GET #show' do
+ it 'is successful' do
+ get :show, params: { id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'does not allow other formats' do
+ get :show, params: { id: group.to_param }, format: :atom
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ describe 'GET #edit' do
+ it 'is successful' do
+ get :edit, params: { id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'GET #new' do
+ it 'is successful' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'GET #index' do
+ it 'is successful' do
+ get :index
+
+ # Redirects to the dashboard
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #create' do
+ it 'creates a group' do
+ expect do
+ post :create, params: { group: { name: 'a name', path: 'a-name' } }
+ end.to change { Group.count }.by(1)
+ end
+ end
+
+ describe 'PUT #update' do
+ it 'updates a group' do
+ expect do
+ put :update, params: { id: group.to_param, group: { name: 'world' } }
+ end.to change { group.reload.name }
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ it 'deletes the group' do
+ delete :destroy, params: { id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
+
+ describe 'GET #activity' do
+ subject { get :activity, params: { id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+
+ describe 'GET #issues' do
+ subject { get :issues, params: { id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+
+ describe 'GET #merge_requests' do
+ subject { get :merge_requests, params: { id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+ end
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 1eeded06459..b1203fd00b0 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -98,6 +98,10 @@ describe Projects::BoardsController do
end
end
+ it_behaves_like 'unauthorized when external service denies access' do
+ subject { list_boards }
+ end
+
def list_boards(format: :html)
get :index, params: {
namespace_id: project.namespace,
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index c34d7c13d57..bfa23af76d5 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -127,6 +127,17 @@ describe Projects::IssuesController do
expect(assigns(:issues).size).to eq(2)
end
end
+
+ context 'external authorization' do
+ before do
+ sign_in user
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'unauthorized when external service denies access' do
+ subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+ end
+ end
end
describe 'GET #new' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 017162519d8..a125e470522 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -238,11 +238,11 @@ describe Projects::MergeRequestsController do
assignee = create(:user)
project.add_developer(assignee)
- update_merge_request({ assignee_id: assignee.id }, format: :json)
+ update_merge_request({ assignee_ids: [assignee.id] }, format: :json)
+
body = JSON.parse(response.body)
- expect(body['assignee'].keys)
- .to match_array(%w(name username avatar_url id state web_url))
+ expect(body['assignees']).to all(include(*%w(name username avatar_url id state web_url)))
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index af437c5561b..1ce06bc877c 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1,6 +1,7 @@
require('spec_helper')
describe ProjectsController do
+ include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
let(:project) { create(:project) }
@@ -411,6 +412,37 @@ describe ProjectsController do
it_behaves_like 'updating a project'
end
+
+ context 'as maintainer' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it_behaves_like 'unauthorized when external service denies access' do
+ subject do
+ put :update,
+ params: {
+ namespace_id: project.namespace,
+ id: project,
+ project: { description: 'Hello world' }
+ }
+ project.reload
+ end
+
+ it 'updates when the service allows access' do
+ external_service_allow_access(user, project)
+
+ expect { subject }.to change(project, :description)
+ end
+
+ it 'does not update when the service rejects access' do
+ external_service_deny_access(user, project)
+
+ expect { subject }.not_to change(project, :description)
+ end
+ end
+ end
end
describe '#transfer' do
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 02a0cfe0272..752d6ae55cc 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe SearchController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
before do
@@ -76,4 +78,41 @@ describe SearchController do
expect(assigns[:search_objects].count).to eq(0)
end
end
+
+ context 'with external authorization service enabled' do
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:note) { create(:note_on_issue, project: project) }
+
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe 'GET #show' do
+ it 'renders a 403 when no project is given' do
+ get :show, params: { scope: 'notes', search: note.note }
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'renders a 200 when a project was set' do
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'GET #autocomplete' do
+ it 'renders a 403 when no project is given' do
+ get :autocomplete, params: { term: 'hello' }
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'renders a 200 when a project was set' do
+ get :autocomplete, params: { project_id: project.id, term: 'hello' }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 4f6a6881193..42d28c53d34 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -223,6 +223,12 @@ describe UsersController do
end
end
+ context 'external authorization' do
+ subject { get :calendar_activities, params: { username: user.username } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+
def create_push_event
push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
EventCreateService.new.push(project, public_user, push_data)
@@ -286,6 +292,12 @@ describe UsersController do
expect(JSON.parse(response.body)).to have_key('html')
end
end
+
+ context 'external authorization' do
+ subject { get :snippets, params: { username: user.username } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe 'GET #exists' do
diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
new file mode 100644
index 00000000000..4098dd02141
--- /dev/null
+++ b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'The group dashboard' do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { create(:user) }
+
+ before do
+ sign_in user
+ end
+
+ describe 'The top navigation' do
+ it 'has all the expected links' do
+ visit dashboard_groups_path
+
+ within('.navbar') do
+ expect(page).to have_button('Projects')
+ expect(page).to have_button('Groups')
+ expect(page).to have_link('Activity')
+ expect(page).to have_link('Milestones')
+ expect(page).to have_link('Snippets')
+ end
+ end
+
+ it 'hides some links when an external authorization service is enabled' do
+ enable_external_authorization_service_check
+ visit dashboard_groups_path
+
+ within('.navbar') do
+ expect(page).to have_button('Projects')
+ expect(page).to have_button('Groups')
+ expect(page).not_to have_link('Activity')
+ expect(page).not_to have_link('Milestones')
+ expect(page).to have_link('Snippets')
+ end
+ end
+ end
+end
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index fbc2e5cc3d3..50b71368e13 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -8,7 +8,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
before do
issue.assignees = [user]
- merge_request.update(assignee: user)
+ merge_request.update(assignees: [user])
sign_in(user)
end
@@ -33,7 +33,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
expect_counters('merge_requests', '1')
- merge_request.update(assignee: nil)
+ merge_request.update(assignees: [])
user.invalidate_cache_counts
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 4965770605a..0c6713f623c 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -48,14 +48,14 @@ describe 'Dashboard Merge Requests' do
let!(:assigned_merge_request) do
create(:merge_request,
- assignee: current_user,
+ assignees: [current_user],
source_project: project,
author: create(:user))
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
- source_branch: 'markdown', assignee: current_user,
+ source_branch: 'markdown', assignees: [current_user],
target_project: public_project, source_project: forked_project,
author: create(:user))
end
diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
new file mode 100644
index 00000000000..c05c3f4f3d6
--- /dev/null
+++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'The group page' do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
+ before do
+ sign_in user
+ group.add_owner(user)
+ end
+
+ def expect_all_sidebar_links
+ within('.nav-sidebar') do
+ expect(page).to have_link('Overview')
+ expect(page).to have_link('Details')
+ expect(page).to have_link('Activity')
+ expect(page).to have_link('Issues')
+ expect(page).to have_link('Merge Requests')
+ expect(page).to have_link('Members')
+ end
+ end
+
+ describe 'The sidebar' do
+ it 'has all the expected links' do
+ visit group_path(group)
+
+ expect_all_sidebar_links
+ end
+
+ it 'shows all project features when policy control is enabled' do
+ stub_application_setting(external_authorization_service_enabled: true)
+
+ visit group_path(group)
+
+ expect_all_sidebar_links
+ end
+
+ it 'hides some links when an external authorization service configured with an url' do
+ enable_external_authorization_service_check
+ visit group_path(group)
+
+ within('.nav-sidebar') do
+ expect(page).to have_link('Overview')
+ expect(page).to have_link('Details')
+ expect(page).not_to have_link('Activity')
+ expect(page).not_to have_link('Contribution Analytics')
+
+ expect(page).not_to have_link('Issues')
+ expect(page).not_to have_link('Merge Requests')
+ expect(page).to have_link('Members')
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 54a8016c157..e1bc4eca619 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -38,7 +38,7 @@ describe 'Group merge requests page' do
context 'when merge request assignee to user' do
before do
- issuable.update!(assignee: user)
+ issuable.update!(assignees: [user])
visit path
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 26c781350e5..6fa2ad8711f 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -30,8 +30,8 @@ describe 'New/edit issue', :js do
# the original method, resulting in infinite recursion when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
- original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
- allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
+ original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options)
+ allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index ea2bb1503bb..bcc11217389 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -68,15 +68,15 @@ describe "User creates a merge request", :js do
fill_in("Title", with: title)
end
- click_button("Assignee")
-
expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
+ find('.js-assignee-search').click
page.within(".dropdown-menu-user") do
expect(page).to have_content("Unassigned")
.and have_content(user.name)
.and have_content(project.users.first.name)
end
+ find('.js-assignee-search').click
click_button("Submit merge request")
diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb
index c169a68cd1c..c9dedab048a 100644
--- a/spec/features/merge_request/user_creates_mr_spec.rb
+++ b/spec/features/merge_request/user_creates_mr_spec.rb
@@ -1,11 +1,18 @@
require 'rails_helper'
describe 'Merge request > User creates MR' do
- it_behaves_like 'a creatable merge request'
+ include ProjectForksHelper
- context 'from a forked project' do
- include ProjectForksHelper
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+ context 'non-fork merge request' do
+ include_context 'merge request create context'
+ it_behaves_like 'a creatable merge request'
+ end
+
+ context 'from a forked project' do
let(:canonical_project) { create(:project, :public, :repository) }
let(:source_project) do
@@ -15,6 +22,7 @@ describe 'Merge request > User creates MR' do
end
context 'to canonical project' do
+ include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
@@ -25,6 +33,7 @@ describe 'Merge request > User creates MR' do
namespace: user.namespace)
end
+ include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
end
diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb
index 3152707136c..25979513ead 100644
--- a/spec/features/merge_request/user_edits_mr_spec.rb
+++ b/spec/features/merge_request/user_edits_mr_spec.rb
@@ -1,13 +1,21 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Merge request > User edits MR' do
include ProjectForksHelper
- it_behaves_like 'an editable merge request'
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ context 'non-fork merge request' do
+ include_context 'merge request edit context'
+ it_behaves_like 'an editable merge request'
+ end
context 'for a forked project' do
- it_behaves_like 'an editable merge request' do
- let(:source_project) { fork_project(target_project, nil, repository: true) }
- end
+ let(:source_project) { fork_project(target_project, nil, repository: true) }
+
+ include_context 'merge request edit context'
+ it_behaves_like 'an editable merge request'
end
end
diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
index d6c770c93f1..0cbf1bcae30 100644
--- a/spec/features/merge_requests/user_filters_by_assignees_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
@@ -7,7 +7,7 @@ describe 'Merge Requests > User filters by assignees', :js do
let(:user) { project.creator }
before do
- create(:merge_request, assignee: user, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
+ create(:merge_request, assignees: [user], title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2')
sign_in(user)
diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
index 1615899a047..4627931f26a 100644
--- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
@@ -10,7 +10,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
before do
sign_in(user)
- mr = create(:merge_request, title: 'Bugfix2', author: user, assignee: user, source_project: project, target_project: project, milestone: milestone)
+ mr = create(:merge_request, title: 'Bugfix2', author: user, assignees: [user], source_project: project, target_project: project, milestone: milestone)
mr.labels << wontfix
visit project_merge_requests_path(project)
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index c691011b9ca..bd91fae1453 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -12,7 +12,7 @@ describe 'Merge requests > User lists merge requests' do
title: 'fix',
source_project: project,
source_branch: 'fix',
- assignee: user,
+ assignees: [user],
milestone: create(:milestone, project: project, due_date: '2013-12-11'),
created_at: 1.minute.ago,
updated_at: 1.minute.ago)
@@ -20,7 +20,7 @@ describe 'Merge requests > User lists merge requests' do
title: 'markdown',
source_project: project,
source_branch: 'markdown',
- assignee: user,
+ assignees: [user],
milestone: create(:milestone, project: project, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index e535c7e5811..c2dd105324d 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -54,8 +54,7 @@ describe 'Merge requests > User mass updates', :js do
describe 'remove assignee' do
before do
- merge_request.assignee = user
- merge_request.save
+ merge_request.assignees = [user]
visit project_merge_requests_path(project)
end
diff --git a/spec/features/projects/classification_label_on_project_pages_spec.rb b/spec/features/projects/classification_label_on_project_pages_spec.rb
new file mode 100644
index 00000000000..92f8aa8eb8d
--- /dev/null
+++ b/spec/features/projects/classification_label_on_project_pages_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Classification label on project pages' do
+ let(:project) do
+ create(:project, external_authorization_classification_label: 'authorized label')
+ end
+ let(:user) { create(:user) }
+
+ before do
+ stub_application_setting(external_authorization_service_enabled: true)
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'shows the classification label on the project page' do
+ visit project_path(project)
+
+ expect(page).to have_content('authorized label')
+ end
+end
diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb
new file mode 100644
index 00000000000..2c41c61a660
--- /dev/null
+++ b/spec/features/projects/forks/fork_list_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'listing forks of a project' do
+ include ProjectForksHelper
+ include ExternalAuthorizationServiceHelpers
+
+ let(:source) { create(:project, :public, :repository) }
+ let!(:fork) { fork_project(source, nil, repository: true) }
+ let(:user) { create(:user) }
+
+ before do
+ source.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'shows the forked project in the list with commit as description' do
+ visit project_forks_path(source)
+
+ page.within('li.project-row') do
+ expect(page).to have_content(fork.full_name)
+ expect(page).to have_css('a.commit-row-message')
+ end
+ end
+
+ it 'does not show the commit message when an external authorization service is used' do
+ enable_external_authorization_service_check
+
+ visit project_forks_path(source)
+
+ page.within('li.project-row') do
+ expect(page).to have_content(fork.full_name)
+ expect(page).not_to have_css('a.commit-row-message')
+ end
+ end
+end
diff --git a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
new file mode 100644
index 00000000000..a8612d77a5e
--- /dev/null
+++ b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe 'viewing an issue with cross project references' do
+ include ExternalAuthorizationServiceHelpers
+ include Gitlab::Routing.url_helpers
+
+ let(:user) { create(:user) }
+ let(:other_project) do
+ create(:project, :public,
+ external_authorization_classification_label: 'other_label')
+ end
+ let(:other_issue) do
+ create(:issue, :closed,
+ title: 'I am in another project',
+ project: other_project)
+ end
+ let(:other_confidential_issue) do
+ create(:issue, :confidential, :closed,
+ title: 'I am in another project and confidential',
+ project: other_project)
+ end
+ let(:other_merge_request) do
+ create(:merge_request, :closed,
+ title: 'I am a merge request in another project',
+ source_project: other_project)
+ end
+ let(:description_referencing_other_issue) do
+ "Referencing: #{other_issue.to_reference(project)}, "\
+ "a confidential issue #{confidential_issue.to_reference}, "\
+ "a cross project confidential issue #{other_confidential_issue.to_reference(project)}, and "\
+ "a cross project merge request #{other_merge_request.to_reference(project)}"
+ end
+ let(:project) { create(:project) }
+ let(:issue) do
+ create(:issue,
+ project: project,
+ description: description_referencing_other_issue )
+ end
+ let(:confidential_issue) do
+ create(:issue, :confidential, :closed,
+ title: "I am in the same project and confidential",
+ project: project)
+ end
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'shows all information related to the cross project reference' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_issue.to_reference(project)} (#{other_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{other_issue.title}']")
+ end
+
+ it 'shows a link to the confidential issue in the same project' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']")
+ end
+
+ it 'does not show the link to a cross project confidential issue when the user does not have access' do
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})")
+ expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ it 'shows the link to a cross project confidential issue when the user has access' do
+ other_project.add_developer(user)
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ context 'when an external authorization service is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'only hits the external service for the project the user is viewing' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'default_label', any_args).at_least(1).and_return(true)
+ expect(::Gitlab::ExternalAuthorization)
+ .not_to receive(:access_allowed?).with(user, 'other_label', any_args)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows only the link to the cross project references' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_issue.to_reference(project)}")
+ expect(page).to have_link("#{other_merge_request.to_reference(project)}")
+ expect(page).not_to have_content("#{other_issue.to_reference(project)} (#{other_issue.state})")
+ expect(page).not_to have_xpath("//a[@title='#{other_issue.title}']")
+ expect(page).not_to have_content("#{other_merge_request.to_reference(project)} (#{other_merge_request.state})")
+ expect(page).not_to have_xpath("//a[@title='#{other_merge_request.title}']")
+ end
+
+ it 'does not link a cross project confidential issue if the user does not have access' do
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)}")
+ expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ it 'links a cross project confidential issue without exposing information when the user has access' do
+ other_project.add_developer(user)
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_confidential_issue.to_reference(project)}")
+ expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ it 'shows a link to the confidential issue in the same project' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']")
+ end
+ end
+end
diff --git a/spec/features/projects/settings/external_authorization_service_settings_spec.rb b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
new file mode 100644
index 00000000000..31b2892cf6f
--- /dev/null
+++ b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Projects > Settings > External Authorization Classification Label setting' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_empty_repo) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'shows the field to set a classification label' do
+ stub_application_setting(external_authorization_service_enabled: true)
+
+ visit edit_project_path(project)
+
+ expect(page).to have_selector('#project_external_authorization_classification_label')
+ end
+end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 444de26733f..1cc47cd6bd1 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -36,7 +36,7 @@ describe 'User uses header search field' do
end
context 'when clicking merge requests' do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me')
@@ -100,7 +100,7 @@ describe 'User uses header search field' do
end
context 'when clicking merge requests' do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.dropdown-menu').click_link('Merge requests assigned to me')
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 86379164cf0..351750c0179 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'User page' do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
context 'with public profile' do
@@ -86,4 +88,24 @@ describe 'User page' do
end
end
end
+
+ context 'most recent activity' do
+ it 'shows the most recent activity' do
+ visit(user_path(user))
+
+ expect(page).to have_content('Most Recent Activity')
+ end
+
+ context 'when external authorization is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'hides the most recent activity' do
+ visit(user_path(user))
+
+ expect(page).not_to have_content('Most Recent Activity')
+ end
+ end
+ end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index fe53fabe54c..6a47cd013f8 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -13,60 +13,32 @@ describe IssuesFinder do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
- context 'filtering by assignee ID' do
- let(:params) { { assignee_id: user.id } }
+ context 'assignee filtering' do
+ let(:issuables) { issues }
- it 'returns issues assigned to that user' do
- expect(issues).to contain_exactly(issue1, issue2)
- end
- end
-
- context 'filtering by assignee usernames' do
- set(:user3) { create(:user) }
- let(:params) { { assignee_username: [user2.username, user3.username] } }
-
- before do
- project2.add_developer(user3)
-
- issue3.assignees = [user2, user3]
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [issue1, issue2] }
end
- it 'returns issues assigned to those users' do
- expect(issues).to contain_exactly(issue3)
- end
- end
-
- context 'filtering by no assignee' do
- let(:params) { { assignee_id: 'None' } }
-
- it 'returns issues not assigned to any assignee' do
- expect(issues).to contain_exactly(issue4)
- end
-
- it 'returns issues not assigned to any assignee' do
- params[:assignee_id] = 0
-
- expect(issues).to contain_exactly(issue4)
- end
-
- it 'returns issues not assigned to any assignee' do
- params[:assignee_id] = 'none'
+ it_behaves_like 'assignee username filter' do
+ before do
+ project2.add_developer(user3)
+ issue3.assignees = [user2, user3]
+ end
- expect(issues).to contain_exactly(issue4)
+ set(:user3) { create(:user) }
+ let(:params) { { assignee_username: [user2.username, user3.username] } }
+ let(:expected_issuables) { [issue3] }
end
- end
-
- context 'filtering by any assignee' do
- let(:params) { { assignee_id: 'Any' } }
- it 'returns issues assigned to any assignee' do
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ it_behaves_like 'no assignee filter' do
+ set(:user3) { create(:user) }
+ let(:expected_issuables) { [issue4] }
end
- it 'returns issues assigned to any assignee' do
- params[:assignee_id] = 'any'
-
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [issue1, issue2, issue3] }
end
end
@@ -559,6 +531,13 @@ describe IssuesFinder do
expect(issues.count).to eq 0
end
end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:issue, project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
end
describe '#row_count', :request_store do
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 3f060ba0553..98b4933fef6 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -226,5 +226,12 @@ describe LabelsFinder do
expect(finder.execute).to eq [project_label_1]
end
end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:label, project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index f508b9bdb6f..117f4a03735 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -136,21 +136,50 @@ describe MergeRequestsFinder do
end
end
- context 'filtering by group milestone' do
- let(:group_milestone) { create(:milestone, group: group) }
+ context 'assignee filtering' do
+ let(:issuables) { described_class.new(user, params).execute }
- before do
- project2.update(namespace: group)
- merge_request2.update(milestone: group_milestone)
- merge_request3.update(milestone: group_milestone)
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [merge_request1, merge_request2] }
end
- it 'returns merge requests assigned to that group milestone' do
- params = { milestone_title: group_milestone.title }
+ it_behaves_like 'assignee username filter' do
+ before do
+ project2.add_developer(user3)
+ merge_request3.assignees = [user2, user3]
+ end
- merge_requests = described_class.new(user, params).execute
+ set(:user3) { create(:user) }
+ let(:params) { { assignee_username: [user2.username, user3.username] } }
+ let(:expected_issuables) { [merge_request3] }
+ end
- expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
+ it_behaves_like 'no assignee filter' do
+ set(:user3) { create(:user) }
+ let(:expected_issuables) { [merge_request4, merge_request5] }
+ end
+
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
+ end
+
+ context 'filtering by group milestone' do
+ let(:group_milestone) { create(:milestone, group: group) }
+
+ before do
+ project2.update(namespace: group)
+ merge_request2.update(milestone: group_milestone)
+ merge_request3.update(milestone: group_milestone)
+ end
+
+ it 'returns merge requests assigned to that group milestone' do
+ params = { milestone_title: group_milestone.title }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
+ end
end
end
@@ -253,6 +282,13 @@ describe MergeRequestsFinder do
expect(finder.row_count).to eq(1)
end
end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:merge_request, source_project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
end
context 'when projects require different access levels for merge requests' do
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 93287f3e9b8..d367f9015c7 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe SnippetsFinder do
+ include ExternalAuthorizationServiceHelpers
include Gitlab::Allowable
describe '#initialize' do
@@ -164,4 +165,35 @@ describe SnippetsFinder do
end
it_behaves_like 'snippet visibility'
+
+ context 'external authorization' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, :public, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:project_snippet, project: project) }
+ let(:project_params) { { project: project } }
+ end
+
+ it 'includes the result if the external service allows access' do
+ external_service_allow_access(user, project)
+
+ results = described_class.new(user, project: project).execute
+
+ expect(results).to contain_exactly(snippet)
+ end
+
+ it 'does not include any results if the external service denies access' do
+ external_service_deny_access(user, project)
+
+ results = described_class.new(user, project: project).execute
+
+ expect(results).to be_empty
+ end
+ end
end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index d4ed41d54f0..22318a9946a 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -47,6 +47,13 @@ describe TodosFinder do
end
end
end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:todo, project: project, user: user) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
end
describe '#sort' do
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 3006b482d41..88a600398b1 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -6,14 +6,14 @@
"source_branch_exists": { "type": "boolean" },
"merge_error": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
- "assignee_id": { "type": ["integer", "null"] },
"allow_collaboration": { "type": "boolean"},
"allow_maintainer_to_push": { "type": "boolean"},
- "assignee": {
- "oneOf": [
- { "type": "null" },
- { "$ref": "user.json" }
- ]
+ "assignees": {
+ "type": ["array"],
+ "items": {
+ "type": "object",
+ "$ref": "../public_api/v4/user/basic.json"
+ }
},
"milestone": {
"type": [ "object", "null" ]
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
index 918f2c4b47d..a423bf70b69 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -64,6 +64,11 @@
},
"additionalProperties": false
},
+ "assignees": {
+ "items": {
+ "$ref": "./merge_request.json"
+ }
+ },
"source_project_id": { "type": "integer" },
"target_project_id": { "type": "integer" },
"labels": {
diff --git a/spec/fixtures/passphrase_x509_certificate.crt b/spec/fixtures/passphrase_x509_certificate.crt
new file mode 100644
index 00000000000..6973163b79e
--- /dev/null
+++ b/spec/fixtures/passphrase_x509_certificate.crt
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEpTCCAo0CAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
+MB4XDTE4MDMyMzE0MDIwOFoXDTE5MDMyMzE0MDIwOFowHTEbMBkGA1UEAwwSZ2l0
+bGFiLXBhc3NwaHJhc2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
+zpsWHOewP/khfDsLUWxaRCinrBzVJm2C01bVahKVR3g/JD4vEH901Wod9Pvbh/9e
+PEfE+YZmgSUUopbL3JUheMnyW416F43HKE/fPW4+QeuIEceuhCXg20eOXmvnWWNM
+0hXZh4hq69rwvMPREC/LkZy/QkTDKhJNLNAqAQu2AJ3C7Yga8hFQYEhx1hpfGtwD
+z/Nf3efat9WN/d6yW9hfJ98NCmImTm5l9Pc0YPNWCAf96vsqsNHBrTkFy6CQwkhH
+K1ynVYuqnHYxSc4FPCT5SAleD9gR/xFBAHb7pPy4yGxMSEmiWaMjjZCVPsghj1jM
+Ej77MTDL3U9LeDfiILhvZ+EeQxqPiFwwG2eaIn3ZEs2Ujvw7Z2VpG9VMcPTnB4jK
+ot6qPM1YXnkGWQ6iT0DTPS3h7zg1xIJXI5N2sI6GXuKrXXwZ1wPqzFLKPv+xBjp8
+P6dih+EImfReFi9zIO1LqGMY+XmRcqodsb6jzsmBimJkqBtatJM7FuUUUN56wiaj
+q9+BWbm+ZdQ2lvqndMljjUjTh6pNERfGAJgkNuLn3X9hXVE0TSpmn0nOgaL5izP3
+7FWUt0PTyGgK2zq9SEhZmK2TKckLkKMk/ZBBBVM/nrnjs72IlbsqdcVoTnApytZr
+xVYTj1hV7QlAfaU3w/M534qXDiy8+HfX5ksWQMtSklECAwEAATANBgkqhkiG9w0B
+AQUFAAOCAgEAMMhzSRq9PqCpui74nwjhmn8Dm2ky7A+MmoXNtk70cS/HWrjzaacb
+B/rxsAUp7f0pj4QMMM0ETMFpbNs8+NPd2FRY0PfWE4yyDpvZO2Oj1HZKLHX72Gjn
+K5KB9DYlVsXhGPfuFWXpxGWF2Az9hDWnj58M3DOAps+6tHuAtudQUuwf5ENQZWwE
+ySpr7yoHm1ykgl0Tsb9ZHi9qLrWRRMNYXRT+gvwP1bba8j9jOtjO/xYiIskwMPLM
+W8SFmQxbg0Cvi8Q89PB6zoTNOhPQyoyeSlw9meeZJHAMK2zxeglEm8C4EQ+I9Y6/
+yylM5/Sc55TjWAvRFgbsq+OozgMvffk/Q2fzcGF44J9DEQ7nrhmJxJ+X4enLknR5
+Hw4+WhdYA+bwjx3YZBNTh9/YMgNPYwQhf5gtcZGTd6X4j6qZfJ6CXBmhkC1Cbfyl
+yM7B7i4JAqPWMeDP50pXCgyKlwgw1JuFW+xkbkYQAj7wtggQ6z1Vjb5W8R8kYn9q
+LXClVtThEeSV5KkVwNX21aFcUs8qeQ+zsgKqpEyM5oILQQ1gDSxLTtrr2KuN+WJN
+wM0acwD45X7gA/aZYpCGkIgHIBq0zIDP1s6IqeebFJjW8lWofhRxOEWomWdRweJG
+N7qQ1WCTQxAPGAkDI8QPjaspvnAhFKmpBG/mR5IXLFKDbttu7WNdYDo=
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/passphrase_x509_certificate_pk.key b/spec/fixtures/passphrase_x509_certificate_pk.key
new file mode 100644
index 00000000000..f9760dfe70e
--- /dev/null
+++ b/spec/fixtures/passphrase_x509_certificate_pk.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C
+
+EuZQOfgaO6LVCNytTHNJmbiq1rbum9xg6ohfBTVt7Cw4+8yLezWva/3sJQtnEk2P
+M2yEQYWIiCX+clPkRiRL8WLjRfLTNcYS6QxxuJdpOrowPrBYr4Aig8jBUUBI4VQf
+w1ZEUQd0mxQGnyzkKpsudFOntCtZbvbrBsIAQUNLcrKEFk3XW/BqE1Q/ja6WfWqX
+b6EKg6DoXi92V90O6sLDfpmTKZq3ThvVDFuWeJ2K/GVp2cs+MkBIBJ8XX+NT1nWg
+g+Ok+yaSI/N9ILX4XDgXunJGwcooI8PhHSjkDWRusi8vbo7RFqIKiSF+h6tIwktF
+Uss3JESKgXZCQ7upCnHSzK/aWFtwHtXxqOi7esqEZd+1sB0LY+XMnbaxweCMx2Kj
+czktKYvoXUs69Whln+yyXULtl5XhJ8lbvlbIG2FbZ9y+/hHOyBqZyeUyCnXDzv8/
+0U0iZwreP3XPVMsy578pIdcdL27q+r05j4yjrJfbX3T9xp2u3F9uVubCa4euEBwV
+yrFdsxJLKON8pFeDS49m5gHNsHmeZ0sUeTPZVGNXdabVetkOA0eAAGK4zAoqG79L
+hEN7cDenz+E4XHp8gMzwwMiVyU4FuAb6SXkfSodctmSTWVbzNBja0FBek3UXy+pn
+9qq7cIpe7NY5gzcbyoy9lSkyYVkAm8j6BIYtY1ZUAmtCklC2ADWARTjd7dI7aEbO
+QbXxNIq2+O/zMOXfougSPoDP8SLyLuE1p6SwfWV7Dwf119hn+mjWlGzAZDxxHhsR
+yYUQCUe0NIKzuUp3WYIx8xIb7/WFwit/JaFaxurjBnhkkEviBn+TgXiuFBO3tv/d
+URpZ39rH0mrDsR61pCiIcoNVkQkynHcAFPd5VtaeSJPvZP280uOCPPS31cr6/0LB
+1JX3lZoWWCuA+JQjxtZDaDTcvEUbfOQ2rexQQo4uylNkBF9F5WOdQBkKG/AfqBq8
+S/TdubYzvpcKhFAlXsI67JdbxGlU4HCsxOLwWzSUYclN4W3l7s7KZ5zxt+MU03Uf
+vara9uuZHiKUjZohjXeqcXTc+UyC8VH1dF19M3Cj9RNrwl2xEDUMtIiALBjbGp1E
+pu2nPj9NhWf9Vw5MtSszutesxXba2nPmvvGvvZ7N3h/k4NsKL7JdENF7XqkI0D2K
+jpO1t6d3cazS1VpMWLZS45kWaM3Y07tVR3V+4Iv9Vo1e9H2u/Z5U4YeJ44sgMsct
+dBOAhHdUAI5+P+ocLXiCKo+EcS0cKvz+CC4ux0vvcF3JrTqZJN1U/JxRka2EyJ1B
+2Xtu3DF36XpBJcs+MJHjJ+kUn6DHYoYxZa+bB8LX6+FQ+G7ue+Dx/RsGlP7if1nq
+DAaM6kZg7/FbFzOZyl5xhwAJMxfgNNU7nSbk9lrvQ4mdwgFjvgGu3jlER4+TcleE
+4svXInxp1zK6ES44tI9fXkhPaFkafxAL7eUSyjjEwMC06h+FtqK3mmoKLo5NrGJE
+zVl69r2WdoSQEylVN1Kbp+U4YbfncInLJqBq2q5w9ASL/8Rhe8b52q6PuVX/bjoz
+0pkSu+At4jVbAhRpER5NGlzG884IaqqvBvMYR5zFJeRroIijyUyH0KslK37/sXRk
+ty0yKrkm31De9gDa3+XlgAVDAgbEQmGVwVVcV0IYYJbjIf36lUdGh4+3krwxolr/
+vZct5Z7QxfJlBtdOstjz5U9o05yOhjoNrPZJXuKMmWOQjSwr7rRSdqmAABF9IrBf
+Pa/ChF1y5j3gJESAFMyiea3kvLq1EbZRaKoybsQE2ctBQ8EQjzUz+OOxVO6GJ4W9
+XHyfcviFrpsVcJEpXQlEtGtKdfKLp48cytob1Fu1JOYPDCrafUQINCZP4H3Nt892
+zZiTmdwux7pbgf4KbONImN5XkpvdCGjQHSkYMmm5ETRK8s7Fmvt2aBPtlyXxJDOq
+iJUqwDV5HZXOnQVE/v/yESKgo2Cb8BWqPZ4/8Ubgu/OADYyv/dtjQel8QQ2FMhO4
+2tnwWbBBJk8VpR/vjFHkGSnj+JJfW/vUVQ+06D3wHYhNp7mh4M+37AngwzGCp7k+
+9aFwb2FBGghArB03E4lIO/959T0cX95WZ6tZtLLEsf3+ug7PPOSswCqsoPsXzFJH
+MgXVGKFXccNSsWol7VvrX/uja7LC1OE+pZNXxCRzSs4aljJBpvQ6Mty0lk2yBC0R
+MdujMoZH9PG9U6stwFd+P17tlGrQdRD3H2uimn82Ck+j2l0z0pzN0JB2WBYEyK0O
+1MC36wLICWjgIPLPOxDEEBeZPbc24DCcYfs/F/hSCHv/XTJzVVILCX11ShGPSXlI
+FL9qyq6jTNh/pVz6NiN/WhUPBFfOSzLRDyU0MRsSHM8b/HPpf3NOI3Ywmmj65c2k
+2kle1F2M5ZTL+XvLS61qLJ/8AgXWvDHP3xWuKGG/pM40CRTUkRW6NAokMr2/pEFw
+IHTE2+84dOKnUIEczzMY3aqzNmYDCmhOY0jD/Ieb4hy9tN+1lbQ/msYMIJ1w7CFR
+38yB/UbDD90NcuDhjrMbzVUv1At2rW7GM9lSbxGOlYDmtMNEL63md1pQ724v4gSE
+mzoFcMkqdh+hjFvv11o4H32lF3mPYcXuL+po76tqxGOiUrLKe/ZqkT5XAclYV/7H
+k3Me++PCh4ZqXBRPvR8Xr90NETtiFCkBQXLdhNWXrRe2v0EbSX+cYAWk68FQKCHa
+HKTz9T7wAvB6QWBXFhH9iCP8rnQLCEhLEhdrt+4v2KFkIVzBgOlMoHsZsMp0sBeq
+c5ZVbJdiKik3P/8ZQTn4jmOnQXCEyWx+LU4acks8Aho4lqq9yKq2DZpwbIRED47E
+r7R/NUevhqqzEHZ2SGD6EDqRN+bHJEi64vq0ryaEielusYXZqlnFXDHJcfLCmR5X
+3bj5pCwQF4ScTukrGQB/c4henG4vlF4CaD0CIIK3W6tH+AoDohYJts6YK49LGxmK
+yXiyKNak8zHYBBoRvd2avRHyGuR5yC9KrN8cbC/kZqMDvAyM65pIK+U7exJwYJhv
+ezCcbiH3bK3anpiRpdeNOot2ba/Y+/ks+DRC+xs4QDIhrmSEBCsLv1JbcWjtHSaG
+lm+1DSVduUk/kN+fBnlfif+TQV9AP3/wb8ekk8jjKXsL7H1tJKHsLLIIvrgrpxjw
+-----END RSA PRIVATE KEY-----
diff --git a/spec/fixtures/x509_certificate.crt b/spec/fixtures/x509_certificate.crt
new file mode 100644
index 00000000000..8a84890b928
--- /dev/null
+++ b/spec/fixtures/x509_certificate.crt
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
+MB4XDTE4MDMxOTE1MjYzMloXDTE5MDMxOTE1MjYzMlowFDESMBAGA1UEAwwJbG9j
+YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+tcM7iphsLlR
+ccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzAn/eVU4jyVWkaBym6MHa8CiDOro9H
+OXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/2FAgFWzrB2HnYSShiN8tBeeDI5cJ
+ii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UTK37k2kbDQZ41rv1ng2w0AUZt0LRA
+NWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ
++1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNkI+cyv0Gle6tk+CkOfE1m0CvNWlNg
+b8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5xMXpdUCsh22CZZHe/4SeFE64amkf
+1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q/nLdY8haMC6KOtpbAWvKX/Jqq0z1
+nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVrVef0pb2mfdtzjzUrYCP0PtnQExPB
+rocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8RSvAoEUs9VbPiUfN7WAyU1K1rTYH
+KV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRlq07Q5LDz33h9KXw1LZT8MWRinVJf
+RePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA
+Skp0tbvVsg3RG2pX0GP25j0ix+f78zG0+BJ6LiKGMoCIBtGKitfUjBg83ru/ILpa
+fpgrQpNQVUnGQ9tmpnqV605ZBBRUC1CRDsvUnyN6p7+yQAq6Fl+2ZKONHpPk+Bl4
+CIewgdkHjTwTpvIM/1DFVCz4R1FxNjY3uqOVcNDczMYEk2Pn2GZNNN35hUHHxWh4
+89ZvI+XKuRFZq3cDPA60PySeJJpCRScWGgnkdEX1gTtWH3WUlq9llxIvRexyNyzZ
+Yqvcfx5UT75/Pp+JPh9lpUCcKLHeUiadjkiLxu3IcrYa4gYx4lA8jgm7adNEahd0
+oMAHoO9DU6XMo7o6tnQH3xQv9RAbQanjuyJR9N7mwmc59bQ6mW+pxCk843GwT73F
+slseJ1nE1fQQQD7mn/KGjmeWtxY2ElUjTay9ff9/AgJeQYRW+oH0cSdo8WCpc2+G
++LZtLWfBgFLHseRlmarSe2pP8KmbaTd3q7Bu0GekVQOxYcNX59Pj4muQZDVLh8aX
+mSQ+Ifts/ljT649MISHn2AZMR4+BUx63tFcatQhbAGGH5LeFdbaGcaVdsUVyZ9a2
+HBmFWNsgEPtcC+WmNzCXbv7jQsLAJXufKG5MnurJgNf/n5uKCmpGsEJDT/KF1k/3
+x9YnqM7zTyV6un+LS3HjEJvwQmqPWe+vFAeXWGCoWxE=
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/x509_certificate_pk.key b/spec/fixtures/x509_certificate_pk.key
new file mode 100644
index 00000000000..c02a3cf6189
--- /dev/null
+++ b/spec/fixtures/x509_certificate_pk.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKAIBAAKCAgEA+tcM7iphsLlRccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzA
+n/eVU4jyVWkaBym6MHa8CiDOro9HOXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/
+2FAgFWzrB2HnYSShiN8tBeeDI5cJii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UT
+K37k2kbDQZ41rv1ng2w0AUZt0LRANWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK
+88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ+1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNk
+I+cyv0Gle6tk+CkOfE1m0CvNWlNgb8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5
+xMXpdUCsh22CZZHe/4SeFE64amkf1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q
+/nLdY8haMC6KOtpbAWvKX/Jqq0z1nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVr
+Vef0pb2mfdtzjzUrYCP0PtnQExPBrocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8
+RSvAoEUs9VbPiUfN7WAyU1K1rTYHKV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRl
+q07Q5LDz33h9KXw1LZT8MWRinVJfRePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEA
+AQKCAgBf1urJ1Meeji/gGETVx9qBWLbDjn9QTayZSyyEd78155tDShIPDLmxQRHW
+MGIReo/5FGSkOgS+DWBZRZ77oGOGrtuMnjkheXhDr8dZvw5b1PBv5ntqWrLnfMYP
+/Ag7xZMyiJLbPqmMX5j1gsFt8zPzUoVMnnl9DYryV0Edrs/utHgfJCM+6yzleUQB
+PkGkqo1yWVVFZ3Nt2nDt9dNsdlC594+dYQ1m2JuArNvYNiw3dpHT98GnhRc1aLh4
+U+q22FiFn3BKGQat43JdlaLa6KO5f8MIQRYWuI8tss2DGPlhRv9AnUcVsLBjAuIH
+bmUVrBosxCYUQ6giatjd2sZPfdC+VIDCbIWRthxkXJ9I/Ap8R98xx/7qIcPFc+XA
+hcK1xOM7zIq2xgAOFeeh8O8Wq9cH8NmUhMCgzIE0WT32Zo0JAW6l0kZc82Y/Yofz
+U+TJKo0NOFZe687HOhanOHbbQSG29XOqxMYTABZ7Ixf+4RZPD5+yQgZWP1BhLluy
+PxZhsLl67xvbfB2i9VVorMN7PbFx5hbni3C7/p63Z0rG5q4/uJBbX3Uuh6KdhIo+
+Zh9UC6u29adIthdxz+ZV5wBccTOgaeHB9wRL9Hbp6ZxyqesQB4RTsFtPNXxZ7K43
+fmJgHZvHhF5gSbeB8JAeBf0cy3pytJM49ZxplifeGVzUJP2gAQKCAQEA/1T9quz5
+sOD03FxV//oRWD1kqfunq3v56sIBG4ZMVZKUqc6wLjTmeklLYKq85AWX8gnCHi0g
+nmG/xDh/rt1/IngMWP98WVuD67hFbrj87g7A7YGIiwZ2gi6hqhqmALN+5JjCSTPp
+XOiPvNnXP0XM4gIHBXV8diHq5rF9NsSh4vx3OExr8KQqVzWoDcnnWNfnDlrFB8cq
+ViII+UqdovXp59hAVOsc+pYAe+8JeQDX17H3U/NMkUw4gU2aWUCvUVjxi9oBG/CW
+ncIdYuW8zne4qXbX7YLC0QUUIDVOWzhLauAUBduTqRTldJo0KAxu887tf+uStXs8
+RACLGIaBQw7BXQKCAQEA+38NFnpflKquU92xRtmqWAVaW7rm865ZO6EIaS4JII/N
+/Ebu1YZrAhT0ruGJQaolYj8w79BEZRF2CYDPZxKFv/ye0O7rWCAGtCdWQ0BXcrIU
+7SdlsdfTNXO1R3WbwCyVxyjg6YF7FjbTaaOAoTiosTjDs2ZOgkbdh/sMeWkSN5HB
+aQz4c8rqq0kkYucLqp4nWYSWSJn88bL8ctwEwW77MheJiSpo1ohNRP3ExHnbCbYw
+RIj7ATSz74ebpd9NMauB5clvMMh4jRG0EQyt7KCoOyfPRFc3fddvTr03LlgFfX/n
+qoxd2nejgAS3NnG1XMxdcUa7cPannt46Sef1uZo3gQKCAQB454zquCYQDKXGBu8u
+NAKsjv2wxBqESENyV4VgvDo/NxawRdAFQUV12GkaEB87ti5aDSbfVS0h8lV1G+/S
+JM5DyybFqcz/Hyebofk20d/q9g+DJ5g5hMjvIhepTc8Xe+d1ZaRyN2Oke/c8TMbx
+DiNTTfR3MEfMRIlPzfHl0jx6GGR3wzBFleb6vsyiIt4qoqmlkXPFGBlDCgDH0v5M
+ITgucacczuw8+HSoOut4Yd7TI1FjbkzubHJBQDb7VnbuBTjzqTpnOYiIkVeK8hBy
+kBxgGodqz0Vi5o2+Jp/A8Co+JHc2wt/r65ovmali4WhUiMLLlQg2aXGDHeK/rUle
+MIl9AoIBAQCPKCYSCnyHypRK5uG3W8VsLzfdCUnXogHnQGXiQTMu1szA8ruWzdnx
+qG4TcgxIVYrMHv5DNAEKquLOzATDPjbmLu1ULvvGAQzv1Yhz5ZchkZ7507g+gIUY
+YxHoaFjNDlP/txQ3tt2SqoizFD/vBap4nsA/SVgdLiuB8PSL07Rr70rx+lEe0H2+
+HHda2Pu6FiZ9/Uvybb0e8+xhkT4fwYW5YM6IRpzAqXuabv1nfZmiMJPPH04JxK88
+BKwjwjVVtbPOUlg5o5ODcXVXUylZjaXVbna8Bw1uU4hngKt9dNtDMeB0I0x1RC7M
+e2Ky2g0LksUJ6uJdjfmiJAt38FLeYJuBAoIBAC2oqaqr86Dug5v8xHpgFoC5u7z7
+BRhaiHpVrUr+wnaNJEXfAEmyKf4xF5xDJqldnYG3c9ETG/7bLcg1dcrMPzXx94Si
+MI3ykwiPeI/sVWYmUlq4U8zCIC7MY6sWzWt3oCBNoCN/EeYx9e7+eLNBB+fADAXq
+v9RMGlUIy7beX0uac8Bs771dsxIb/RrYw58wz+jrwGlzuDmcPWiu+ARu7hnBqCAV
+AITlCV/tsEk7u08oBuv47+rVGCh1Qb19pNswyTtTZARAGErJO0Q+39BNuu0M2TIn
+G3M8eNmGHC+mNsZTVgKRuyk9Ye0s4Bo0KcqSndiPFGHjcrF7/t+RqEOXr/E=
+-----END RSA PRIVATE KEY-----
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index 57b16b12cb0..47fee5d2b21 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -132,9 +132,94 @@ describe('Assignee component', () => {
-1,
);
});
+
+ it('has correct "cannot merge" tooltip when user cannot merge', () => {
+ const user = Object.assign({}, UsersMock.user, { can_merge: false });
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users: [user],
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge');
+ });
});
describe('Two or more assignees/users', () => {
+ it('has correct "cannot merge" tooltip when one user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = true;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge');
+ });
+
+ it('has correct "cannot merge" tooltip when no user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge');
+ });
+
+ it('has correct "cannot merge" tooltip when more than one user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge');
+ });
+
+ it('has no "cannot merge" tooltip when every user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ users[0].can_merge = true;
+ users[1].can_merge = true;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual(null);
+ });
+
it('displays two assignee icons when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
component = new AssigneeComponent({
diff --git a/spec/lib/gitlab/external_authorization/access_spec.rb b/spec/lib/gitlab/external_authorization/access_spec.rb
new file mode 100644
index 00000000000..5dc2521b310
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/access_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
+ subject(:access) { described_class.new(build(:user), 'dummy_label') }
+
+ describe '#loaded?' do
+ it 'is `true` when it was loaded recently' do
+ Timecop.freeze do
+ allow(access).to receive(:loaded_at).and_return(5.minutes.ago)
+
+ expect(access).to be_loaded
+ end
+ end
+
+ it 'is `false` when there is no loading time' do
+ expect(access).not_to be_loaded
+ end
+
+ it 'is `false` when there the result was loaded a long time ago' do
+ Timecop.freeze do
+ allow(access).to receive(:loaded_at).and_return(2.weeks.ago)
+
+ expect(access).not_to be_loaded
+ end
+ end
+ end
+
+ describe 'load!' do
+ let(:fake_client) { double('ExternalAuthorization::Client') }
+ let(:fake_response) do
+ double(
+ 'Response',
+ 'successful?' => true,
+ 'valid?' => true,
+ 'reason' => nil
+ )
+ end
+
+ before do
+ allow(access).to receive(:load_from_cache)
+ allow(fake_client).to receive(:request_access).and_return(fake_response)
+ allow(Gitlab::ExternalAuthorization::Client).to receive(:new) { fake_client }
+ end
+
+ context 'when loading from the webservice' do
+ it 'loads from the webservice it the cache was empty' do
+ expect(access).to receive(:load_from_cache)
+ expect(access).to receive(:load_from_service).and_call_original
+
+ access.load!
+
+ expect(access).to be_loaded
+ end
+
+ it 'assigns the accessibility, reason and loaded_at' do
+ allow(fake_response).to receive(:successful?).and_return(false)
+ allow(fake_response).to receive(:reason).and_return('Inaccessible label')
+
+ access.load!
+
+ expect(access.reason).to eq('Inaccessible label')
+ expect(access).not_to have_access
+ expect(access.loaded_at).not_to be_nil
+ end
+
+ it 'returns itself' do
+ expect(access.load!).to eq(access)
+ end
+
+ it 'stores the result in redis' do
+ Timecop.freeze do
+ fake_cache = double
+ expect(fake_cache).to receive(:store).with(true, nil, Time.now)
+ expect(access).to receive(:cache).and_return(fake_cache)
+
+ access.load!
+ end
+ end
+
+ context 'when the request fails' do
+ before do
+ allow(fake_client).to receive(:request_access) do
+ raise ::Gitlab::ExternalAuthorization::RequestFailed.new('Service unavailable')
+ end
+ end
+
+ it 'is loaded' do
+ access.load!
+
+ expect(access).to be_loaded
+ end
+
+ it 'assigns the correct accessibility, reason and loaded_at' do
+ access.load!
+
+ expect(access.reason).to eq('Service unavailable')
+ expect(access).not_to have_access
+ expect(access.loaded_at).not_to be_nil
+ end
+
+ it 'does not store the result in redis' do
+ fake_cache = double
+ expect(fake_cache).not_to receive(:store)
+ allow(access).to receive(:cache).and_return(fake_cache)
+
+ access.load!
+ end
+ end
+ end
+
+ context 'When loading from cache' do
+ let(:fake_cache) { double('ExternalAuthorization::Cache') }
+
+ before do
+ allow(access).to receive(:cache).and_return(fake_cache)
+ end
+
+ it 'does not load from the webservice' do
+ Timecop.freeze do
+ expect(fake_cache).to receive(:load).and_return([true, nil, Time.now])
+
+ expect(access).to receive(:load_from_cache).and_call_original
+ expect(access).not_to receive(:load_from_service)
+
+ access.load!
+ end
+ end
+
+ it 'loads from the webservice when the cached result was too old' do
+ Timecop.freeze do
+ expect(fake_cache).to receive(:load).and_return([true, nil, 2.days.ago])
+
+ expect(access).to receive(:load_from_cache).and_call_original
+ expect(access).to receive(:load_from_service).and_call_original
+ allow(fake_cache).to receive(:store)
+
+ access.load!
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb
new file mode 100644
index 00000000000..58e7d626707
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/cache_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do
+ let(:user) { build_stubbed(:user) }
+ let(:cache_key) { "external_authorization:user-#{user.id}:label-dummy_label" }
+
+ subject(:cache) { described_class.new(user, 'dummy_label') }
+
+ def read_from_redis(key)
+ Gitlab::Redis::Cache.with do |redis|
+ redis.hget(cache_key, key)
+ end
+ end
+
+ def set_in_redis(key, value)
+ Gitlab::Redis::Cache.with do |redis|
+ redis.hmset(cache_key, key, value)
+ end
+ end
+
+ describe '#load' do
+ it 'reads stored info from redis' do
+ Timecop.freeze do
+ set_in_redis(:access, false)
+ set_in_redis(:reason, 'Access denied for now')
+ set_in_redis(:refreshed_at, Time.now)
+
+ access, reason, refreshed_at = cache.load
+
+ expect(access).to eq(false)
+ expect(reason).to eq('Access denied for now')
+ expect(refreshed_at).to be_within(1.second).of(Time.now)
+ end
+ end
+ end
+
+ describe '#store' do
+ it 'sets the values in redis' do
+ Timecop.freeze do
+ cache.store(true, 'the reason', Time.now)
+
+ expect(read_from_redis(:access)).to eq('true')
+ expect(read_from_redis(:reason)).to eq('the reason')
+ expect(read_from_redis(:refreshed_at)).to eq(Time.now.to_s)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb
new file mode 100644
index 00000000000..fa18c1e56e8
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/client_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Client do
+ let(:user) { build(:user, email: 'dummy_user@example.com') }
+ let(:dummy_url) { 'https://dummy.net/' }
+ subject(:client) { described_class.new(user, 'dummy_label') }
+
+ before do
+ stub_application_setting(external_authorization_service_url: dummy_url)
+ end
+
+ describe '#request_access' do
+ it 'performs requests to the configured endpoint' do
+ expect(Excon).to receive(:post).with(dummy_url, any_args)
+
+ client.request_access
+ end
+
+ it 'adds the correct params for the user to the body of the request' do
+ expected_body = {
+ user_identifier: 'dummy_user@example.com',
+ project_classification_label: 'dummy_label'
+ }.to_json
+ expect(Excon).to receive(:post)
+ .with(dummy_url, hash_including(body: expected_body))
+
+ client.request_access
+ end
+
+ it 'respects the the timeout' do
+ stub_application_setting(
+ external_authorization_service_timeout: 3
+ )
+
+ expect(Excon).to receive(:post).with(dummy_url,
+ hash_including(
+ connect_timeout: 3,
+ read_timeout: 3,
+ write_timeout: 3
+ ))
+
+ client.request_access
+ end
+
+ it 'adds the mutual tls params when they are present' do
+ stub_application_setting(
+ external_auth_client_cert: 'the certificate data',
+ external_auth_client_key: 'the key data',
+ external_auth_client_key_pass: 'open sesame'
+ )
+ expected_params = {
+ client_cert_data: 'the certificate data',
+ client_key_data: 'the key data',
+ client_key_pass: 'open sesame'
+ }
+
+ expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params))
+
+ client.request_access
+ end
+
+ it 'returns an expected response' do
+ expect(Excon).to receive(:post)
+
+ expect(client.request_access)
+ .to be_kind_of(::Gitlab::ExternalAuthorization::Response)
+ end
+
+ it 'wraps exceptions if the request fails' do
+ expect(Excon).to receive(:post) { raise Excon::Error.new('the request broke') }
+
+ expect { client.request_access }
+ .to raise_error(::Gitlab::ExternalAuthorization::RequestFailed)
+ end
+
+ describe 'for ldap users' do
+ let(:user) do
+ create(:omniauth_user,
+ email: 'dummy_user@example.com',
+ extern_uid: 'external id',
+ provider: 'ldapprovider')
+ end
+
+ it 'includes the ldap dn for ldap users' do
+ expected_body = {
+ user_identifier: 'dummy_user@example.com',
+ project_classification_label: 'dummy_label',
+ user_ldap_dn: 'external id'
+ }.to_json
+ expect(Excon).to receive(:post)
+ .with(dummy_url, hash_including(body: expected_body))
+
+ client.request_access
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/logger_spec.rb b/spec/lib/gitlab/external_authorization/logger_spec.rb
new file mode 100644
index 00000000000..81f1b2390e6
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/logger_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Logger do
+ let(:request_time) { Time.parse('2018-03-26 20:22:15') }
+
+ def fake_access(has_access, user, load_type = :request)
+ access = double('access')
+ allow(access).to receive_messages(user: user,
+ has_access?: has_access,
+ loaded_at: request_time,
+ label: 'dummy_label',
+ load_type: load_type)
+
+ access
+ end
+
+ describe '.log_access' do
+ it 'logs a nice message for an access request' do
+ expected_message = "GRANTED admin@example.com access to 'dummy_label' (the/project/path)"
+ fake_access = fake_access(true, build(:user, email: 'admin@example.com'))
+
+ expect(described_class).to receive(:info).with(expected_message)
+
+ described_class.log_access(fake_access, 'the/project/path')
+ end
+
+ it 'does not trip without a project path' do
+ expected_message = "DENIED admin@example.com access to 'dummy_label'"
+ fake_access = fake_access(false, build(:user, email: 'admin@example.com'))
+
+ expect(described_class).to receive(:info).with(expected_message)
+
+ described_class.log_access(fake_access, nil)
+ end
+
+ it 'adds the load time for cached accesses' do
+ expected_message = "DENIED admin@example.com access to 'dummy_label' - cache #{request_time}"
+ fake_access = fake_access(false, build(:user, email: 'admin@example.com'), :cache)
+
+ expect(described_class).to receive(:info).with(expected_message)
+
+ described_class.log_access(fake_access, nil)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/response_spec.rb b/spec/lib/gitlab/external_authorization/response_spec.rb
new file mode 100644
index 00000000000..43211043eca
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/response_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Response do
+ let(:excon_response) { double }
+ subject(:response) { described_class.new(excon_response) }
+
+ describe '#valid?' do
+ it 'is valid for 200, 401, and 403 responses' do
+ [200, 401, 403].each do |status|
+ allow(excon_response).to receive(:status).and_return(status)
+
+ expect(response).to be_valid
+ end
+ end
+
+ it "is invalid for other statuses" do
+ expect(excon_response).to receive(:status).and_return(500)
+
+ expect(response).not_to be_valid
+ end
+ end
+
+ describe '#reason' do
+ it 'returns a reason if it was included in the response body' do
+ expect(excon_response).to receive(:body).and_return({ reason: 'Not authorized' }.to_json)
+
+ expect(response.reason).to eq('Not authorized')
+ end
+
+ it 'returns nil when there was no body' do
+ expect(excon_response).to receive(:body).and_return('')
+
+ expect(response.reason).to eq(nil)
+ end
+ end
+
+ describe '#successful?' do
+ it 'is `true` if the status is 200' do
+ allow(excon_response).to receive(:status).and_return(200)
+
+ expect(response).to be_successful
+ end
+
+ it 'is `false` if the status is 401 or 403' do
+ [401, 403].each do |status|
+ allow(excon_response).to receive(:status).and_return(status)
+
+ expect(response).not_to be_successful
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization_spec.rb b/spec/lib/gitlab/external_authorization_spec.rb
new file mode 100644
index 00000000000..7394fbfe0ce
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization, :request_store do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { build(:user) }
+ let(:label) { 'dummy_label' }
+
+ describe '#access_allowed?' do
+ it 'is always true when the feature is disabled' do
+ # Not using `stub_application_setting` because the method is prepended in
+ # `EE::ApplicationSetting` which breaks when using `any_instance`
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/33587
+ expect(::Gitlab::CurrentSettings.current_application_settings)
+ .to receive(:external_authorization_service_enabled) { false }
+
+ expect(described_class).not_to receive(:access_for_user_to_label)
+
+ expect(described_class.access_allowed?(user, label)).to be_truthy
+ end
+ end
+
+ describe '#rejection_reason' do
+ it 'is always nil when the feature is disabled' do
+ expect(::Gitlab::CurrentSettings.current_application_settings)
+ .to receive(:external_authorization_service_enabled) { false }
+
+ expect(described_class).not_to receive(:access_for_user_to_label)
+
+ expect(described_class.rejection_reason(user, label)).to be_nil
+ end
+ end
+
+ describe '#access_for_user_to_label' do
+ it 'only loads the access once per request' do
+ enable_external_authorization_service_check
+
+ expect(::Gitlab::ExternalAuthorization::Access)
+ .to receive(:new).with(user, label).once.and_call_original
+
+ 2.times { described_class.access_for_user_to_label(user, label, nil) }
+ end
+
+ it 'logs the access request once per request' do
+ expect(::Gitlab::ExternalAuthorization::Logger)
+ .to receive(:log_access)
+ .with(an_instance_of(::Gitlab::ExternalAuthorization::Access),
+ 'the/project/path')
+ .once
+
+ 2.times { described_class.access_for_user_to_label(user, label, 'the/project/path') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
index 26529c4759d..569d5dcc757 100644
--- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -97,13 +97,13 @@ describe Gitlab::HookData::IssuableBuilder do
end
context 'merge_request is assigned' do
- let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:data) { described_class.new(merge_request).build(user: user) }
it 'returns correct hook data' do
expect(data[:object_attributes]['assignee_id']).to eq(user.id)
- expect(data[:assignee]).to eq(user.hook_attrs)
- expect(data).not_to have_key(:assignees)
+ expect(data[:assignees].first).to eq(user.hook_attrs)
+ expect(data).not_to have_key(:assignee)
end
end
end
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
index 9ce697adbba..39f80f92fa6 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -10,6 +10,7 @@ describe Gitlab::HookData::MergeRequestBuilder do
it 'includes safe attribute' do
%w[
assignee_id
+ assignee_ids
author_id
created_at
description
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e418516569a..ed557ffd4e3 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -102,6 +102,7 @@ merge_requests:
- merge_request_pipelines
- merge_request_assignees
- suggestions
+- assignees
merge_request_diff:
- merge_request
- merge_request_diff_commits
@@ -336,6 +337,9 @@ push_event_payload:
issue_assignees:
- issue
- assignee
+merge_request_assignees:
+- merge_request
+- assignee
lfs_file_locks:
- user
project_badges:
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index d0ed588f05f..ebb62124cb1 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -496,6 +496,7 @@ Project:
- merge_requests_ff_only_enabled
- merge_requests_rebase_enabled
- jobs_cache_index
+- external_authorization_classification_label
- pages_https_only
Author:
- name
@@ -621,3 +622,7 @@ Suggestion:
- outdated
- lines_above
- lines_below
+MergeRequestAssignee:
+- id
+- user_id
+- merge_request_id
diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb
index 6ec86163233..916f3876a8e 100644
--- a/spec/lib/gitlab/issuable_metadata_spec.rb
+++ b/spec/lib/gitlab/issuable_metadata_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::IssuableMetadata do
let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) }
let!(:downvote) { create(:award_emoji, :downvote, awardable: closed_issue) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: issue) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do
@@ -39,7 +39,7 @@ describe Gitlab::IssuableMetadata do
end
context 'merge requests' do
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:merge_request_closed) { create(:merge_request, state: "closed", source_project: project, target_project: project, title: "Closed Test") }
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 5fa1369c00a..fee1d701e3a 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -19,7 +19,7 @@ describe Notify do
create(:merge_request, source_project: project,
target_project: project,
author: current_user,
- assignee: assignee,
+ assignees: [assignee],
description: 'Awesome description')
end
@@ -275,7 +275,7 @@ describe Notify do
context 'for merge requests' do
describe 'that are new' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
@@ -300,7 +300,7 @@ describe Notify do
end
context 'when sent with a reason' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id, NotificationReason::ASSIGNED) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id, NotificationReason::ASSIGNED) }
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
@@ -324,7 +324,7 @@ describe Notify do
describe 'that are reassigned' do
let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -351,7 +351,7 @@ describe Notify do
end
context 'when sent with a reason' do
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::ASSIGNED) }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) }
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
@@ -364,11 +364,11 @@ describe Notify do
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED)
is_expected.to have_body_text(text)
- new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::MENTIONED)
+ new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED)
expect(new_subject).to have_body_text(text)
- new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, nil)
+ new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil)
expect(new_subject).to have_body_text(text)
end
@@ -376,7 +376,7 @@ describe Notify do
end
describe 'that are new with a description' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
@@ -476,7 +476,7 @@ describe Notify do
source_project: project,
target_project: project,
author: current_user,
- assignee: assignee,
+ assignees: [assignee],
description: 'Awesome description')
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c81572d739e..c7d7dbac736 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe ApplicationSetting do
- let(:setting) { described_class.create_from_defaults }
+ subject(:setting) { described_class.create_from_defaults }
it { include(CacheableAttributes) }
it { include(ApplicationSettingImplementation) }
@@ -284,6 +284,52 @@ describe ApplicationSetting do
expect(subject).to be_valid
end
end
+
+ describe 'when external authorization service is enabled' do
+ before do
+ setting.external_authorization_service_enabled = true
+ end
+
+ it { is_expected.not_to allow_value('not a URL').for(:external_authorization_service_url) }
+ it { is_expected.to allow_value('https://example.com').for(:external_authorization_service_url) }
+ it { is_expected.to allow_value('').for(:external_authorization_service_url) }
+ it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) }
+ it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) }
+ it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) }
+ it { is_expected.not_to allow_value('not a certificate').for(:external_auth_client_cert) }
+ it { is_expected.to allow_value('').for(:external_auth_client_cert) }
+ it { is_expected.to allow_value('').for(:external_auth_client_key) }
+
+ context 'when setting a valid client certificate for external authorization' do
+ let(:certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') }
+
+ before do
+ setting.external_auth_client_cert = certificate_data
+ end
+
+ it 'requires a valid client key when a certificate is set' do
+ expect(setting).not_to allow_value('fefefe').for(:external_auth_client_key)
+ end
+
+ it 'requires a matching certificate' do
+ other_private_key = File.read('spec/fixtures/x509_certificate_pk.key')
+
+ expect(setting).not_to allow_value(other_private_key).for(:external_auth_client_key)
+ end
+
+ it 'the credentials are valid when the private key can be read and matches the certificate' do
+ tls_attributes = [:external_auth_client_key_pass,
+ :external_auth_client_key,
+ :external_auth_client_cert]
+ setting.external_auth_client_key = File.read('spec/fixtures/passphrase_x509_certificate_pk.key')
+ setting.external_auth_client_key_pass = '5iveL!fe'
+
+ setting.validate
+
+ expect(setting.errors).not_to include(*tls_attributes)
+ end
+ end
+ end
end
context 'restrict creating duplicates' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 83b0f172f03..f3e78630c1b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -684,12 +684,12 @@ describe Ci::Pipeline, :mailer do
source_branch: 'feature',
target_project: project,
target_branch: 'master',
- assignee: assignee,
+ assignees: assignees,
milestone: milestone,
labels: labels)
end
- let(:assignee) { create(:user) }
+ let(:assignees) { create_list(:user, 2) }
let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_list(:label, 2) }
@@ -710,7 +710,7 @@ describe Ci::Pipeline, :mailer do
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s,
'CI_MERGE_REQUEST_TITLE' => merge_request.title,
- 'CI_MERGE_REQUEST_ASSIGNEES' => assignee.username,
+ 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(','))
end
@@ -730,7 +730,7 @@ describe Ci::Pipeline, :mailer do
end
context 'without assignee' do
- let(:assignee) { nil }
+ let(:assignees) { [] }
it 'does not expose assignee variable' do
expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
diff --git a/spec/models/concerns/deprecated_assignee_spec.rb b/spec/models/concerns/deprecated_assignee_spec.rb
new file mode 100644
index 00000000000..e394de0aa34
--- /dev/null
+++ b/spec/models/concerns/deprecated_assignee_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeprecatedAssignee do
+ let(:user) { create(:user) }
+
+ describe '#assignee_id=' do
+ it 'creates the merge_request_assignees relation' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ merge_request.reload
+
+ expect(merge_request.merge_request_assignees.count).to eq(1)
+ end
+
+ it 'nullifies the assignee_id column' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ merge_request.reload
+
+ expect(merge_request.read_attribute(:assignee_id)).to be_nil
+ end
+
+ context 'when relation already exists' do
+ it 'overwrites existing assignees' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.merge_request_assignees.create!(user_id: user.id)
+ merge_request.merge_request_assignees.create!(user_id: other_user.id)
+
+ expect { merge_request.update!(assignee_id: other_user.id) }
+ .to change { merge_request.reload.merge_request_assignees.count }
+ .from(2).to(1)
+ end
+ end
+ end
+
+ describe '#assignee=' do
+ it 'creates the merge_request_assignees relation' do
+ merge_request = create(:merge_request, assignee: user)
+
+ merge_request.reload
+
+ expect(merge_request.merge_request_assignees.count).to eq(1)
+ end
+
+ it 'nullifies the assignee_id column' do
+ merge_request = create(:merge_request, assignee: user)
+
+ merge_request.reload
+
+ expect(merge_request.read_attribute(:assignee_id)).to be_nil
+ end
+
+ context 'when relation already exists' do
+ it 'overwrites existing assignees' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignee: nil)
+ merge_request.merge_request_assignees.create!(user_id: user.id)
+ merge_request.merge_request_assignees.create!(user_id: other_user.id)
+
+ expect { merge_request.update!(assignee: other_user) }
+ .to change { merge_request.reload.merge_request_assignees.count }
+ .from(2).to(1)
+ end
+ end
+ end
+
+ describe '#assignee_id' do
+ it 'returns the first assignee ID' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignees: [user, other_user])
+
+ merge_request.reload
+
+ expect(merge_request.assignee_id).to eq(merge_request.assignee_ids.first)
+ end
+ end
+
+ describe '#assignees' do
+ context 'when assignee_id exists and there is no relation' do
+ it 'creates the relation' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignees }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
+ end
+
+ it 'nullifies the assignee_id' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignees }
+ .to change { merge_request.read_attribute(:assignee_id) }
+ .from(user.id).to(nil)
+ end
+ end
+
+ context 'when DB is read-only' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'returns a users relation' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
+ expect(merge_request.assignees).to eq([user])
+ end
+
+ it 'returns an empty relation if no assignee_id is set' do
+ merge_request = create(:merge_request, assignee_id: nil)
+
+ expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
+ expect(merge_request.assignees).to eq([])
+ end
+ end
+ end
+
+ describe '#assignee_ids' do
+ context 'when assignee_id exists and there is no relation' do
+ it 'creates the relation' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignee_ids }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
+ end
+
+ it 'nullifies the assignee_id' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignee_ids }
+ .to change { merge_request.read_attribute(:assignee_id) }
+ .from(user.id).to(nil)
+ end
+ end
+
+ context 'when DB is read-only' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'returns a list of user IDs' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ expect(merge_request.assignee_ids).to be_a(Array)
+ expect(merge_request.assignee_ids).to eq([user.id])
+ end
+
+ it 'returns an empty relation if no assignee_id is set' do
+ merge_request = create(:merge_request, assignee_id: nil)
+
+ expect(merge_request.assignee_ids).to be_a(Array)
+ expect(merge_request.assignee_ids).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 27ed298ae08..64f02978d79 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -502,8 +502,8 @@ describe Issuable do
let(:user2) { create(:user) }
before do
- merge_request.update(assignee: user)
- merge_request.update(assignee: user2)
+ merge_request.update(assignees: [user])
+ merge_request.update(assignees: [user, user2])
expect(Gitlab::HookData::IssuableBuilder)
.to receive(:new).with(merge_request).and_return(builder)
end
@@ -512,8 +512,7 @@ describe Issuable do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'assignee_id' => [user.id, user2.id],
- 'assignee' => [user.hook_attrs, user2.hook_attrs]
+ 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
))
merge_request.to_hook_data(user, old_associations: { assignees: [user] })
diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb
index 94798f0590d..f63ad958ed3 100644
--- a/spec/models/concerns/protected_ref_access_spec.rb
+++ b/spec/models/concerns/protected_ref_access_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe ProtectedRefAccess do
+ include ExternalAuthorizationServiceHelpers
+
subject(:protected_ref_access) do
create(:protected_branch, :maintainers_can_push).push_access_levels.first
end
@@ -29,5 +31,15 @@ describe ProtectedRefAccess do
expect(protected_ref_access.check_access(developer)).to be_falsy
end
+
+ context 'external authorization' do
+ it 'is false if external authorization denies access' do
+ maintainer = create(:user)
+ project.add_maintainer(maintainer)
+ external_service_deny_access(maintainer, project)
+
+ expect(protected_ref_access.check_access(maintainer)).to be_falsey
+ end
+ end
end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index d192fe70506..e91b5c4c86f 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -263,7 +263,7 @@ describe Event do
context 'merge request diff note event' do
let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) }
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 892dd053e39..0cd69cb4817 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Issue do
+ include ExternalAuthorizationServiceHelpers
+
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
it { is_expected.to have_many(:assignees) }
@@ -779,4 +781,47 @@ describe Issue do
it_behaves_like 'throttled touch' do
subject { create(:issue, updated_at: 1.hour.ago) }
end
+
+ context 'when an external authentication service' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe '#visible_to_user?' do
+ it 'is `false` when an external authorization service is enabled' do
+ issue = build(:issue, project: build(:project, :public))
+
+ expect(issue).not_to be_visible_to_user
+ end
+
+ it 'checks the external service to determine if an issue is readable by a user' do
+ project = build(:project, :public,
+ external_authorization_classification_label: 'a-label')
+ issue = build(:issue, project: project)
+ user = build(:user)
+
+ expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
+ expect(issue.visible_to_user?(user)).to be_falsy
+ end
+
+ it 'does not check the external service if a user does not have access to the project' do
+ project = build(:project, :private,
+ external_authorization_classification_label: 'a-label')
+ issue = build(:issue, project: project)
+ user = build(:user)
+
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+ expect(issue.visible_to_user?(user)).to be_falsy
+ end
+
+ it 'does not check the external webservice for admins' do
+ issue = build(:issue)
+ user = build(:admin)
+
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ issue.visible_to_user?(user)
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6f34ef9c1bc..f61857ea5ff 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -13,7 +13,7 @@ describe MergeRequest do
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
- it { is_expected.to belong_to(:assignee) }
+ it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:merge_request_diffs) }
context 'for forks' do
@@ -181,31 +181,6 @@ describe MergeRequest do
expect(MergeRequest::Metrics.count).to eq(1)
end
end
-
- describe '#refresh_merge_request_assignees' do
- set(:user) { create(:user) }
-
- it 'creates merge request assignees relation upon MR creation' do
- merge_request = create(:merge_request, assignee: nil)
-
- expect(merge_request.merge_request_assignees).to be_empty
-
- expect { merge_request.update!(assignee: user) }
- .to change { merge_request.reload.merge_request_assignees.count }
- .from(0).to(1)
- end
-
- it 'updates merge request assignees relation upon MR assignee change' do
- another_user = create(:user)
- merge_request = create(:merge_request, assignee: user)
-
- expect { merge_request.update!(assignee: another_user) }
- .to change { merge_request.reload.merge_request_assignees.first.assignee }
- .from(user).to(another_user)
-
- expect(merge_request.merge_request_assignees.count).to eq(1)
- end
- end
end
describe 'respond to' do
@@ -337,34 +312,18 @@ describe MergeRequest do
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
- allow(subject).to receive(:assignee).and_return(nil)
+ allow(subject).to receive(:assignees).and_return([])
expect(subject.card_attributes)
- .to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ .to eq({ 'Author' => 'Robert', 'Assignee' => "" })
end
- it 'includes the assignee name' do
+ it 'includes the assignees name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
- allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))
+ allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')])
expect(subject.card_attributes)
- .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
- end
- end
-
- describe '#assignee_ids' do
- it 'returns an array of the assigned user id' do
- subject.assignee_id = 123
-
- expect(subject.assignee_ids).to eq([123])
- end
- end
-
- describe '#assignee_ids=' do
- it 'sets assignee_id to the last id in the array' do
- subject.assignee_ids = [123, 456]
-
- expect(subject.assignee_id).to eq(456)
+ .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' })
end
end
@@ -372,7 +331,7 @@ describe MergeRequest do
let(:user) { create(:user) }
it 'returns true for a user that is assigned to a merge request' do
- subject.assignee = user
+ subject.assignees = [user]
expect(subject.assignee_or_author?(user)).to eq(true)
end
@@ -1949,15 +1908,14 @@ describe MergeRequest do
it 'updates when assignees change' do
user1 = create(:user)
user2 = create(:user)
- mr = create(:merge_request, assignee: user1)
+ mr = create(:merge_request, assignees: [user1])
mr.project.add_developer(user1)
mr.project.add_developer(user2)
expect(user1.assigned_open_merge_requests_count).to eq(1)
expect(user2.assigned_open_merge_requests_count).to eq(0)
- mr.assignee = user2
- mr.save
+ mr.assignees = [user2]
expect(user1.assigned_open_merge_requests_count).to eq(0)
expect(user2.assigned_open_merge_requests_count).to eq(1)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5eb31430ccd..7222580e115 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe Project do
include ProjectForksHelper
include GitHelpers
+ include ExternalAuthorizationServiceHelpers
it_behaves_like 'having unique enum values'
@@ -4417,6 +4418,25 @@ describe Project do
end
end
+ describe '#external_authorization_classification_label' do
+ it 'falls back to the default when none is configured' do
+ enable_external_authorization_service_check
+
+ expect(build(:project).external_authorization_classification_label)
+ .to eq('default_label')
+ end
+
+ it 'returns the classification label if it was configured on the project' do
+ enable_external_authorization_service_check
+
+ project = build(:project,
+ external_authorization_classification_label: 'hello')
+
+ expect(project.external_authorization_classification_label)
+ .to eq('hello')
+ end
+ end
+
describe "#pages_https_only?" do
subject { build(:project) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a45a2737b13..d1338e34bb8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2816,9 +2816,9 @@ describe User do
project = create(:project, :public)
archived_project = create(:project, :public, :archived)
- create(:merge_request, source_project: project, author: user, assignee: user)
- create(:merge_request, :closed, source_project: project, author: user, assignee: user)
- create(:merge_request, source_project: archived_project, author: user, assignee: user)
+ create(:merge_request, source_project: project, author: user, assignees: [user])
+ create(:merge_request, :closed, source_project: project, author: user, assignees: [user])
+ create(:merge_request, source_project: archived_project, author: user, assignees: [user])
expect(user.assigned_open_merge_requests_count(force: true)).to eq 1
end
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index c03d95b34db..09be831dcd5 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe BasePolicy do
+ include ExternalAuthorizationServiceHelpers
+
describe '.class_for' do
it 'detects policy class based on the subject ancestors' do
expect(DeclarativePolicy.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy)
@@ -16,4 +18,25 @@ describe BasePolicy do
expect(DeclarativePolicy.class_for(:global)).to eq(GlobalPolicy)
end
end
+
+ describe 'read cross project' do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(current_user, [user]) }
+
+ it { is_expected.to be_allowed(:read_cross_project) }
+
+ context 'when an external authorization service is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it { is_expected.not_to be_allowed(:read_cross_project) }
+
+ it 'allows admins' do
+ expect(described_class.new(build(:admin), nil)).to be_allowed(:read_cross_project)
+ end
+ end
+ end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 008d118b557..b149dbcf871 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe IssuePolicy do
+ include ExternalAuthorizationServiceHelpers
+
let(:guest) { create(:user) }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -204,4 +206,21 @@ describe IssuePolicy do
end
end
end
+
+ context 'with external authorization enabled' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:policies) { described_class.new(user, issue) }
+
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'can read the issue iid without accessing the external service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(policies).to be_allowed(:read_issue_iid)
+ end
+ end
end
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index 1efa70addc2..81279225d61 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe MergeRequestPolicy do
+ include ExternalAuthorizationServiceHelpers
+
let(:guest) { create(:user) }
let(:author) { create(:user) }
let(:developer) { create(:user) }
@@ -47,4 +49,21 @@ describe MergeRequestPolicy do
expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request)
end
end
+
+ context 'with external authorization enabled' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:policies) { described_class.new(user, merge_request) }
+
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'can read the issue iid without accessing the external service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(policies).to be_allowed(:read_merge_request_iid)
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 125ed818bc6..42f8bf3137b 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe ProjectPolicy do
+ include ExternalAuthorizationServiceHelpers
include_context 'ProjectPolicy context'
set(:guest) { create(:user) }
set(:reporter) { create(:user) }
@@ -292,4 +293,56 @@ describe ProjectPolicy do
projects: [clusterable])
end
end
+
+ context 'reading a project' do
+ it 'allows access when a user has read access to the repo' do
+ expect(described_class.new(owner, project)).to be_allowed(:read_project)
+ expect(described_class.new(developer, project)).to be_allowed(:read_project)
+ expect(described_class.new(admin, project)).to be_allowed(:read_project)
+ end
+
+ it 'never checks the external service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(described_class.new(owner, project)).to be_allowed(:read_project)
+ end
+
+ context 'with an external authorization service' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'allows access when the external service allows it' do
+ external_service_allow_access(owner, project)
+ external_service_allow_access(developer, project)
+
+ expect(described_class.new(owner, project)).to be_allowed(:read_project)
+ expect(described_class.new(developer, project)).to be_allowed(:read_project)
+ end
+
+ it 'does not check the external service for admins and allows access' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(described_class.new(admin, project)).to be_allowed(:read_project)
+ end
+
+ it 'prevents all but seeing a public project in a list when access is denied' do
+ [developer, owner, build(:user), nil].each do |user|
+ external_service_deny_access(user, project)
+ policy = described_class.new(user, project)
+
+ expect(policy).not_to be_allowed(:read_project)
+ expect(policy).not_to be_allowed(:owner_access)
+ expect(policy).not_to be_allowed(:change_namespace)
+ end
+ end
+
+ it 'passes the full path to external authorization for logging purposes' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(owner, 'default_label', project.full_path).and_call_original
+
+ described_class.new(owner, project).allowed?(:read_project)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 0ac23505de7..065b16c6221 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -270,8 +270,8 @@ describe API::Events do
end
context 'when exists some events' do
- let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
- let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
+ let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+ let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
before do
create_event(merge_request1)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 7ffa365c651..45818edbf68 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -5,14 +5,15 @@ describe API::MergeRequests do
let(:base_time) { Time.now }
set(:user) { create(:user) }
+ set(:user2) { create(:user) }
set(:admin) { create(:user, :admin) }
let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
- let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
- let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
+ let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
let(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) }
@@ -20,6 +21,9 @@ describe API::MergeRequests do
before do
project.add_reporter(user)
+ project.add_reporter(user2)
+
+ stub_licensed_features(multiple_merge_request_assignees: false)
end
shared_context 'with labels' do
@@ -45,9 +49,9 @@ describe API::MergeRequests do
get api(endpoint_path, user)
end
- create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+ create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time)
- merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+ merge_request = create(:merge_request, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time)
merge_request.metrics.update!(merged_by: user,
latest_closed_by: user,
@@ -333,7 +337,7 @@ describe API::MergeRequests do
state: 'closed',
milestone: milestone1,
author: user,
- assignee: user,
+ assignees: [user],
source_project: project,
target_project: project,
title: "Test",
@@ -451,7 +455,7 @@ describe API::MergeRequests do
context 'when authenticated' do
let!(:project2) { create(:project, :public, namespace: user.namespace) }
- let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) }
+ let!(:merge_request2) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2) }
let(:user2) { create(:user) }
it 'returns an array of all merge requests except unauthorized ones' do
@@ -494,7 +498,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests created by current user if no scope is given' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2)
@@ -502,7 +506,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests authored by the given user' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user), params: { author_id: user2.id, scope: :all }
@@ -510,7 +514,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests assigned to the given user' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all }
@@ -535,7 +539,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests assigned to me' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'assigned_to_me' }
@@ -543,7 +547,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests assigned to me (kebab-case)' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'assigned-to-me' }
@@ -551,7 +555,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests created by me' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'created_by_me' }
@@ -559,7 +563,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests created by me (kebab-case)' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'created-by-me' }
@@ -567,7 +571,7 @@ describe API::MergeRequests do
end
it 'returns merge requests reacted by the authenticated user by the given emoji' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
award_emoji = create(:award_emoji, awardable: merge_request3, user: user2, name: 'star')
get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' }
@@ -700,7 +704,7 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests", user)
end.count
- create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, created_at: base_time)
+ create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, created_at: base_time)
expect do
get api("/projects/#{project.id}/merge_requests", user)
@@ -730,7 +734,7 @@ describe API::MergeRequests do
describe "GET /projects/:id/merge_requests/:merge_request_iid" do
it 'matches json schema' do
- merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
+ merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_gitlab_http_status(200)
@@ -851,7 +855,7 @@ describe API::MergeRequests do
end
context 'Work in Progress' do
- let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+ let!(:merge_request_wip) { create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
it "returns merge request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
@@ -867,7 +871,7 @@ describe API::MergeRequests do
merge_request_overflow = create(:merge_request, :simple,
author: user,
- assignee: user,
+ assignees: [user],
source_project: project,
source_branch: 'expand-collapse-files',
target_project: project,
@@ -1005,6 +1009,71 @@ describe API::MergeRequests do
end
describe 'POST /projects/:id/merge_requests' do
+ context 'support for deprecated assignee_id' do
+ let(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ assignee_id: user2.id
+ }
+ end
+
+ it 'creates a new merge request' do
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new merge request when assignee_id is empty' do
+ params[:assignee_id] = ''
+
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignee']).to be_nil
+ end
+
+ it 'filters assignee_id of unauthorized user' do
+ private_project = create(:project, :private, :repository)
+ another_user = create(:user)
+ private_project.add_maintainer(user)
+ params[:assignee_id] = another_user.id
+
+ post api("/projects/#{private_project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['assignee']).to be_nil
+ end
+ end
+
+ context 'single assignee restrictions' do
+ let(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ assignee_ids: [user.id, user2.id]
+ }
+ end
+
+ it 'creates a new project merge request with no more than one assignee' do
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignees'].count).to eq(1)
+ expect(json_response['assignees'].first['name']).to eq(user.name)
+ expect(json_response.dig('assignee', 'name')).to eq(user.name)
+ end
+ end
+
context 'between branches projects' do
context 'different labels' do
let(:params) do
@@ -1574,6 +1643,19 @@ describe API::MergeRequests do
expect(json_response['force_remove_source_branch']).to be_truthy
end
+ it 'filters assignee_id of unauthorized user' do
+ private_project = create(:project, :private, :repository)
+ mr = create(:merge_request, source_project: private_project, target_project: private_project)
+ another_user = create(:user)
+ private_project.add_maintainer(user)
+ params = { assignee_id: another_user.id }
+
+ put api("/projects/#{private_project.id}/merge_requests/#{mr.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['assignee']).to be_nil
+ end
+
context 'when updating labels' do
it 'allows special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
@@ -1728,7 +1810,7 @@ describe API::MergeRequests do
issue = create(:issue, project: jira_project)
description = "Closes #{ext_issue.to_reference(jira_project)}\ncloses #{issue.to_reference}"
merge_request = create(:merge_request,
- :simple, author: user, assignee: user, source_project: jira_project, description: description)
+ :simple, author: user, assignees: [user], source_project: jira_project, description: description)
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2bfb17d9c9a..352ea448c00 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -46,6 +46,8 @@ shared_examples 'languages and percentages JSON response' do
end
describe API::Projects do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
@@ -1336,6 +1338,39 @@ describe API::Projects do
end
end
end
+
+ context 'with external authorization' do
+ let(:project) do
+ create(:project,
+ namespace: user.namespace,
+ external_authorization_classification_label: 'the-label')
+ end
+
+ context 'when the user has access to the project' do
+ before do
+ external_service_allow_access(user, project)
+ end
+
+ it 'includes the label in the response' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['external_authorization_classification_label']).to eq('the-label')
+ end
+ end
+
+ context 'when the external service denies access' do
+ before do
+ external_service_deny_access(user, project)
+ end
+
+ it 'returns a 404' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
describe 'GET /projects/:id/users' do
@@ -1890,6 +1925,20 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(403)
end
end
+
+ context 'when updating external classification' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'updates the classification label' do
+ put(api("/projects/#{project.id}", user), params: { external_authorization_classification_label: 'new label' })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(project.reload.external_authorization_classification_label).to eq('new label')
+ end
+ end
end
describe 'POST /projects/:id/archive' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index f869325e892..527ab1cfb66 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -116,6 +116,39 @@ describe API::Settings, 'Settings' do
expect(json_response['performance_bar_allowed_group_id']).to be_nil
end
+ context 'external policy classification settings' do
+ let(:settings) do
+ {
+ external_authorization_service_enabled: true,
+ external_authorization_service_url: 'https://custom.service/',
+ external_authorization_service_default_label: 'default',
+ external_authorization_service_timeout: 9.99,
+ external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'),
+ external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'),
+ external_auth_client_key_pass: "5iveL!fe"
+ }
+ end
+ let(:attribute_names) { settings.keys.map(&:to_s) }
+
+ it 'includes the attributes in the API' do
+ get api("/application/settings", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ attribute_names.each do |attribute|
+ expect(json_response.keys).to include(attribute)
+ end
+ end
+
+ it 'allows updating the settings' do
+ put api("/application/settings", admin), params: settings
+
+ expect(response).to have_gitlab_http_status(200)
+ settings.each do |attribute, value|
+ expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
+ end
+ end
+ end
+
context "missing plantuml_url value when plantuml_enabled is true" do
it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { plantuml_enabled: true }
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index d02b4c554b1..b58d95ccb43 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe GroupChildEntity do
+ include ExternalAuthorizationServiceHelpers
include Gitlab::Routing.url_helpers
let(:user) { create(:user) }
@@ -109,4 +110,22 @@ describe GroupChildEntity do
it_behaves_like 'group child json'
end
+
+ describe 'for a project with external authorization enabled' do
+ let(:object) do
+ create(:project, :with_avatar,
+ description: 'Awesomeness')
+ end
+
+ before do
+ enable_external_authorization_service_check
+ object.add_maintainer(user)
+ end
+
+ it 'does not hit the external authorization service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(json[:can_edit]).to eq(false)
+ end
+ end
end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index a4a733eff77..258e5635113 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ApplicationSettings::UpdateService do
+ include ExternalAuthorizationServiceHelpers
+
let(:application_settings) { create(:application_setting) }
let(:admin) { create(:user, :admin) }
let(:params) { {} }
@@ -143,4 +145,37 @@ describe ApplicationSettings::UpdateService do
end
end
end
+
+ context 'when external authorization is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not save the settings with an error if the service denies access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(admin, 'new-label') { false }
+
+ described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute
+
+ expect(application_settings.errors[:external_authorization_service_default_label]).to be_present
+ end
+
+ it 'saves the setting when the user has access to the label' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(admin, 'new-label') { true }
+
+ described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute
+
+ # Read the attribute directly to avoid the stub from
+ # `enable_external_authorization_service_check`
+ expect(application_settings[:external_authorization_service_default_label]).to eq('new-label')
+ end
+
+ it 'does not validate the label if it was not passed' do
+ expect(::Gitlab::ExternalAuthorization)
+ .not_to receive(:access_allowed?)
+
+ described_class.new(application_settings, admin, { home_page_url: 'http://foo.bar' }).execute
+ end
+ end
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index ca366cdf1df..363b7266940 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -76,14 +76,14 @@ describe Issuable::BulkUpdateService do
end
describe 'updating merge request assignee' do
- let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) }
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) }
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
new_assignee = create(:user)
project.add_developer(new_assignee)
- result = bulk_update(merge_request, assignee_id: new_assignee.id)
+ result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id])
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
@@ -93,22 +93,22 @@ describe Issuable::BulkUpdateService do
assignee = create(:user)
project.add_developer(assignee)
- expect { bulk_update(merge_request, assignee_id: assignee.id) }
- .to change { merge_request.reload.assignee }.from(user).to(assignee)
+ expect { bulk_update(merge_request, assignee_ids: [assignee.id]) }
+ .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id])
end
end
context "when the new assignee ID is #{IssuableFinder::NONE}" do
it 'unassigns the issues' do
- expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) }
- .to change { merge_request.reload.assignee }.to(nil)
+ expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) }
+ .to change { merge_request.reload.assignee_ids }.to([])
end
end
context 'when the new assignee ID is not present' do
it 'does not unassign' do
- expect { bulk_update(merge_request, assignee_id: nil) }
- .not_to change { merge_request.reload.assignee }
+ expect { bulk_update(merge_request, assignee_ids: []) }
+ .not_to change { merge_request.reload.assignee_ids }
end
end
end
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
index 8ccbba7fa58..15d1bb73ca3 100644
--- a/spec/services/issuable/destroy_service_spec.rb
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -34,7 +34,7 @@ describe Issuable::DestroyService do
end
context 'when issuable is a merge request' do
- let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignees: [user]) }
it 'destroys the merge request' do
expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1)
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index d37ca13ebd2..91bf4dccd77 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -43,9 +43,9 @@ describe Members::DestroyService do
shared_examples 'a service destroying a member with access' do
it_behaves_like 'a service destroying a member'
- it 'invalidates cached counts for todos and assigned issues and merge requests', :aggregate_failures do
+ it 'invalidates cached counts for assigned issues and merge requests', :aggregate_failures do
create(:issue, project: group_project, assignees: [member_user])
- create(:merge_request, source_project: group_project, assignee: member_user)
+ create(:merge_request, source_project: group_project, assignees: [member_user])
create(:todo, :pending, project: group_project, user: member_user)
create(:todo, :done, project: group_project, user: member_user)
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 433ffbd97f0..706bcea8199 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -4,7 +4,7 @@ describe MergeRequests::CloseService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, assignees: [user2], author: create(:user)) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 393299cce00..20bf1cbb8b6 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -118,7 +118,7 @@ describe MergeRequests::CreateFromIssueService do
result = service.execute
- expect(result[:merge_request].assignee).to eq(user)
+ expect(result[:merge_request].assignees).to eq([user])
end
context 'when ref branch is set' do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index dc5d1cf2f04..30271e04c8e 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -32,7 +32,7 @@ describe MergeRequests::CreateService do
expect(merge_request).to be_valid
expect(merge_request.work_in_progress?).to be(false)
expect(merge_request.title).to eq('Awesome merge_request')
- expect(merge_request.assignee).to be_nil
+ expect(merge_request.assignees).to be_empty
expect(merge_request.merge_params['force_remove_source_branch']).to eq('1')
end
@@ -73,7 +73,7 @@ describe MergeRequests::CreateService do
description: "well this is not done yet\n/wip",
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
@@ -89,7 +89,7 @@ describe MergeRequests::CreateService do
description: "well this is not done yet\n/wip",
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
@@ -106,11 +106,11 @@ describe MergeRequests::CreateService do
description: 'please fix',
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
- it { expect(merge_request.assignee).to eq assignee }
+ it { expect(merge_request.assignees).to eq([assignee]) }
it 'creates a todo for new assignee' do
attributes = {
@@ -301,7 +301,7 @@ describe MergeRequests::CreateService do
let(:opts) do
{
- assignee_id: create(:user).id,
+ assignee_ids: create(:user).id,
milestone_id: 1,
title: 'Title',
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"),
@@ -317,7 +317,7 @@ describe MergeRequests::CreateService do
it 'assigns and sets milestone to issuable from command' do
expect(merge_request).to be_persisted
- expect(merge_request.assignee).to eq(assignee)
+ expect(merge_request.assignees).to eq([assignee])
expect(merge_request.milestone).to eq(milestone)
end
end
@@ -332,28 +332,28 @@ describe MergeRequests::CreateService do
end
it 'removes assignee_id when user id is invalid' do
- opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'removes assignee_id when user id is 0' do
- opts = { title: 'Title', description: 'Description', assignee_id: 0 }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'saves assignee when user id is valid' do
project.add_maintainer(assignee)
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee).to eq(assignee)
+ expect(merge_request.assignees).to eq([assignee])
end
context 'when assignee is set' do
@@ -361,7 +361,7 @@ describe MergeRequests::CreateService do
{
title: 'Title',
description: 'Description',
- assignee_id: assignee.id,
+ assignee_ids: [assignee.id],
source_branch: 'feature',
target_branch: 'master'
}
@@ -387,7 +387,7 @@ describe MergeRequests::CreateService do
levels.each do |level|
it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
project.update(visibility_level: level)
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
merge_request = described_class.new(project, user, opts).execute
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index 1430e12a07e..a87d8b8752c 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -7,7 +7,7 @@ describe MergeRequests::FfMergeService do
create(:merge_request,
source_branch: 'flatten-dir',
target_branch: 'improve/awesome',
- assignee: user2,
+ assignees: [user2],
author: create(:user))
end
let(:project) { merge_request.project }
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 887ec17171e..b0b3273e3dc 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe MergeRequests::MergeService do
set(:user) { create(:user) }
set(:user2) { create(:user) }
- let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) }
+ let(:merge_request) { create(:merge_request, :simple, author: user2, assignees: [user2]) }
let(:project) { merge_request.project }
before do
@@ -111,7 +111,7 @@ describe MergeRequests::MergeService do
end
context 'closes related todos' do
- let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user], author: user) }
let(:project) { merge_request.project }
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
let!(:todo) do
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index a3b48abae26..24d09c1fd00 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -149,7 +149,7 @@ describe MergeRequests::MergeToRefService do
end
context 'does not close related todos' do
- let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user], author: user) }
let(:project) { merge_request.project }
let!(:todo) do
create(:todo, :assigned,
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 5ad6f5528f9..2cebefee5d6 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe MergeRequests::PostMergeService do
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:project) { merge_request.project }
before do
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 21e71509ed6..8b6db1ce33e 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -4,7 +4,7 @@ describe MergeRequests::ReopenService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, :closed, assignees: [user2], author: create(:user)) }
let(:project) { merge_request.project }
before do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 8e367db031c..0525899ebfa 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -13,7 +13,7 @@ describe MergeRequests::UpdateService, :mailer do
let(:merge_request) do
create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}",
- assignee_id: user3.id,
+ assignee_ids: [user3.id],
source_project: project,
author: create(:user))
end
@@ -48,7 +48,7 @@ describe MergeRequests::UpdateService, :mailer do
{
title: 'New title',
description: 'Also please fix',
- assignee_id: user2.id,
+ assignee_ids: [user.id],
state_event: 'close',
label_ids: [label.id],
target_branch: 'target',
@@ -71,7 +71,7 @@ describe MergeRequests::UpdateService, :mailer do
it 'matches base expectations' do
expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('New title')
- expect(@merge_request.assignee).to eq(user2)
+ expect(@merge_request.assignees).to match_array([user])
expect(@merge_request).to be_closed
expect(@merge_request.labels.count).to eq(1)
expect(@merge_request.labels.first.title).to eq(label.name)
@@ -106,7 +106,7 @@ describe MergeRequests::UpdateService, :mailer do
note = find_note('assigned to')
expect(note).not_to be_nil
- expect(note.note).to include "assigned to #{user2.to_reference}"
+ expect(note.note).to include "assigned to #{user.to_reference} and unassigned #{user3.to_reference}"
end
it 'creates a resource label event' do
@@ -293,7 +293,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'when is reassigned' do
before do
- update_merge_request({ assignee: user2 })
+ update_merge_request({ assignee_ids: [user2.id] })
end
it 'marks previous assignee pending todos as done' do
@@ -387,7 +387,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'when the assignee changes' do
it 'updates open merge request counter for assignees when merge request is reassigned' do
- update_merge_request(assignee_id: user2.id)
+ update_merge_request(assignee_ids: [user2.id])
expect(user3.assigned_open_merge_requests_count).to eq 0
expect(user2.assigned_open_merge_requests_count).to eq 1
@@ -541,36 +541,36 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- context 'updating asssignee_id' do
+ context 'updating asssignee_ids' do
it 'does not update assignee when assignee_id is invalid' do
- merge_request.update(assignee_id: user.id)
+ merge_request.update(assignee_ids: [user.id])
- update_merge_request(assignee_id: -1)
+ update_merge_request(assignee_ids: [-1])
- expect(merge_request.reload.assignee).to eq(user)
+ expect(merge_request.reload.assignees).to eq([user])
end
it 'unassigns assignee when user id is 0' do
- merge_request.update(assignee_id: user.id)
+ merge_request.update(assignee_ids: [user.id])
- update_merge_request(assignee_id: 0)
+ update_merge_request(assignee_ids: [0])
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'saves assignee when user id is valid' do
- update_merge_request(assignee_id: user.id)
+ update_merge_request(assignee_ids: [user.id])
- expect(merge_request.assignee_id).to eq(user.id)
+ expect(merge_request.assignee_ids).to eq([user.id])
end
it 'does not update assignee_id when user cannot read issue' do
- non_member = create(:user)
- original_assignee = merge_request.assignee
+ non_member = create(:user)
+ original_assignees = merge_request.assignees
- update_merge_request(assignee_id: non_member.id)
+ update_merge_request(assignee_ids: [non_member.id])
- expect(merge_request.assignee_id).to eq(original_assignee.id)
+ expect(merge_request.reload.assignees).to eq(original_assignees)
end
context "when issuable feature is private" do
@@ -583,7 +583,7 @@ describe MergeRequests::UpdateService, :mailer do
feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level"
project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
- expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee }
+ expect { update_merge_request(assignee_ids: [assignee]) }.not_to change { merge_request.reload.assignees }
end
end
end
@@ -619,7 +619,7 @@ describe MergeRequests::UpdateService, :mailer do
end
it 'is allowed by a user that can push to the source and can update the merge request' do
- merge_request.update!(assignee: user)
+ merge_request.update!(assignees: [user])
source_project.add_developer(user)
update_merge_request(allow_collaboration: true, title: 'Updated title')
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 9ba4a11104a..ac4aabf3fbd 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe NotificationService, :mailer do
include EmailSpec::Matchers
+ include ExternalAuthorizationServiceHelpers
include NotificationHelpers
let(:notification) { described_class.new }
@@ -125,11 +126,7 @@ describe NotificationService, :mailer do
shared_examples 'participating by assignee notification' do
it 'emails the participant' do
- if issuable.is_a?(Issue)
- issuable.assignees << participant
- else
- issuable.update_attribute(:assignee, participant)
- end
+ issuable.assignees << participant
notification_trigger
@@ -620,13 +617,13 @@ describe NotificationService, :mailer do
context "merge request diff note" do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignees: [user], author: create(:user)) }
let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
before do
build_team(note.project)
project.add_maintainer(merge_request.author)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
end
describe '#new_note' do
@@ -637,7 +634,7 @@ describe NotificationService, :mailer do
notification.new_note(note)
expect(SentNotification.last(3).map(&:recipient).map(&:id))
- .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id)
+ .to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id)
expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end
end
@@ -1223,11 +1220,12 @@ describe NotificationService, :mailer do
let(:group) { create(:group) }
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
- let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
+ let(:assignee) { create(:user) }
+ let(:merge_request) { create :merge_request, source_project: project, assignees: [assignee], description: 'cc @participant' }
before do
project.add_maintainer(merge_request.author)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
@@ -1239,7 +1237,7 @@ describe NotificationService, :mailer do
it do
notification.new_merge_request(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
@@ -1254,9 +1252,11 @@ describe NotificationService, :mailer do
it 'adds "assigned" reason for assignee, if any' do
notification.new_merge_request(merge_request, @u_disabled)
- email = find_email_for(merge_request.assignee)
+ merge_request.assignees.each do |assignee|
+ email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ end
end
it "emails any mentioned users with the mention level" do
@@ -1347,9 +1347,9 @@ describe NotificationService, :mailer do
end
it do
- notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, [assignee])
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(merge_request.author)
should_email(@u_watcher)
should_email(@u_participant_mentioned)
@@ -1365,17 +1365,19 @@ describe NotificationService, :mailer do
end
it 'adds "assigned" reason for new assignee' do
- notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, [assignee])
- email = find_email_for(merge_request.assignee)
+ merge_request.assignees.each do |assignee|
+ email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ end
end
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
- let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, merge_request.author) }
+ let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) }
end
end
@@ -1388,7 +1390,7 @@ describe NotificationService, :mailer do
it do
notification.push_to_merge_request(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
@@ -1430,7 +1432,7 @@ describe NotificationService, :mailer do
should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2)
- should_not_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_not_email(assignee) }
should_not_email(merge_request.author)
should_not_email(@u_watcher)
should_not_email(@u_participant_mentioned)
@@ -1499,7 +1501,7 @@ describe NotificationService, :mailer do
it do
notification.close_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -1529,7 +1531,7 @@ describe NotificationService, :mailer do
it do
notification.merge_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -1581,7 +1583,7 @@ describe NotificationService, :mailer do
it do
notification.reopen_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1606,7 +1608,7 @@ describe NotificationService, :mailer do
it do
notification.resolve_all_discussions(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1850,8 +1852,8 @@ describe NotificationService, :mailer do
let(:guest) { create(:user) }
let(:developer) { create(:user) }
let(:assignee) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) }
- let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") }
+ let(:merge_request) { create(:merge_request, source_project: private_project, assignees: [assignee]) }
+ let(:merge_request1) { create(:merge_request, source_project: private_project, assignees: [assignee], description: "cc @#{guest.username}") }
let(:note) { create(:note, noteable: merge_request, project: private_project) }
before do
@@ -2217,6 +2219,46 @@ describe NotificationService, :mailer do
end
end
+ context 'with external authorization service' do
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:note) { create(:note, noteable: issue, project: project) }
+ let(:member) { create(:user) }
+
+ subject { NotificationService.new }
+
+ before do
+ project.add_maintainer(member)
+ member.global_notification_setting.update!(level: :watch)
+ end
+
+ it 'sends email when the service is not enabled' do
+ expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original
+
+ subject.new_issue(issue, member)
+ end
+
+ context 'when the service is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not send an email' do
+ expect(Notify).not_to receive(:new_issue_email)
+
+ subject.new_issue(issue, member)
+ end
+
+ it 'still delivers email to admins' do
+ member.update!(admin: true)
+
+ expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original
+
+ subject.new_issue(issue, member)
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index e8418b09dc2..e1ec932918e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Projects::CreateService, '#execute' do
+ include ExternalAuthorizationServiceHelpers
include GitHelpers
let(:gitlab_shell) { Gitlab::Shell.new }
@@ -344,6 +345,42 @@ describe Projects::CreateService, '#execute' do
expect(rugged.config['gitlab.fullpath']).to eq project.full_path
end
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not save the project with an error if the service denies access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label', any_args) { false }
+
+ project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' }))
+
+ expect(project.errors[:external_authorization_classification_label]).to be_present
+ expect(project).not_to be_persisted
+ end
+
+ it 'saves the project when the user has access to the label' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label', any_args) { true }
+
+ project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' }))
+
+ expect(project).to be_persisted
+ expect(project.external_authorization_classification_label).to eq('new-label')
+ end
+
+ it 'does not save the project when the user has no access to the default label and no label is provided' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'default_label', any_args) { false }
+
+ project = create_project(user, opts)
+
+ expect(project.errors[:external_authorization_classification_label]).to be_present
+ expect(project).not_to be_persisted
+ end
+ end
+
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 90eaea9c872..95eb17b5e3a 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Projects::UpdateService do
+ include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
let(:user) { create(:user) }
@@ -361,6 +362,46 @@ describe Projects::UpdateService do
call_service
end
end
+
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not save the project with an error if the service denies access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label') { false }
+
+ result = update_project(project, user, { external_authorization_classification_label: 'new-label' })
+
+ expect(result[:message]).to be_present
+ expect(result[:status]).to eq(:error)
+ end
+
+ it 'saves the new label if the service allows access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label') { true }
+
+ result = update_project(project, user, { external_authorization_classification_label: 'new-label' })
+
+ expect(result[:status]).to eq(:success)
+ expect(project.reload.external_authorization_classification_label).to eq('new-label')
+ end
+
+ it 'checks the default label when the classification label was cleared' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'default_label') { true }
+
+ update_project(project, user, { external_authorization_classification_label: '' })
+ end
+
+ it 'does not check the label when it does not change' do
+ expect(::Gitlab::ExternalAuthorization)
+ .not_to receive(:access_allowed?)
+
+ update_project(project, user, { name: 'New name' })
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index c7e5cca324f..c450f89c3cb 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -16,7 +16,9 @@ describe QuickActions::InterpretService do
let(:service) { described_class.new(project, developer) }
before do
- stub_licensed_features(multiple_issue_assignees: false)
+ stub_licensed_features(multiple_issue_assignees: false,
+ multiple_merge_request_assignees: false)
+
project.add_developer(developer)
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 13d7d795703..51c5a803dbd 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -166,8 +166,8 @@ describe SystemNoteService do
end
end
- describe '.change_issue_assignees' do
- subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) }
+ describe '.change_issuable_assignees' do
+ subject { described_class.change_issuable_assignees(noteable, project, author, [assignee]) }
let(:assignee) { create(:user) }
let(:assignee1) { create(:user) }
@@ -180,7 +180,7 @@ describe SystemNoteService do
def build_note(old_assignees, new_assignees)
issue.assignees = new_assignees
- described_class.change_issue_assignees(issue, project, author, old_assignees).note
+ described_class.change_issuable_assignees(issue, project, author, old_assignees).note
end
it_behaves_like 'a note with overridable created_at'
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 8631f3f9a33..89411b2e908 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -272,28 +272,6 @@ describe TodoService do
end
end
- describe '#reassigned_issue' do
- it 'creates a pending todo for new assignee' do
- unassigned_issue.assignees << john_doe
- service.reassigned_issue(unassigned_issue, author)
-
- should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo if unassigned' do
- issue.assignees.destroy_all # rubocop: disable DestroyAll
-
- should_not_create_any_todo { service.reassigned_issue(issue, author) }
- end
-
- it 'creates a todo if new assignee is the current user' do
- unassigned_issue.assignees << john_doe
- service.reassigned_issue(unassigned_issue, john_doe)
-
- should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
- end
- end
-
describe '#mark_pending_todos_as_done' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
@@ -504,10 +482,60 @@ describe TodoService do
end
end
+ describe '#reassigned_issuable' do
+ shared_examples 'reassigned issuable' do
+ it 'creates a pending todo for new assignee' do
+ issuable_unassigned.assignees = [john_doe]
+ service.reassigned_issuable(issuable_unassigned, author)
+
+ should_create_todo(user: john_doe, target: issuable_unassigned, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo if unassigned' do
+ issuable_assigned.assignees = []
+
+ should_not_create_any_todo { service.reassigned_issuable(issuable_assigned, author) }
+ end
+
+ it 'creates a todo if new assignee is the current user' do
+ issuable_assigned.assignees = [john_doe]
+ service.reassigned_issuable(issuable_assigned, john_doe)
+
+ should_create_todo(user: john_doe, target: issuable_assigned, author: john_doe, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo for guests' do
+ service.reassigned_issuable(issuable_assigned, author)
+ should_not_create_todo(user: guest, target: issuable_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'does not create a directly addressed todo for guests' do
+ service.reassigned_issuable(addressed_issuable_assigned, author)
+ should_not_create_todo(user: guest, target: addressed_issuable_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
+ end
+
+ context 'issuable is a merge request' do
+ it_behaves_like 'reassigned issuable' do
+ let(:issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:issuable_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) }
+ end
+ end
+
+ context 'issuable is an issue' do
+ it_behaves_like 'reassigned issuable' do
+ let(:issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:issuable_unassigned) { create(:issue, project: project, author: author, assignees: []) }
+ end
+ end
+ end
+
describe 'Merge Requests' do
- let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
- let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
- let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
+ let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) }
describe '#new_merge_request' do
it 'creates a pending todo if assigned' do
@@ -659,38 +687,6 @@ describe TodoService do
end
end
- describe '#reassigned_merge_request' do
- it 'creates a pending todo for new assignee' do
- mr_unassigned.update_attribute(:assignee, john_doe)
- service.reassigned_merge_request(mr_unassigned, author)
-
- should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo if unassigned' do
- mr_assigned.update_attribute(:assignee, nil)
-
- should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) }
- end
-
- it 'creates a todo if new assignee is the current user' do
- mr_assigned.update_attribute(:assignee, john_doe)
- service.reassigned_merge_request(mr_assigned, john_doe)
-
- should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo for guests' do
- service.reassigned_merge_request(mr_assigned, author)
- should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
- end
-
- it 'does not create a directly addressed todo for guests' do
- service.reassigned_merge_request(addressed_mr_assigned, author)
- should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
- end
- end
-
describe '#merge_merge_request' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author)
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 83f1495a1c6..450e76d5f58 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -78,7 +78,7 @@ describe Users::DestroyService do
end
context "for an merge request the user was assigned to" do
- let!(:merge_request) { create(:merge_request, source_project: project, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
before do
service.execute(user)
@@ -91,7 +91,7 @@ describe Users::DestroyService do
it 'migrates the merge request so that it is "Unassigned"' do
migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
- expect(migrated_merge_request.assignee).to be_nil
+ expect(migrated_merge_request.assignees).to be_empty
end
end
end
diff --git a/spec/support/external_authorization_service_helpers.rb b/spec/support/external_authorization_service_helpers.rb
new file mode 100644
index 00000000000..79dd9a3d58e
--- /dev/null
+++ b/spec/support/external_authorization_service_helpers.rb
@@ -0,0 +1,33 @@
+module ExternalAuthorizationServiceHelpers
+ def enable_external_authorization_service_check
+ stub_application_setting(external_authorization_service_enabled: true)
+
+ stub_application_setting(external_authorization_service_url: 'https://authorize.me')
+ stub_application_setting(external_authorization_service_default_label: 'default_label')
+ stub_request(:post, "https://authorize.me").to_return(status: 200)
+ end
+
+ def external_service_set_access(allowed, user, project)
+ enable_external_authorization_service_check
+ classification_label = ::Gitlab::CurrentSettings.current_application_settings
+ .external_authorization_service_default_label
+
+ # Reload the project so cached licensed features are reloaded
+ if project
+ classification_label = Project.find(project.id).external_authorization_classification_label
+ end
+
+ allow(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?)
+ .with(user, classification_label, any_args)
+ .and_return(allowed)
+ end
+
+ def external_service_allow_access(user, project = nil)
+ external_service_set_access(true, user, project)
+ end
+
+ def external_service_deny_access(user, project = nil)
+ external_service_set_access(false, user, project)
+ end
+end
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
index 4df80b4168a..ab6687f1d07 100644
--- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -46,9 +46,9 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
end
- let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
- let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
- let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
+ let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
+ let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') }
+ let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') }
diff --git a/spec/support/shared_contexts/merge_request_create.rb b/spec/support/shared_contexts/merge_request_create.rb
new file mode 100644
index 00000000000..529f481c2b6
--- /dev/null
+++ b/spec/support/shared_contexts/merge_request_create.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+shared_context 'merge request create context' do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { target_project }
+ let!(:milestone) { create(:milestone, project: target_project) }
+ let!(:label) { create(:label, project: target_project) }
+ let!(:label2) { create(:label, project: target_project) }
+
+ before do
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
+
+ sign_in(user)
+ visit project_new_merge_request_path(target_project,
+ merge_request: {
+ source_project_id: source_project.id,
+ target_project_id: target_project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+end
diff --git a/spec/support/shared_contexts/merge_request_edit.rb b/spec/support/shared_contexts/merge_request_edit.rb
new file mode 100644
index 00000000000..c84510ff47d
--- /dev/null
+++ b/spec/support/shared_contexts/merge_request_edit.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+shared_context 'merge request edit context' do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:milestone) { create(:milestone, project: target_project) }
+ let!(:label) { create(:label, project: target_project) }
+ let!(:label2) { create(:label, project: target_project) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { target_project }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project,
+ target_project: target_project,
+ source_branch: 'fix',
+ target_branch: 'master')
+ end
+
+ before do
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
+
+ sign_in(user)
+ visit edit_project_merge_request_path(target_project, merge_request)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb
new file mode 100644
index 00000000000..8dd78fd0a25
--- /dev/null
+++ b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+shared_examples 'disabled when using an external authorization service' do
+ include ExternalAuthorizationServiceHelpers
+
+ it 'works when the feature is not enabled' do
+ subject
+
+ expect(response).to be_success
+ end
+
+ it 'renders a 404 with a message when the feature is enabled' do
+ enable_external_authorization_service_check
+
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+end
+
+shared_examples 'unauthorized when external service denies access' do
+ include ExternalAuthorizationServiceHelpers
+
+ it 'allows access when the authorization service allows it' do
+ external_service_allow_access(user, project)
+
+ subject
+
+ # Account for redirects after updates
+ expect(response.status).to be_between(200, 302)
+ end
+
+ it 'allows access when the authorization service denies it' do
+ external_service_deny_access(user, project)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 7038a366144..ec1b1754cf0 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -1,42 +1,17 @@
RSpec.shared_examples 'a creatable merge request' do
include WaitForRequests
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:target_project) { create(:project, :public, :repository) }
- let(:source_project) { target_project }
- let!(:milestone) { create(:milestone, project: target_project) }
- let!(:label) { create(:label, project: target_project) }
- let!(:label2) { create(:label, project: target_project) }
-
- before do
- source_project.add_maintainer(user)
- target_project.add_maintainer(user)
- target_project.add_maintainer(user2)
-
- sign_in(user)
- visit project_new_merge_request_path(
- target_project,
- merge_request: {
- source_project_id: source_project.id,
- target_project_id: target_project.id,
- source_branch: 'fix',
- target_branch: 'master'
- })
- end
-
it 'creates new merge request', :js do
- click_button 'Assignee'
+ find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
click_link user2.name
end
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user2.name
end
-
click_link 'Assign to me'
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index eef0327c9a6..a6121fcc50a 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -1,34 +1,10 @@
RSpec.shared_examples 'an editable merge request' do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:milestone) { create(:milestone, project: target_project) }
- let!(:label) { create(:label, project: target_project) }
- let!(:label2) { create(:label, project: target_project) }
- let(:target_project) { create(:project, :public, :repository) }
- let(:source_project) { target_project }
- let(:merge_request) do
- create(:merge_request,
- source_project: source_project,
- target_project: target_project,
- source_branch: 'fix',
- target_branch: 'master')
- end
-
- before do
- source_project.add_maintainer(user)
- target_project.add_maintainer(user)
- target_project.add_maintainer(user2)
-
- sign_in(user)
- visit edit_project_merge_request_path(target_project, merge_request)
- end
-
it 'updates merge request', :js do
- click_button 'Assignee'
+ find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
click_link user.name
end
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
new file mode 100644
index 00000000000..bab7963f06f
--- /dev/null
+++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+shared_examples 'multiple assignees merge request' do |action, save_button_title|
+ it "#{action} a MR with multiple assignees", :js do
+ find('.js-assignee-search').click
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ click_link user2.name
+ end
+
+ # Extra click needed in order to toggle the dropdown
+ find('.js-assignee-search').click
+
+ expect(all('input[name="merge_request[assignee_ids][]"]', visible: false).map(&:value))
+ .to match_array([user.id.to_s, user2.id.to_s])
+
+ page.within '.js-assignee-search' do
+ expect(page).to have_content "#{user2.name} + 1 more"
+ end
+
+ click_button save_button_title
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content '2 Assignees'
+
+ click_link 'Edit'
+
+ expect(page).to have_content user.name
+ expect(page).to have_content user2.name
+ end
+ end
+
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ # Closing dropdown to persist
+ click_link 'Edit'
+
+ expect(page).to have_content user2.name
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/finders/assignees_filter_spec.rb b/spec/support/shared_examples/finders/assignees_filter_spec.rb
new file mode 100644
index 00000000000..782a2d97746
--- /dev/null
+++ b/spec/support/shared_examples/finders/assignees_filter_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+shared_examples 'assignee ID filter' do
+ it 'returns issuables assigned to that user' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'assignee username filter' do
+ it 'returns issuables assigned to those users' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'no assignee filter' do
+ let(:params) { { assignee_id: 'None' } }
+
+ it 'returns issuables not assigned to any assignee' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables not assigned to any assignee' do
+ params[:assignee_id] = 0
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables not assigned to any assignee' do
+ params[:assignee_id] = 'none'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'any assignee filter' do
+ context '' do
+ let(:params) { { assignee_id: 'Any' } }
+
+ it 'returns issuables assigned to any assignee' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables assigned to any assignee' do
+ params[:assignee_id] = 'any'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb
new file mode 100644
index 00000000000..d7e17cc0b70
--- /dev/null
+++ b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+shared_examples 'a finder with external authorization service' do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'finds the subject' do
+ expect(described_class.new(user).execute).to include(subject)
+ end
+
+ context 'with an external authorization service' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not include the subject when no project was given' do
+ expect(described_class.new(user).execute).not_to include(subject)
+ end
+
+ it 'includes the subject when a project id was given' do
+ expect(described_class.new(user, project_params).execute).to include(subject)
+ end
+ end
+end
diff --git a/spec/validators/x509_certificate_credentials_validator_spec.rb b/spec/validators/x509_certificate_credentials_validator_spec.rb
new file mode 100644
index 00000000000..24ef68c1fab
--- /dev/null
+++ b/spec/validators/x509_certificate_credentials_validator_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe X509CertificateCredentialsValidator do
+ let(:certificate_data) { File.read('spec/fixtures/x509_certificate.crt') }
+ let(:pkey_data) { File.read('spec/fixtures/x509_certificate_pk.key') }
+
+ let(:validatable) do
+ Class.new do
+ include ActiveModel::Validations
+
+ attr_accessor :certificate, :private_key, :passphrase
+
+ def initialize(certificate, private_key, passphrase = nil)
+ @certificate, @private_key, @passphrase = certificate, private_key, passphrase
+ end
+ end
+ end
+
+ subject(:validator) do
+ described_class.new(certificate: :certificate, pkey: :private_key)
+ end
+
+ it 'is not valid when the certificate is not valid' do
+ record = validatable.new('not a certificate', nil)
+
+ validator.validate(record)
+
+ expect(record.errors[:certificate]).to include('is not a valid X509 certificate.')
+ end
+
+ it 'is not valid without a certificate' do
+ record = validatable.new(nil, nil)
+
+ validator.validate(record)
+
+ expect(record.errors[:certificate]).not_to be_empty
+ end
+
+ context 'when a valid certificate is passed' do
+ let(:record) { validatable.new(certificate_data, nil) }
+
+ it 'does not track an error for the certificate' do
+ validator.validate(record)
+
+ expect(record.errors[:certificate]).to be_empty
+ end
+
+ it 'adds an error when not passing a correct private key' do
+ validator.validate(record)
+
+ expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?')
+ end
+
+ it 'has no error when the private key is correct' do
+ record.private_key = pkey_data
+
+ validator.validate(record)
+
+ expect(record.errors).to be_empty
+ end
+ end
+
+ context 'when using a passphrase' do
+ let(:passphrase_certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') }
+ let(:passphrase_pkey_data) { File.read('spec/fixtures/passphrase_x509_certificate_pk.key') }
+
+ let(:record) { validatable.new(passphrase_certificate_data, passphrase_pkey_data, '5iveL!fe') }
+
+ subject(:validator) do
+ described_class.new(certificate: :certificate, pkey: :private_key, pass: :passphrase)
+ end
+
+ it 'is valid with the correct data' do
+ validator.validate(record)
+
+ expect(record.errors).to be_empty
+ end
+
+ it 'adds an error when the passphrase is wrong' do
+ record.passphrase = 'wrong'
+
+ validator.validate(record)
+
+ expect(record.errors[:private_key]).not_to be_empty
+ end
+ end
+end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index c13eab30054..529afa03f9c 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -17,7 +17,7 @@ describe 'projects/merge_requests/edit.html.haml' do
source_project: forked_project,
target_project: project,
author: user,
- assignee: user,
+ assignees: [user],
milestone: milestone)
end
@@ -40,7 +40,7 @@ describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
- expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
end
@@ -52,7 +52,7 @@ describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
- expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index d9bda1a3414..23cb319a202 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -53,19 +53,6 @@ describe 'projects/merge_requests/show.html.haml' do
expect(rendered).not_to have_css('.cannot-be-merged')
end
end
-
- context 'when assignee is not allowed to merge' do
- it 'shows a warning icon' do
- reporter = create(:user)
- project.add_reporter(reporter)
- closed_merge_request.update(assignee_id: reporter.id)
- assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
-
- render
-
- expect(rendered).to have_css('.cannot-be-merged')
- end
- end
end
context 'when the merge request is closed' do