Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-10 18:12:05 +00:00
parent 0a412bceb9
commit 2e2c1a521c
34 changed files with 581 additions and 165 deletions

View File

@ -199,6 +199,7 @@ Style/FormatString:
- 'ee/app/components/namespaces/free_user_cap/personal_usage_quota_limitations_alert_component.rb' - 'ee/app/components/namespaces/free_user_cap/personal_usage_quota_limitations_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/preview_alert_component.rb' - 'ee/app/components/namespaces/free_user_cap/preview_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb' - 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/usage_quota_trial_alert_component.rb'
- 'ee/app/controllers/admin/elasticsearch_controller.rb' - 'ee/app/controllers/admin/elasticsearch_controller.rb'
- 'ee/app/controllers/admin/geo/application_controller.rb' - 'ee/app/controllers/admin/geo/application_controller.rb'
- 'ee/app/controllers/admin/geo/projects_controller.rb' - 'ee/app/controllers/admin/geo/projects_controller.rb'

View File

@ -470,7 +470,7 @@ Style/IfUnlessModifier:
- 'db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb' - 'db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb'
- 'db/post_migrate/20220131000001_schedule_trace_expiry_removal.rb' - 'db/post_migrate/20220131000001_schedule_trace_expiry_removal.rb'
- 'db/post_migrate/20220523171107_drop_deploy_tokens_token_column.rb' - 'db/post_migrate/20220523171107_drop_deploy_tokens_token_column.rb'
- 'ee/app/components/namespaces/storage/limit_alert.rb' - 'ee/app/components/namespaces/storage/limit_alert_component.rb'
- 'ee/app/controllers/admin/elasticsearch_controller.rb' - 'ee/app/controllers/admin/elasticsearch_controller.rb'
- 'ee/app/controllers/admin/emails_controller.rb' - 'ee/app/controllers/admin/emails_controller.rb'
- 'ee/app/controllers/admin/geo/application_controller.rb' - 'ee/app/controllers/admin/geo/application_controller.rb'

View File

@ -179,6 +179,7 @@ export default {
> >
<gl-avatar <gl-avatar
:shape="$options.AVATAR_SHAPE_OPTION_RECT" :shape="$options.AVATAR_SHAPE_OPTION_RECT"
:entity-id="group.id"
:entity-name="group.name" :entity-name="group.name"
:src="group.avatarUrl" :src="group.avatarUrl"
:alt="group.name" :alt="group.name"

View File

@ -73,7 +73,9 @@ export default {
<template> <template>
<div ref="milestoneDetails" class="issue-milestone-details"> <div ref="milestoneDetails" class="issue-milestone-details">
<gl-icon :size="16" class="gl-mr-2 flex-shrink-0" name="clock" /> <gl-icon :size="16" class="gl-mr-2 flex-shrink-0" name="clock" />
<span class="milestone-title d-inline-block">{{ milestone.title }}</span> <span class="milestone-title gl-display-inline-block gl-text-truncate">{{
milestone.title
}}</span>
<gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone"> <gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
<span class="bold">{{ __('Milestone') }}</span> <br /> <span class="bold">{{ __('Milestone') }}</span> <br />
<span>{{ milestone.title }}</span> <br /> <span>{{ milestone.title }}</span> <br />

View File

@ -1,15 +1,25 @@
<script> <script>
import { GlPopover, GlSkeletonLoader } from '@gitlab/ui'; import { GlIcon, GlPopover, GlSkeletonLoader, GlTooltipDirective } from '@gitlab/ui';
import query from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import StatusBox from '~/issuable/components/status_box.vue'; import StatusBox from '~/issuable/components/status_box.vue';
import { IssuableStatus } from '~/issues/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import query from '../queries/issue.query.graphql';
export default { export default {
components: { components: {
GlIcon,
GlPopover, GlPopover,
GlSkeletonLoader, GlSkeletonLoader,
IssueDueDate,
IssueMilestone,
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
StatusBox, StatusBox,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
target: { target: {
@ -44,6 +54,9 @@ export default {
showDetails() { showDetails() {
return Object.keys(this.issue).length > 0; return Object.keys(this.issue).length > 0;
}, },
isIssueClosed() {
return this.issue?.state === IssuableStatus.Closed;
},
}, },
apollo: { apollo: {
issue: { issue: {
@ -69,6 +82,14 @@ export default {
</gl-skeleton-loader> </gl-skeleton-loader>
<div v-else-if="showDetails" class="gl-display-flex gl-align-items-center"> <div v-else-if="showDetails" class="gl-display-flex gl-align-items-center">
<status-box issuable-type="issue" :initial-state="issue.state" /> <status-box issuable-type="issue" :initial-state="issue.state" />
<gl-icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
:title="__('Confidential')"
class="gl-text-orange-500 gl-mr-2"
:aria-label="__('Confidential')"
/>
<span class="gl-text-secondary"> <span class="gl-text-secondary">
{{ __('Opened') }} <time :datetime="issue.createdAt">{{ formattedTime }}</time> {{ __('Opened') }} <time :datetime="issue.createdAt">{{ formattedTime }}</time>
</span> </span>
@ -79,5 +100,27 @@ export default {
{{ `${projectPath}#${iid}` }} {{ `${projectPath}#${iid}` }}
</div> </div>
<!-- eslint-enable @gitlab/vue-require-i18n-strings --> <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
<div v-if="!$apollo.queries.issue.loading" class="gl-display-flex gl-text-secondary gl-mt-2">
<issue-due-date
v-if="issue.dueDate"
:date="issue.dueDate.toString()"
:closed="isIssueClosed"
tooltip-placement="top"
class="gl-mr-4"
css-class="gl-display-flex gl-white-space-nowrap"
/>
<issue-weight
v-if="issue.weight"
:weight="issue.weight"
tag-name="span"
class="gl-display-flex gl-mr-4"
/>
<issue-milestone
v-if="issue.milestone"
:milestone="issue.milestone"
class="gl-display-flex gl-overflow-hidden"
/>
</div>
</gl-popover> </gl-popover>
</template> </template>

View File

@ -6,6 +6,14 @@ query issue($projectPath: ID!, $iid: String!) {
title title
createdAt createdAt
state state
confidential
dueDate
milestone {
id
title
startDate
dueDate
}
} }
} }
} }

View File

@ -37,7 +37,7 @@ $avatar-sizes: (
), ),
60: ( 60: (
font-size: 32px, font-size: 32px,
line-height: 58px, line-height: 60px,
border-radius: $border-radius-large border-radius: $border-radius-large
), ),
64: ( 64: (
@ -47,7 +47,7 @@ $avatar-sizes: (
), ),
90: ( 90: (
font-size: 36px, font-size: 36px,
line-height: 88px, line-height: 90px,
border-radius: $border-radius-large border-radius: $border-radius-large
), ),
96: ( 96: (
@ -72,7 +72,6 @@ $avatar-sizes: (
float: left; float: left;
margin-right: $gl-padding; margin-right: $gl-padding;
border-radius: $avatar-radius; border-radius: $avatar-radius;
border: 1px solid $t-gray-a-08;
@each $size, $size-config in $avatar-sizes { @each $size, $size-config in $avatar-sizes {
&.s#{$size} { &.s#{$size} {
@ -83,13 +82,12 @@ $avatar-sizes: (
.avatar { .avatar {
transition-property: none; transition-property: none;
width: 40px; width: 40px;
height: 40px; height: 40px;
padding: 0; padding: 0;
background: $gray-lightest; background: $gray-lightest;
overflow: hidden; overflow: hidden;
border-color: rgba($black, $gl-avatar-border-opacity); box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
&.avatar-inline { &.avatar-inline {
float: none; float: none;
@ -180,6 +178,10 @@ $avatar-sizes: (
@each $size, $size-config in $avatar-sizes { @each $size, $size-config in $avatar-sizes {
&.s#{$size} { &.s#{$size} {
border-radius: map-get($size-config, 'border-radius'); border-radius: map-get($size-config, 'border-radius');
.avatar {
border-radius: map-get($size-config, 'border-radius');
}
} }
} }
} }

View File

@ -134,16 +134,6 @@
.avatar-container { .avatar-container {
@include gl-font-weight-normal; @include gl-font-weight-normal;
flex: none; flex: none;
box-shadow: $avatar-box-shadow;
&.rect-avatar {
@include gl-border-none;
.avatar.s32 {
border-radius: $border-radius-default;
box-shadow: $avatar-box-shadow;
}
}
} }
} }

View File

@ -25,7 +25,7 @@
} }
.avatar-container { .avatar-container {
flex: 0 0 40px; flex: 0 0 32px;
background-color: $white; background-color: $white;
} }

View File

@ -1043,7 +1043,7 @@ kbd {
text-align: left; text-align: left;
} }
.context-header .avatar-container { .context-header .avatar-container {
flex: 0 0 40px; flex: 0 0 32px;
background-color: #333; background-color: #333;
} }
.context-header .sidebar-context-title { .context-header .sidebar-context-title {
@ -1376,18 +1376,6 @@ kbd {
.nav-sidebar-inner-scroll > div.context-header a .avatar-container { .nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400; font-weight: 400;
flex: none; flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container.rect-avatar {
border-style: none;
}
.nav-sidebar-inner-scroll
> div.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
} }
.sidebar-top-level-items { .sidebar-top-level-items {
margin-bottom: 60px; margin-bottom: 60px;
@ -1400,18 +1388,6 @@ kbd {
.sidebar-top-level-items .context-header a .avatar-container { .sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400; font-weight: 400;
flex: none; flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items .context-header a .avatar-container.rect-avatar {
border-style: none;
}
.sidebar-top-level-items
.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
} }
.sidebar-top-level-items .sidebar-top-level-items
> li.active > li.active
@ -1628,7 +1604,6 @@ svg.s16 {
float: left; float: left;
margin-right: 16px; margin-right: 16px;
border-radius: 50%; border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.08);
} }
.avatar.s16, .avatar.s16,
.avatar-container.s16 { .avatar-container.s16 {
@ -1649,7 +1624,7 @@ svg.s16 {
padding: 0; padding: 0;
background: #222; background: #222;
overflow: hidden; overflow: hidden;
border-color: rgba(255, 255, 255, 0.1); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
} }
.avatar.avatar-tile { .avatar.avatar-tile {
border-radius: 0; border-radius: 0;
@ -1714,9 +1689,15 @@ svg.s16 {
.rect-avatar.s16 { .rect-avatar.s16 {
border-radius: 2px; border-radius: 2px;
} }
.rect-avatar.s16 .avatar {
border-radius: 2px;
}
.rect-avatar.s32 { .rect-avatar.s32 {
border-radius: 4px; border-radius: 4px;
} }
.rect-avatar.s32 .avatar {
border-radius: 4px;
}
:root { :root {
color-scheme: dark; color-scheme: dark;
} }
@ -1817,6 +1798,10 @@ body.gl-dark {
background-color: #262626; background-color: #262626;
border-right: 1px solid #303030; border-right: 1px solid #303030;
} }
.avatar-container,
.avatar {
background: rgba(255, 255, 255, 0.04);
}
.nav-sidebar li a { .nav-sidebar li a {
color: var(--gray-600); color: var(--gray-600);
} }

View File

@ -1022,7 +1022,7 @@ kbd {
text-align: left; text-align: left;
} }
.context-header .avatar-container { .context-header .avatar-container {
flex: 0 0 40px; flex: 0 0 32px;
background-color: #fff; background-color: #fff;
} }
.context-header .sidebar-context-title { .context-header .sidebar-context-title {
@ -1355,18 +1355,6 @@ kbd {
.nav-sidebar-inner-scroll > div.context-header a .avatar-container { .nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400; font-weight: 400;
flex: none; flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container.rect-avatar {
border-style: none;
}
.nav-sidebar-inner-scroll
> div.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
} }
.sidebar-top-level-items { .sidebar-top-level-items {
margin-bottom: 60px; margin-bottom: 60px;
@ -1379,18 +1367,6 @@ kbd {
.sidebar-top-level-items .context-header a .avatar-container { .sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400; font-weight: 400;
flex: none; flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items .context-header a .avatar-container.rect-avatar {
border-style: none;
}
.sidebar-top-level-items
.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
} }
.sidebar-top-level-items .sidebar-top-level-items
> li.active > li.active
@ -1607,7 +1583,6 @@ svg.s16 {
float: left; float: left;
margin-right: 16px; margin-right: 16px;
border-radius: 50%; border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.08);
} }
.avatar.s16, .avatar.s16,
.avatar-container.s16 { .avatar-container.s16 {
@ -1628,7 +1603,7 @@ svg.s16 {
padding: 0; padding: 0;
background: #fdfdfd; background: #fdfdfd;
overflow: hidden; overflow: hidden;
border-color: rgba(0, 0, 0, 0.1); box-shadow: inset 0 0 0 1px rgba(31, 31, 31, 0.1);
} }
.avatar.avatar-tile { .avatar.avatar-tile {
border-radius: 0; border-radius: 0;
@ -1693,9 +1668,15 @@ svg.s16 {
.rect-avatar.s16 { .rect-avatar.s16 {
border-radius: 2px; border-radius: 2px;
} }
.rect-avatar.s16 .avatar {
border-radius: 2px;
}
.rect-avatar.s32 { .rect-avatar.s32 {
border-radius: 4px; border-radius: 4px;
} }
.rect-avatar.s32 .avatar {
border-radius: 4px;
}
.tab-width-8 { .tab-width-8 {
tab-size: 8; tab-size: 8;

View File

@ -48,6 +48,17 @@
border-right: 1px solid $gray-50; border-right: 1px solid $gray-50;
} }
.gl-avatar:not(.gl-avatar-identicon),
.avatar-container,
.avatar {
background: rgba($gray-950, 0.04);
}
.gl-avatar {
@include gl-border-none;
box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
}
.nav-sidebar { .nav-sidebar {
li { li {
a { a {

View File

@ -4,14 +4,10 @@ module Integrations
module BaseDataFields module BaseDataFields
extend ActiveSupport::Concern extend ActiveSupport::Concern
LEGACY_FOREIGN_KEY_NAME = %w(
Integrations::IssueTrackerData
).freeze
included do included do
# TODO: Once we rename the tables we can't rely on `table_name` anymore. # TODO: Once we rename the tables we can't rely on `table_name` anymore.
# https://gitlab.com/gitlab-org/gitlab/-/issues/331953 # https://gitlab.com/gitlab-org/gitlab/-/issues/331953
belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: foreign_key_name belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: :integration_id
validates :integration, presence: true validates :integration, presence: true
end end
@ -25,16 +21,6 @@ module Integrations
algorithm: 'aes-256-gcm' algorithm: 'aes-256-gcm'
} }
end end
private
# Older data field models use the `service_id` foreign key for the
# integration association.
def foreign_key_name
return :service_id if self.name.in?(LEGACY_FOREIGN_KEY_NAME)
:integration_id
end
end end
def activated? def activated?

View File

@ -44,7 +44,7 @@ module Integrations
end end
included do included do
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData' has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::IssueTrackerData'
has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::JiraTrackerData' has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::JiraTrackerData'
has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData' has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData'

View File

@ -3,7 +3,7 @@
.context-header .context-header
= link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do = link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
%span{ class: ['avatar-container', 'settings-avatar', 's32'] } %span{ class: ['avatar-container', 'settings-avatar', 's32'] }
= image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' } = image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32', 'gl-rounded-full!'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
%span.sidebar-context-title= _('User Settings') %span.sidebar-context-title= _('User Settings')
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do

View File

@ -1,8 +0,0 @@
---
name: pull_mirror_bulk_branches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93211
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368797
milestone: '15.2'
type: development
group: group::source code
default_enabled: false

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RenameIssueTrackerDataServiceIdToIntegrationId < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
rename_column_concurrently :issue_tracker_data, :service_id, :integration_id
end
def down
undo_rename_column_concurrently :issue_tracker_data, :service_id, :integration_id
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class CleanupIssueTrackerDataServiceId < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :issue_tracker_data, :service_id, :integration_id
end
def down
undo_cleanup_concurrent_column_rename :issue_tracker_data, :service_id, :integration_id
end
end

View File

@ -0,0 +1 @@
4ee9f603c04284cbc0fcb6aa47ecc0f0fe238b4d68083a51f5f170edca19608b

View File

@ -0,0 +1 @@
b4ff0087acba9b91182219ea49a5a7d1bfd5b55391f0174ea62a2bfa14af03ce

View File

@ -16597,7 +16597,6 @@ ALTER SEQUENCE issue_metrics_id_seq OWNED BY issue_metrics.id;
CREATE TABLE issue_tracker_data ( CREATE TABLE issue_tracker_data (
id bigint NOT NULL, id bigint NOT NULL,
service_id integer NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL,
encrypted_project_url character varying, encrypted_project_url character varying,
@ -16605,7 +16604,9 @@ CREATE TABLE issue_tracker_data (
encrypted_issues_url character varying, encrypted_issues_url character varying,
encrypted_issues_url_iv character varying, encrypted_issues_url_iv character varying,
encrypted_new_issue_url character varying, encrypted_new_issue_url character varying,
encrypted_new_issue_url_iv character varying encrypted_new_issue_url_iv character varying,
integration_id integer,
CONSTRAINT check_7ca00cd891 CHECK ((integration_id IS NOT NULL))
); );
CREATE SEQUENCE issue_tracker_data_id_seq CREATE SEQUENCE issue_tracker_data_id_seq
@ -28671,7 +28672,7 @@ CREATE INDEX index_issue_metrics_on_issue_id_and_timestamps ON issue_metrics USI
CREATE INDEX index_issue_on_project_id_state_id_and_blocking_issues_count ON issues USING btree (project_id, state_id, blocking_issues_count); CREATE INDEX index_issue_on_project_id_state_id_and_blocking_issues_count ON issues USING btree (project_id, state_id, blocking_issues_count);
CREATE INDEX index_issue_tracker_data_on_service_id ON issue_tracker_data USING btree (service_id); CREATE INDEX index_issue_tracker_data_on_integration_id ON issue_tracker_data USING btree (integration_id);
CREATE UNIQUE INDEX index_issue_user_mentions_on_note_id ON issue_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL); CREATE UNIQUE INDEX index_issue_user_mentions_on_note_id ON issue_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
@ -32039,6 +32040,9 @@ ALTER TABLE ONLY approvals
ALTER TABLE ONLY namespaces ALTER TABLE ONLY namespaces
ADD CONSTRAINT fk_319256d87a FOREIGN KEY (file_template_project_id) REFERENCES projects(id) ON DELETE SET NULL; ADD CONSTRAINT fk_319256d87a FOREIGN KEY (file_template_project_id) REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE ONLY issue_tracker_data
ADD CONSTRAINT fk_33921c0ee1 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE;
ALTER TABLE ONLY namespaces ALTER TABLE ONLY namespaces
ADD CONSTRAINT fk_3448c97865 FOREIGN KEY (push_rule_id) REFERENCES push_rules(id) ON DELETE SET NULL; ADD CONSTRAINT fk_3448c97865 FOREIGN KEY (push_rule_id) REFERENCES push_rules(id) ON DELETE SET NULL;
@ -34055,9 +34059,6 @@ ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
ALTER TABLE ONLY operations_strategies_user_lists ALTER TABLE ONLY operations_strategies_user_lists
ADD CONSTRAINT fk_rails_ccb7e4bc0b FOREIGN KEY (user_list_id) REFERENCES operations_user_lists(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_ccb7e4bc0b FOREIGN KEY (user_list_id) REFERENCES operations_user_lists(id) ON DELETE CASCADE;
ALTER TABLE ONLY issue_tracker_data
ADD CONSTRAINT fk_rails_ccc0840427 FOREIGN KEY (service_id) REFERENCES integrations(id) ON DELETE CASCADE;
ALTER TABLE ONLY resource_milestone_events ALTER TABLE ONLY resource_milestone_events
ADD CONSTRAINT fk_rails_cedf8cce4d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; ADD CONSTRAINT fk_rails_cedf8cce4d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -1125,12 +1125,6 @@ Geo secondary sites continue to replicate and verify data, and the secondary sit
This bug was [fixed in GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/292983). This bug was [fixed in GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/292983).
### GitLab Pages return 404 errors after promoting
This is due to [Pages data not being managed by Geo](datatypes.md#limitations-on-replicationverification).
Find advice to resolve those error messages in the
[Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node).
### Primary site returns 500 error when accessing `/admin/geo/replication/projects` ### Primary site returns 500 error when accessing `/admin/geo/replication/projects`
Navigating to **Admin > Geo > Replication** (or `/admin/geo/replication/projects`) on a primary Geo site, shows a 500 error, while that same link on the secondary works fine. The primary's `production.log` has a similar entry to the following: Navigating to **Admin > Geo > Replication** (or `/admin/geo/replication/projects`) on a primary Geo site, shows a 500 error, while that same link on the secondary works fine. The primary's `production.log` has a similar entry to the following:

View File

@ -1319,25 +1319,6 @@ and in your Pages log shows this error:
sudo gitlab-ctl restart gitlab-pages sudo gitlab-ctl restart gitlab-pages
``` ```
### 404 error after promoting a Geo secondary to a primary node
Pages files are not among the
[supported data types](../geo/replication/datatypes.md#limitations-on-replicationverification) for replication in Geo. After a secondary node is promoted to a primary node, attempts to access a Pages site result in a `404 Not Found` error.
It is possible to copy the subfolders and files in the [Pages path](#change-storage-path)
to the new primary node to resolve this.
For example, you can adapt the `rsync` strategy from the
[moving repositories documentation](../operations/moving_repositories.md).
Alternatively, run the CI pipelines of those projects that contain a `pages` job again.
### 404 or 500 error when accessing GitLab Pages in a Geo setup
Pages sites are only available on the primary Geo site, while the codebase of the project is available on all sites.
If you try to access a Pages page on a secondary site, a 404 or 500 HTTP code is returned depending on the access control.
Read more which [features don't support Geo replication/verification](../geo/replication/datatypes.md#limitations-on-replicationverification).
### Failed to connect to the internal GitLab API ### Failed to connect to the internal GitLab API
If you see the following error: If you see the following error:

View File

@ -188,6 +188,24 @@ Refer to [`strong_memoize.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/maste
end end
``` ```
Alternatively, use the `strong_memoize_attr` helper to memoize the method for you:
```ruby
class Find
include Gitlab::Utils::StrongMemoize
def result
search
end
strong_memoize_attr :result
strong_memoize_attr :enabled?, :enabled
def enabled?
Feature.enabled?(:some_feature)
end
end
```
- Clear memoization - Clear memoization
```ruby ```ruby

View File

@ -297,9 +297,6 @@ A 404 can also be related to incorrect permissions. If [Pages Access Control](pa
navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site. navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site.
To fix this, verify that the user is a member of the project. To fix this, verify that the user is a member of the project.
For Geo instances, 404 errors on Pages occur after promoting a secondary to a primary.
Find more details in the [Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node)
### Cannot play media content on Safari ### Cannot play media content on Safari
Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6) Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6)

View File

@ -21,6 +21,20 @@ module Gitlab
# end # end
# end # end
# #
# Or like:
#
# include Gitlab::Utils::StrongMemoize
#
# def trigger_from_token
# Ci::Trigger.find_by_token(params[:token].to_s)
# end
# strong_memoize_attr :trigger_from_token
#
# strong_memoize_attr :enabled?, :enabled
# def enabled?
# Feature.enabled?(:some_feature)
# end
#
def strong_memoize(name) def strong_memoize(name)
key = ivar(name) key = ivar(name)
@ -40,6 +54,34 @@ module Gitlab
remove_instance_variable(key) if instance_variable_defined?(key) remove_instance_variable(key) if instance_variable_defined?(key)
end end
module StrongMemoizeClassMethods
def strong_memoize_attr(method_name, member_name = nil)
member_name ||= method_name
if method_defined?(method_name) || private_method_defined?(method_name)
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
:do_strong_memoize, self, method_name, member_name)
else
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
:queue_strong_memoize, self, method_name, member_name)
end
end
def method_added(method_name)
super
if member_name = StrongMemoize
.send(:strong_memoize_queue, self).delete(method_name) # rubocop:disable GitlabSecurity/PublicSend
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
:do_strong_memoize, self, method_name, member_name)
end
end
end
def self.included(base)
base.singleton_class.prepend(StrongMemoizeClassMethods)
end
private private
# Convert `"name"`/`:name` into `:@name` # Convert `"name"`/`:name` into `:@name`
@ -54,6 +96,37 @@ module Gitlab
raise ArgumentError, "Invalid type of '#{name}'" raise ArgumentError, "Invalid type of '#{name}'"
end end
end end
class <<self
private
def strong_memoize_queue(klass)
klass.instance_variable_get(:@strong_memoize_queue) || klass.instance_variable_set(:@strong_memoize_queue, {})
end
def queue_strong_memoize(klass, method_name, member_name)
strong_memoize_queue(klass)[method_name] = member_name
end
def do_strong_memoize(klass, method_name, member_name)
method = klass.instance_method(method_name)
# Methods defined within a class method are already public by default, so we don't need to
# explicitly make them public.
scope = %i(private protected).find do |scope|
klass.send("#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend
.include? method_name
end
klass.define_method(method_name) do |*args, &block|
strong_memoize(member_name) do
method.bind_call(self, *args, &block)
end
end
klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend
end
end
end end
end end
end end

View File

@ -106,3 +106,41 @@ RSpec.describe API::Issues, '(JavaScript fixtures)', type: :request do
expect(response).to be_successful expect(response).to be_successful
end end
end end
RSpec.describe GraphQL::Query, type: :request do
include ApiHelpers
include GraphqlHelpers
include JavaScriptFixturesHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
before_all do
project.add_reporter(user)
end
issue_popover_query_path = 'issuable/popover/queries/issue.query.graphql'
it "graphql/#{issue_popover_query_path}.json" do
query = get_graphql_query_as_string(issue_popover_query_path, ee: Gitlab.ee?)
issue = create(
:issue,
project: project,
confidential: true,
created_at: Time.parse('2020-07-01T04:08:01Z'),
due_date: Date.new(2020, 7, 5),
milestone: create(
:milestone,
project: project,
title: '15.2',
start_date: Date.new(2020, 7, 1),
due_date: Date.new(2020, 7, 30)
)
)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: issue.iid.to_s })
expect_graphql_errors_to_be_empty
end
end

View File

@ -1,34 +1,21 @@
import { GlSkeletonLoader } from '@gitlab/ui'; import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import issueQueryResponse from 'test_fixtures/graphql/issuable/popover/queries/issue.query.graphql.json';
import issueQuery from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import StatusBox from '~/issuable/components/status_box.vue'; import StatusBox from '~/issuable/components/status_box.vue';
import IssuePopover from '~/issuable/popover/components/issue_popover.vue'; import IssuePopover from '~/issuable/popover/components/issue_popover.vue';
import issueQuery from '~/issuable/popover/queries/issue.query.graphql';
describe('Issue Popover', () => { describe('Issue Popover', () => {
let wrapper; let wrapper;
Vue.use(VueApollo); Vue.use(VueApollo);
const issueQueryResponse = {
data: {
project: {
__typename: 'Project',
id: '1',
issue: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/1',
createdAt: '2020-07-01T04:08:01Z',
state: 'opened',
title: 'Issue title',
},
},
},
};
const mountComponent = ({ const mountComponent = ({
queryResponse = jest.fn().mockResolvedValue(issueQueryResponse), queryResponse = jest.fn().mockResolvedValue(issueQueryResponse),
} = {}) => { } = {}) => {
@ -77,5 +64,31 @@ describe('Issue Popover', () => {
it('shows reference', () => { it('shows reference', () => {
expect(wrapper.text()).toContain('foo/bar#1'); expect(wrapper.text()).toContain('foo/bar#1');
}); });
it('shows confidential icon', () => {
const icon = wrapper.findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('eye-slash');
});
it('shows due date', () => {
const component = wrapper.findComponent(IssueDueDate);
expect(component.exists()).toBe(true);
expect(component.props('date')).toBe('2020-07-05');
expect(component.props('closed')).toBe(false);
});
it('shows milestone', () => {
const component = wrapper.findComponent(IssueMilestone);
expect(component.exists()).toBe(true);
expect(component.props('milestone')).toMatchObject({
title: '15.2',
startDate: '2020-07-01',
dueDate: '2020-07-30',
});
});
}); });
}); });

View File

@ -1,10 +1,27 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'fast_spec_helper'
require 'rspec-benchmark'
RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end
RSpec.describe Gitlab::Utils::StrongMemoize do RSpec.describe Gitlab::Utils::StrongMemoize do
let(:klass) do let(:klass) do
struct = Struct.new(:value) do strong_memoize_class = described_class
Struct.new(:value) do
include strong_memoize_class
def self.method_added_list
@method_added_list ||= []
end
def self.method_added(name)
method_added_list << name
end
def method_name def method_name
strong_memoize(:method_name) do strong_memoize(:method_name) do
trace << value trace << value
@ -12,21 +29,56 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
end end
end end
def method_name_attr
trace << value
value
end
strong_memoize_attr :method_name_attr
strong_memoize_attr :different_method_name_attr, :different_member_name_attr
def different_method_name_attr
trace << value
value
end
strong_memoize_attr :enabled?
def enabled?
true
end
def trace def trace
@trace ||= [] @trace ||= []
end end
end
struct.include(described_class) protected
struct
def private_method
end
private :private_method
strong_memoize_attr :private_method
public
def protected_method
end
protected :protected_method
strong_memoize_attr :protected_method
private
def public_method
end
public :public_method
strong_memoize_attr :public_method
end
end end
subject(:object) { klass.new(value) } subject(:object) { klass.new(value) }
shared_examples 'caching the value' do shared_examples 'caching the value' do
it 'only calls the block once' do it 'only calls the block once' do
value0 = object.method_name value0 = object.send(method_name)
value1 = object.method_name value1 = object.send(method_name)
expect(value0).to eq(value) expect(value0).to eq(value)
expect(value1).to eq(value) expect(value1).to eq(value)
@ -34,8 +86,8 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
end end
it 'returns and defines the instance variable for the exact value' do it 'returns and defines the instance variable for the exact value' do
returned_value = object.method_name returned_value = object.send(method_name)
memoized_value = object.instance_variable_get(:@method_name) memoized_value = object.instance_variable_get(:"@#{member_name}")
expect(returned_value).to eql(value) expect(returned_value).to eql(value)
expect(memoized_value).to eql(value) expect(memoized_value).to eql(value)
@ -46,12 +98,19 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
[nil, false, true, 'value', 0, [0]].each do |value| [nil, false, true, 'value', 0, [0]].each do |value|
context "with value #{value}" do context "with value #{value}" do
let(:value) { value } let(:value) { value }
let(:method_name) { :method_name }
let(:member_name) { :method_name }
it_behaves_like 'caching the value' it_behaves_like 'caching the value'
it 'raises exception for invalid key' do it 'raises exception for invalid type as key' do
expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/ expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/
end end
it 'raises exception for invalid characters in key' do
expect { object.strong_memoize(:enabled?) { 20 } }
.to raise_error /is not allowed as an instance variable name/
end
end end
end end
@ -109,4 +168,64 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
expect(object.instance_variable_defined?(:@method_name)).to be(false) expect(object.instance_variable_defined?(:@method_name)).to be(false)
end end
end end
describe '.strong_memoize_attr' do
[nil, false, true, 'value', 0, [0]].each do |value|
let(:value) { value }
context "memoized after method definition with value #{value}" do
let(:method_name) { :method_name_attr }
let(:member_name) { :method_name_attr }
it_behaves_like 'caching the value'
it 'calls the existing .method_added' do
expect(klass.method_added_list).to include(:method_name_attr)
end
end
context "memoized before method definition with different member name and value #{value}" do
let(:method_name) { :different_method_name_attr }
let(:member_name) { :different_member_name_attr }
it_behaves_like 'caching the value'
it 'calls the existing .method_added' do
expect(klass.method_added_list).to include(:different_method_name_attr)
end
end
context 'with valid method name' do
let(:method_name) { :enabled? }
context 'with invalid member name' do
let(:member_name) { :enabled? }
it 'is invalid' do
expect { object.send(method_name) { value } }.to raise_error /is not allowed as an instance variable name/
end
end
end
end
describe 'method visibility' do
it 'sets private visibility' do
expect(klass.private_instance_methods).to include(:private_method)
expect(klass.protected_instance_methods).not_to include(:private_method)
expect(klass.public_instance_methods).not_to include(:private_method)
end
it 'sets protected visibility' do
expect(klass.private_instance_methods).not_to include(:protected_method)
expect(klass.protected_instance_methods).to include(:protected_method)
expect(klass.public_instance_methods).not_to include(:protected_method)
end
it 'sets public visibility' do
expect(klass.private_instance_methods).not_to include(:public_method)
expect(klass.protected_instance_methods).not_to include(:public_method)
expect(klass.public_instance_methods).to include(:public_method)
end
end
end
end end

View File

@ -11,7 +11,7 @@ RSpec.describe Integration do
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to(:project).inverse_of(:integrations) } it { is_expected.to belong_to(:project).inverse_of(:integrations) }
it { is_expected.to belong_to(:group).inverse_of(:integrations) } it { is_expected.to belong_to(:group).inverse_of(:integrations) }
it { is_expected.to have_one(:issue_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:service_id).class_name('Integrations::IssueTrackerData') } it { is_expected.to have_one(:issue_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::IssueTrackerData') }
it { is_expected.to have_one(:jira_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::JiraTrackerData') } it { is_expected.to have_one(:jira_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::JiraTrackerData') }
end end

View File

@ -3,7 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IssuePolicy do RSpec.describe IssuePolicy do
include_context 'ProjectPolicyTable context'
include ExternalAuthorizationServiceHelpers include ExternalAuthorizationServiceHelpers
include ProjectHelpers
let(:guest) { create(:user) } let(:guest) { create(:user) }
let(:author) { create(:user) } let(:author) { create(:user) }
@ -50,6 +52,19 @@ RSpec.describe IssuePolicy do
end end
end end
shared_examples 'grants the expected permissions' do |policy|
specify do
enable_admin_mode!(user) if admin_mode
update_feature_access_level(project, feature_access_level)
if expected_count == 1
expect(permissions(user, issue)).to be_allowed(policy)
else
expect(permissions(user, issue)).to be_disallowed(policy)
end
end
end
context 'a private project' do context 'a private project' do
let(:project) { create(:project, :private) } let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) } let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
@ -85,7 +100,6 @@ RSpec.describe IssuePolicy do
it 'allows reporters from group links to read, update, and admin issues' do it 'allows reporters from group links to read, update, and admin issues' do
expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality) expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality) expect(permissions(reporter_from_group_link, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality)
end end
@ -217,7 +231,7 @@ RSpec.describe IssuePolicy do
it 'allows reporters from group links to read, update, reopen and admin issues' do it 'allows reporters from group links to read, update, reopen and admin issues' do
expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality) expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality) expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:reopen_issue)
expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality) expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue) expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue)
expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality) expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality)
@ -454,7 +468,7 @@ RSpec.describe IssuePolicy do
end end
end end
context 'when peronsal namespace' do context 'when personal namespace' do
let(:project) { create(:project) } let(:project) { create(:project) }
it 'is disallowed' do it 'is disallowed' do
@ -465,4 +479,34 @@ RSpec.describe IssuePolicy do
end end
end end
end end
context 'when user is an inherited member from the group' do
let(:user) { create_user_from_membership(group, membership) }
let(:project) { create(:project, project_level, group: group) }
let(:issue) { create(:issue, project: project) }
context 'and policy allows guest access' do
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
permission_table_for_guest_feature_access
end
with_them do
it_behaves_like 'grants the expected permissions', :read_issue
it_behaves_like 'grants the expected permissions', :read_issue_iid
end
end
context 'and policy allows reporter access' do
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
permission_table_for_reporter_issue_access
end
with_them do
it_behaves_like 'grants the expected permissions', :update_issue
it_behaves_like 'grants the expected permissions', :admin_issue
it_behaves_like 'grants the expected permissions', :set_issue_metadata
it_behaves_like 'grants the expected permissions', :set_confidentiality
end
end
end
end end

View File

@ -9,6 +9,8 @@ RSpec.describe API::Issues do
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE) create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
end end
let_it_be(:group) { create(:group, :public) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
@ -85,6 +87,8 @@ RSpec.describe API::Issues do
end end
before_all do before_all do
group.add_reporter(user)
group.add_guest(guest)
project.add_reporter(user) project.add_reporter(user)
project.add_guest(guest) project.add_guest(guest)
private_mrs_project.add_reporter(user) private_mrs_project.add_reporter(user)
@ -107,6 +111,22 @@ RSpec.describe API::Issues do
end end
end end
shared_examples 'returns project issues without confidential issues for guests' do
specify do
get api(api_url, guest)
expect_paginated_array_response_contain_exactly(open_issue.id, closed_issue.id)
end
end
shared_examples 'returns all project issues for reporters' do
specify do
get api(api_url, user)
expect_paginated_array_response_contain_exactly(open_issue.id, confidential_issue.id, closed_issue.id)
end
end
describe "GET /projects/:id/issues" do describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" } let(:base_url) { "/projects/#{project.id}" }
@ -183,6 +203,30 @@ RSpec.describe API::Issues do
end end
end end
context 'when user is an inherited member from the group' do
let!(:open_issue) { create(:issue, project: group_project) }
let!(:confidential_issue) { create(:issue, :confidential, project: group_project) }
let!(:closed_issue) { create(:issue, state: :closed, project: group_project) }
let!(:api_url) { "/projects/#{group_project.id}/issues" }
context 'and group project is public and issues are private' do
let_it_be(:group_project) do
create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group)
end
it_behaves_like 'returns project issues without confidential issues for guests'
it_behaves_like 'returns all project issues for reporters'
end
context 'and group project is private' do
let_it_be(:group_project) { create(:project, :private, group: group) }
it_behaves_like 'returns project issues without confidential issues for guests'
it_behaves_like 'returns all project issues for reporters'
end
end
it 'avoids N+1 queries' do it 'avoids N+1 queries' do
get api("/projects/#{project.id}/issues", user) get api("/projects/#{project.id}/issues", user)

View File

@ -66,6 +66,13 @@ module ApiHelpers
expect(json_response.map { |item| item['id'] }).to contain_exactly(*items) expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
end end
def expect_paginated_array_response_contain_exactly(*items)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
end
def stub_last_activity_update def stub_last_activity_update
allow_any_instance_of(Users::ActivityService).to receive(:execute) allow_any_instance_of(Users::ActivityService).to receive(:execute)
end end

View File

@ -545,5 +545,62 @@ RSpec.shared_context 'ProjectPolicyTable context' do
:private | :non_member | nil | 0 :private | :non_member | nil | 0
:private | :anonymous | nil | 0 :private | :anonymous | nil | 0
end end
# Based on the permission_table_for_reporter_feature_access table, but for issue
# features where public and internal projects with issues enabled only allow
# access to reporters and above (excluding admins if admin mode is disabled)
#
# project_level, :feature_access_level, :membership, :admin_mode, :expected_count
def permission_table_for_reporter_issue_access
:public | :enabled | :admin | true | 1
:public | :enabled | :admin | false | 0
:public | :enabled | :reporter | nil | 1
:public | :enabled | :guest | nil | 0
:public | :enabled | :non_member | nil | 0
:public | :enabled | :anonymous | nil | 0
:public | :private | :admin | true | 1
:public | :private | :admin | false | 0
:public | :private | :reporter | nil | 1
:public | :private | :guest | nil | 0
:public | :private | :non_member | nil | 0
:public | :private | :anonymous | nil | 0
:public | :disabled | :reporter | nil | 0
:public | :disabled | :guest | nil | 0
:public | :disabled | :non_member | nil | 0
:public | :disabled | :anonymous | nil | 0
:internal | :enabled | :admin | true | 1
:internal | :enabled | :admin | false | 0
:internal | :enabled | :reporter | nil | 1
:internal | :enabled | :guest | nil | 0
:internal | :enabled | :non_member | nil | 0
:internal | :enabled | :anonymous | nil | 0
:internal | :private | :admin | true | 1
:internal | :private | :admin | false | 0
:internal | :private | :reporter | nil | 1
:internal | :private | :guest | nil | 0
:internal | :private | :non_member | nil | 0
:internal | :private | :anonymous | nil | 0
:internal | :disabled | :reporter | nil | 0
:internal | :disabled | :guest | nil | 0
:internal | :disabled | :non_member | nil | 0
:internal | :disabled | :anonymous | nil | 0
:private | :private | :admin | true | 1
:private | :private | :admin | false | 0
:private | :private | :reporter | nil | 1
:private | :private | :guest | nil | 0
:private | :private | :non_member | nil | 0
:private | :private | :anonymous | nil | 0
:private | :disabled | :reporter | nil | 0
:private | :disabled | :guest | nil | 0
:private | :disabled | :non_member | nil | 0
:private | :disabled | :anonymous | nil | 0
end
# rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/AbcSize
end end