Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5e9fe672fa
commit
0510f42da3
38 changed files with 804 additions and 3970 deletions
|
@ -1 +1 @@
|
|||
2ed9a2c78ec556eb8d64e03203c864355ea5a128
|
||||
bb2e3f4a916f031f38c9fb1c4fc955f50f0e4275
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { get } from 'lodash';
|
||||
import { REST, GRAPHQL } from './constants';
|
||||
|
||||
const accessors = {
|
||||
[REST]: {
|
||||
detailsPath: 'details_path',
|
||||
groupId: 'id',
|
||||
hasDetails: 'has_details',
|
||||
pipelineStatus: ['details', 'status'],
|
||||
sourceJob: ['source_job', 'name'],
|
||||
},
|
||||
[GRAPHQL]: {
|
||||
detailsPath: 'detailsPath',
|
||||
groupId: 'name',
|
||||
hasDetails: 'hasDetails',
|
||||
pipelineStatus: 'status',
|
||||
sourceJob: ['sourceJob', 'name'],
|
||||
},
|
||||
};
|
||||
|
||||
const accessValue = (dataMethod, prop, item) => {
|
||||
return get(item, accessors[dataMethod][prop]);
|
||||
};
|
||||
|
||||
export { accessors, accessValue };
|
|
@ -8,9 +8,6 @@ export const UPSTREAM = 'upstream';
|
|||
*/
|
||||
export const ONE_COL_WIDTH = 180;
|
||||
|
||||
export const REST = 'rest';
|
||||
export const GRAPHQL = 'graphql';
|
||||
|
||||
export const STAGE_VIEW = 'stage';
|
||||
export const LAYER_VIEW = 'layer';
|
||||
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
|
||||
|
|
|
@ -7,8 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
|||
import { reportToSentry } from '../../utils';
|
||||
import ActionComponent from '../jobs_shared/action_component.vue';
|
||||
import JobNameComponent from '../jobs_shared/job_name_component.vue';
|
||||
import { accessValue } from './accessors';
|
||||
import { REST, SINGLE_JOB } from './constants';
|
||||
import { SINGLE_JOB } from './constants';
|
||||
|
||||
/**
|
||||
* Renders the badge for the pipeline graph and the job's dropdown.
|
||||
|
@ -47,11 +46,6 @@ export default {
|
|||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [delayedJobMixin],
|
||||
inject: {
|
||||
dataMethod: {
|
||||
default: REST,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
|
@ -111,10 +105,10 @@ export default {
|
|||
return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
|
||||
},
|
||||
detailsPath() {
|
||||
return accessValue(this.dataMethod, 'detailsPath', this.status);
|
||||
return this.status.detailsPath;
|
||||
},
|
||||
hasDetails() {
|
||||
return accessValue(this.dataMethod, 'hasDetails', this.status);
|
||||
return this.status.hasDetails;
|
||||
},
|
||||
isSingleItem() {
|
||||
return this.type === SINGLE_JOB;
|
||||
|
@ -189,7 +183,7 @@ export default {
|
|||
if (this.isSingleItem) {
|
||||
/*
|
||||
This is so the jobDropdown still toggles. Issue to refactor:
|
||||
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
|
||||
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
|
||||
*/
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
|||
import { __, sprintf } from '~/locale';
|
||||
import CiStatus from '~/vue_shared/components/ci_icon.vue';
|
||||
import { reportToSentry } from '../../utils';
|
||||
import { accessValue } from './accessors';
|
||||
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
|
||||
import { DOWNSTREAM, UPSTREAM } from './constants';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
|
@ -18,11 +17,6 @@ export default {
|
|||
GlLoadingIcon,
|
||||
GlBadge,
|
||||
},
|
||||
inject: {
|
||||
dataMethod: {
|
||||
default: REST,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
columnTitle: {
|
||||
type: String,
|
||||
|
@ -40,20 +34,9 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/*
|
||||
The next two props will be removed or required
|
||||
once the graph transition is done.
|
||||
See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
|
||||
*/
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: -1,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -65,7 +48,7 @@ export default {
|
|||
return `js-linked-pipeline-${this.pipeline.id}`;
|
||||
},
|
||||
pipelineStatus() {
|
||||
return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline);
|
||||
return this.pipeline.status;
|
||||
},
|
||||
projectName() {
|
||||
return this.pipeline.project.name;
|
||||
|
@ -97,12 +80,10 @@ export default {
|
|||
return this.type === UPSTREAM;
|
||||
},
|
||||
isSameProject() {
|
||||
return this.projectId > -1
|
||||
? this.projectId === this.pipeline.project.id
|
||||
: !this.pipeline.multiproject;
|
||||
return !this.pipeline.multiproject;
|
||||
},
|
||||
sourceJobName() {
|
||||
return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
|
||||
return this.pipeline.sourceJob?.name ?? '';
|
||||
},
|
||||
sourceJobInfo() {
|
||||
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
|
||||
|
|
|
@ -4,8 +4,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import { reportToSentry } from '../../utils';
|
||||
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
|
||||
import ActionComponent from '../jobs_shared/action_component.vue';
|
||||
import { accessValue } from './accessors';
|
||||
import { GRAPHQL } from './constants';
|
||||
import JobGroupDropdown from './job_group_dropdown.vue';
|
||||
import JobItem from './job_item.vue';
|
||||
|
||||
|
@ -97,7 +95,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
getGroupId(group) {
|
||||
return accessValue(GRAPHQL, 'groupId', group);
|
||||
return group.name;
|
||||
},
|
||||
groupId(group) {
|
||||
return `ci-badge-${escape(group.name)}`;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GRAPHQL } from './components/graph/constants';
|
||||
import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
|
||||
import { reportToSentry } from './utils';
|
||||
|
||||
|
@ -23,7 +22,6 @@ const createPipelinesDetailApp = (
|
|||
pipelineProjectPath,
|
||||
pipelineIid,
|
||||
graphqlResourceEtag,
|
||||
dataMethod: GRAPHQL,
|
||||
},
|
||||
errorCaptured(err, _vm, info) {
|
||||
reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);
|
||||
|
|
60
app/finders/groups/user_groups_finder.rb
Normal file
60
app/finders/groups/user_groups_finder.rb
Normal file
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Groups::UserGroupsFinder
|
||||
#
|
||||
# Used to filter Groups where a user is member
|
||||
#
|
||||
# Arguments:
|
||||
# current_user - user requesting group info on target user
|
||||
# target_user - user for which groups will be found
|
||||
# params:
|
||||
# permissions: string (see Types::Groups::UserPermissionsEnum)
|
||||
# search: string used for search on path and group name
|
||||
#
|
||||
# Initially created to filter user groups and descendants where the user can create projects
|
||||
module Groups
|
||||
class UserGroupsFinder
|
||||
def initialize(current_user, target_user, params = {})
|
||||
@current_user = current_user
|
||||
@target_user = target_user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
return Group.none unless current_user&.can?(:read_user_groups, target_user)
|
||||
return Group.none if target_user.blank?
|
||||
|
||||
items = by_permission_scope
|
||||
items = by_search(items)
|
||||
|
||||
sort(items)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :target_user, :params
|
||||
|
||||
def sort(items)
|
||||
items.order(path: :asc, id: :asc) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def by_search(items)
|
||||
return items if params[:search].blank?
|
||||
|
||||
items.search(params[:search])
|
||||
end
|
||||
|
||||
def by_permission_scope
|
||||
if permission_scope_create_projects?
|
||||
target_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
|
||||
else
|
||||
target_user.groups
|
||||
end
|
||||
end
|
||||
|
||||
def permission_scope_create_projects?
|
||||
params[:permission_scope] == :create_projects &&
|
||||
Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -124,6 +124,16 @@ module Resolvers
|
|||
[args[:iid], args[:iids]].any? ? 0 : 0.01
|
||||
end
|
||||
|
||||
def self.before_connection_authorization(&block)
|
||||
@before_connection_authorization_block = block
|
||||
end
|
||||
|
||||
# rubocop: disable Style/TrivialAccessors
|
||||
def self.before_connection_authorization_block
|
||||
@before_connection_authorization_block
|
||||
end
|
||||
# rubocop: enable Style/TrivialAccessors
|
||||
|
||||
def offset_pagination(relation)
|
||||
::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(relation)
|
||||
end
|
||||
|
|
44
app/graphql/resolvers/users/groups_resolver.rb
Normal file
44
app/graphql/resolvers/users/groups_resolver.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module Users
|
||||
class GroupsResolver < BaseResolver
|
||||
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||
include LooksAhead
|
||||
|
||||
type Types::GroupType.connection_type, null: true
|
||||
|
||||
authorize :read_user_groups
|
||||
authorizes_object!
|
||||
|
||||
argument :search, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Search by group name or path.'
|
||||
argument :permission_scope,
|
||||
::Types::PermissionTypes::GroupEnum,
|
||||
required: false,
|
||||
description: 'Filter by permissions the user has on groups.'
|
||||
|
||||
before_connection_authorization do |nodes, current_user|
|
||||
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
|
||||
end
|
||||
|
||||
def resolve_with_lookahead(**args)
|
||||
return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
|
||||
|
||||
apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preloads
|
||||
{
|
||||
path: [:route],
|
||||
full_path: [:route]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Resolvers::Users::GroupsResolver.prepend_mod_with('Resolvers::Users::GroupsResolver')
|
|
@ -5,7 +5,7 @@ module Types
|
|||
class Group < BasePermissionType
|
||||
graphql_name 'GroupPermissions'
|
||||
|
||||
abilities :read_group
|
||||
abilities :read_group, :create_projects
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
12
app/graphql/types/permission_types/group_enum.rb
Normal file
12
app/graphql/types/permission_types/group_enum.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module PermissionTypes
|
||||
class GroupEnum < BaseEnum
|
||||
graphql_name 'GroupPermission'
|
||||
description 'User permission on groups'
|
||||
|
||||
value 'CREATE_PROJECTS', value: :create_projects, description: 'Groups where the user can create projects.'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -59,6 +59,9 @@ module Types
|
|||
type: Types::GroupMemberType.connection_type,
|
||||
null: true,
|
||||
description: 'Group memberships of the user.'
|
||||
field :groups,
|
||||
resolver: Resolvers::Users::GroupsResolver,
|
||||
description: 'Groups where the user has access.'
|
||||
field :group_count,
|
||||
resolver: Resolvers::Users::GroupCountResolver,
|
||||
description: 'Group count for the user.'
|
||||
|
|
|
@ -174,7 +174,11 @@ module IssuesHelper
|
|||
end
|
||||
|
||||
def issue_header_actions_data(project, issuable, current_user)
|
||||
new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?)
|
||||
new_issuable_params = { issue: { description: _('Related to #%{issue_id}.') % { issue_id: issuable.iid } + "\n\n" } }
|
||||
if issuable.incident?
|
||||
new_issuable_params[:issuable_template] = 'incident'
|
||||
new_issuable_params[:issue][:issue_type] = 'incident'
|
||||
end
|
||||
|
||||
{
|
||||
can_create_issue: show_new_issue_link?(project).to_s,
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Preloaders
|
||||
# This class preloads the max access level (role) for the user within the given groups and
|
||||
# stores the values in requests store.
|
||||
# Will only be able to preload max access level for groups where the user is a direct member
|
||||
class UserMaxAccessLevelInGroupsPreloader
|
||||
include BulkMemberAccessLoad
|
||||
|
||||
def initialize(groups, user)
|
||||
@groups = groups
|
||||
@user = user
|
||||
end
|
||||
|
||||
def execute
|
||||
group_memberships = GroupMember.active_without_invites_and_requests
|
||||
.non_minimal_access
|
||||
.where(user: @user, source_id: @groups)
|
||||
.group(:source_id)
|
||||
.maximum(:access_level)
|
||||
|
||||
group_memberships.each do |group_id, max_access_level|
|
||||
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -25,6 +25,7 @@ class UserPolicy < BasePolicy
|
|||
enable :update_user_status
|
||||
enable :read_user_personal_access_tokens
|
||||
enable :read_group_count
|
||||
enable :read_user_groups
|
||||
end
|
||||
|
||||
rule { default }.enable :read_user_profile
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: paginatable_namespace_drop_down_for_project_creation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66112
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338930
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: false
|
|
@ -10176,6 +10176,7 @@ Represents a Group Membership.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="grouppermissionscreateprojects"></a>`createProjects` | [`Boolean!`](#boolean) | Indicates the user can perform `create_projects` on this resource. |
|
||||
| <a id="grouppermissionsreadgroup"></a>`readGroup` | [`Boolean!`](#boolean) | Indicates the user can perform `read_group` on this resource. |
|
||||
|
||||
### `GroupReleaseStats`
|
||||
|
@ -10910,6 +10911,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="mergerequestassigneeauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
|
||||
| <a id="mergerequestassigneeauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
|
||||
|
||||
##### `MergeRequestAssignee.groups`
|
||||
|
||||
Groups where the user has access.
|
||||
|
||||
Returns [`GroupConnection`](#groupconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#connection-pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mergerequestassigneegroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
|
||||
| <a id="mergerequestassigneegroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
|
||||
|
||||
##### `MergeRequestAssignee.reviewRequestedMergeRequests`
|
||||
|
||||
Merge requests assigned to the user for review.
|
||||
|
@ -11139,6 +11157,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="mergerequestreviewerauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
|
||||
| <a id="mergerequestreviewerauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
|
||||
|
||||
##### `MergeRequestReviewer.groups`
|
||||
|
||||
Groups where the user has access.
|
||||
|
||||
Returns [`GroupConnection`](#groupconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#connection-pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mergerequestreviewergroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
|
||||
| <a id="mergerequestreviewergroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
|
||||
|
||||
##### `MergeRequestReviewer.reviewRequestedMergeRequests`
|
||||
|
||||
Merge requests assigned to the user for review.
|
||||
|
@ -14010,6 +14045,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="usercoreauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
|
||||
| <a id="usercoreauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
|
||||
|
||||
##### `UserCore.groups`
|
||||
|
||||
Groups where the user has access.
|
||||
|
||||
Returns [`GroupConnection`](#groupconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#connection-pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="usercoregroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
|
||||
| <a id="usercoregroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
|
||||
|
||||
##### `UserCore.reviewRequestedMergeRequests`
|
||||
|
||||
Merge requests assigned to the user for review.
|
||||
|
@ -15162,6 +15214,14 @@ Group member relation.
|
|||
| <a id="groupmemberrelationdirect"></a>`DIRECT` | Members in the group itself. |
|
||||
| <a id="groupmemberrelationinherited"></a>`INHERITED` | Members in the group's ancestor groups. |
|
||||
|
||||
### `GroupPermission`
|
||||
|
||||
User permission on groups.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="grouppermissioncreate_projects"></a>`CREATE_PROJECTS` | Groups where the user can create projects. |
|
||||
|
||||
### `HealthStatus`
|
||||
|
||||
Health status of an issue or epic.
|
||||
|
@ -16961,6 +17021,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="userauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
|
||||
| <a id="userauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
|
||||
|
||||
###### `User.groups`
|
||||
|
||||
Groups where the user has access.
|
||||
|
||||
Returns [`GroupConnection`](#groupconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#connection-pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||
|
||||
####### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="usergroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
|
||||
| <a id="usergroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
|
||||
|
||||
###### `User.reviewRequestedMergeRequests`
|
||||
|
||||
Merge requests assigned to the user for review.
|
||||
|
|
|
@ -98,15 +98,15 @@ delete a project. To allow only users with the Administrator role to delete proj
|
|||
|
||||
## Default delayed project deletion **(PREMIUM SELF)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2 for groups created after August 12, 2021.
|
||||
|
||||
Projects in a group (but not a personal namespace) can be deleted after a delayed period, by
|
||||
[configuring in Group Settings](../../group/index.md#enable-delayed-project-removal).
|
||||
Projects in a group (but not a personal namespace) can be deleted after a delayed period.
|
||||
You can [configure it in group settings](../../group/index.md#enable-delayed-project-removal).
|
||||
|
||||
To enable delayed project deletion by default in new groups:
|
||||
|
||||
1. Check the **Default delayed project deletion** checkbox.
|
||||
1. Click **Save changes**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Default deletion delay **(PREMIUM SELF)**
|
||||
|
||||
|
|
|
@ -13,48 +13,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - A simplified version was made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/294076) in GitLab 13.10.
|
||||
> - [Redesigned](https://gitlab.com/gitlab-org/gitlab/-/issues/326926) in 14.2.
|
||||
|
||||
The Security Configuration page displays what security scans are available, links to documentation and also simple enablement tools for the current project.
|
||||
The Security Configuration page lists the following for the security testing and compliance tools:
|
||||
|
||||
To view a project's security configuration, go to the project's home page,
|
||||
then in the left sidebar go to **Security & Compliance > Configuration**.
|
||||
|
||||
For each security control the page displays:
|
||||
|
||||
- Its name, description and a documentation link.
|
||||
- Name, description, and a documentation link.
|
||||
- Whether or not it is available.
|
||||
- A configuration button or a link to its configuration guide.
|
||||
|
||||
## Security testing
|
||||
|
||||
You can configure the following security controls:
|
||||
|
||||
- Auto DevOps
|
||||
- Click **Enable Auto DevOps** on the alert to enable it for the current project. For more details, see [Auto DevOps](../../../topics/autodevops/index.md).
|
||||
- SAST
|
||||
- Click **Enable SAST** to use SAST for the current project. For more details, see [Configure SAST in the UI](../sast/index.md#configure-sast-in-the-ui).
|
||||
- DAST **(ULTIMATE)**
|
||||
- Click **Enable DAST** to use DAST for the current Project. To manage the available DAST profiles used for on-demand scans Click **Manage Scans**. For more details, see [DAST on-demand scans](../dast/index.md#on-demand-scans).
|
||||
- Dependency Scanning **(ULTIMATE)**
|
||||
- Select **Configure via Merge Request** to create a merge request with the changes required to
|
||||
enable Dependency Scanning. For more details, see [Enable Dependency Scanning via an automatic merge request](../dependency_scanning/index.md#enable-dependency-scanning-via-an-automatic-merge-request).
|
||||
|
||||
- Container Scanning **(ULTIMATE)**
|
||||
- Can be configured via `.gitlab-ci.yml`. For more details, see [Container Scanning](../../../user/application_security/container_scanning/index.md#configuration).
|
||||
- Cluster Image Scanning **(ULTIMATE)**
|
||||
- Can be configured via `.gitlab-ci.yml`. For more details, see [Cluster Image Scanning](../../../user/application_security/cluster_image_scanning/#configuration).
|
||||
- Secret Detection
|
||||
- Select **Configure via Merge Request** to create a merge request with the changes required to
|
||||
enable Secret Detection. For more details, see [Enable Secret Detection via an automatic merge request](../secret_detection/index.md#enable-secret-detection-via-an-automatic-merge-request).
|
||||
|
||||
- API Fuzzing **(ULTIMATE)**
|
||||
- Click **Enable API Fuzzing** to use API Fuzzing for the current Project. For more details, see [API Fuzzing](../../../user/application_security/api_fuzzing/index.md#enable-web-api-fuzzing).
|
||||
- Coverage Fuzzing **(ULTIMATE)**
|
||||
- Can be configured via `.gitlab-ci.yml`. For more details, see [Coverage Fuzzing](../../../user/application_security/coverage_fuzzing/index.md#configuration).
|
||||
|
||||
## Status **(ULTIMATE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20711) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.6.
|
||||
|
||||
The status of each security control is determined by the project's latest default branch
|
||||
[CI pipeline](../../../ci/pipelines/index.md).
|
||||
If a job with the expected security report artifact exists in the pipeline, the feature's status is
|
||||
|
@ -63,11 +27,42 @@ _enabled_.
|
|||
If the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md),
|
||||
all security features are configured by default.
|
||||
|
||||
Click **View history** to see the `.gitlab-ci.yml` file's history.
|
||||
To view a project's security configuration:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Security & Compliance > Configuration**.
|
||||
|
||||
Select **Configuration history** to see the `.gitlab-ci.yml` file's history.
|
||||
|
||||
## Security testing
|
||||
|
||||
You can configure the following security controls:
|
||||
|
||||
- Static Application Security Testing (SAST)
|
||||
- Select **Enable SAST** to configure SAST for the current project.
|
||||
For more details, read [Configure SAST in the UI](../sast/index.md#configure-sast-in-the-ui).
|
||||
- Dynamic Application Security Testing (DAST) **(ULTIMATE)**
|
||||
- Select **Enable DAST** to configure DAST for the current project.
|
||||
- Select **Manage scans** to manage the saved DAST scans, site profiles, and scanner profiles.
|
||||
For more details, read [DAST on-demand scans](../dast/index.md#on-demand-scans).
|
||||
- Dependency Scanning **(ULTIMATE)**
|
||||
- Select **Configure via Merge Request** to create a merge request with the changes required to
|
||||
enable Dependency Scanning. For more details, see [Enable Dependency Scanning via an automatic merge request](../dependency_scanning/index.md#enable-dependency-scanning-via-an-automatic-merge-request).
|
||||
- Container Scanning **(ULTIMATE)**
|
||||
- Can be configured with `.gitlab-ci.yml`. For more details, read [Container Scanning](../../../user/application_security/container_scanning/index.md#configuration).
|
||||
- Cluster Image Scanning **(ULTIMATE)**
|
||||
- Can be configured with `.gitlab-ci.yml`. For more details, read [Cluster Image Scanning](../../../user/application_security/cluster_image_scanning/#configuration).
|
||||
- Secret Detection
|
||||
- Select **Configure via Merge Request** to create a merge request with the changes required to
|
||||
enable Secret Detection. For more details, read [Enable Secret Detection via an automatic merge request](../secret_detection/index.md#enable-secret-detection-via-an-automatic-merge-request).
|
||||
- API Fuzzing **(ULTIMATE)**
|
||||
- Select **Enable API Fuzzing** to use API Fuzzing for the current project. For more details, read [API Fuzzing](../../../user/application_security/api_fuzzing/index.md#enable-web-api-fuzzing).
|
||||
- Coverage Fuzzing **(ULTIMATE)**
|
||||
- Can be configured with `.gitlab-ci.yml`. For more details, read [Coverage Fuzzing](../../../user/application_security/coverage_fuzzing/index.md#configuration).
|
||||
|
||||
## Compliance **(ULTIMATE)**
|
||||
|
||||
You can configure the following security controls:
|
||||
|
||||
- License Compliance **(ULTIMATE)**
|
||||
- Can be configured via `.gitlab-ci.yml`. For more details, see [License Compliance](../../../user/compliance/license_compliance/index.md#configuration).
|
||||
- Can be configured with `.gitlab-ci.yml`. For more details, read [License Compliance](../../../user/compliance/license_compliance/index.md#configuration).
|
||||
|
|
|
@ -664,16 +664,16 @@ Projects can be configured to be deleted either:
|
|||
|
||||
- Immediately.
|
||||
- After a delayed interval. During this interval period, the projects are in a read-only state
|
||||
and can be restored, if required. The default interval period is seven days but
|
||||
and can be restored. The default interval period is seven days but
|
||||
[is configurable](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay).
|
||||
|
||||
On:
|
||||
On self-managed GitLab, projects are deleted immediately by default.
|
||||
In GitLab 14.2 and later, an administrator can
|
||||
[change the default setting](../admin_area/settings/visibility_and_access_controls.md#default-delayed-project-deletion)
|
||||
for projects in newly-created groups.
|
||||
|
||||
- GitLab self-managed instances, projects are deleted immediately by default. In GitLab
|
||||
14.2 and later, an administrator can
|
||||
[change the default setting](../admin_area/settings/visibility_and_access_controls.md#default-delayed-project-deletion) for projects in newly-created groups.
|
||||
- GitLab.com, see [GitLab.com settings page](../gitlab_com/index.md#delayed-project-deletion) for
|
||||
the default setting.
|
||||
On GitLab.com, see the [GitLab.com settings page](../gitlab_com/index.md#delayed-project-deletion) for
|
||||
the default setting.
|
||||
|
||||
To enable delayed deletion of projects in a group:
|
||||
|
||||
|
|
|
@ -7,12 +7,14 @@ module Gitlab
|
|||
class Redactor
|
||||
include ::Gitlab::Graphql::Laziness
|
||||
|
||||
def initialize(type, context)
|
||||
def initialize(type, context, resolver)
|
||||
@type = type
|
||||
@context = context
|
||||
@resolver = resolver
|
||||
end
|
||||
|
||||
def redact(nodes)
|
||||
perform_before_authorize_action(nodes)
|
||||
remove_unauthorized(nodes)
|
||||
|
||||
nodes
|
||||
|
@ -29,6 +31,13 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def perform_before_authorize_action(nodes)
|
||||
before_connection_authorization_block = @resolver&.before_connection_authorization_block
|
||||
return unless before_connection_authorization_block.respond_to?(:call)
|
||||
|
||||
before_connection_authorization_block.call(nodes, @context[:current_user])
|
||||
end
|
||||
|
||||
def remove_unauthorized(nodes)
|
||||
nodes
|
||||
.map! { |lazy| force(lazy) }
|
||||
|
@ -49,14 +58,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def redact_connection(conn, context)
|
||||
redactor = Redactor.new(@field.type.unwrap.node_type, context)
|
||||
redactor = Redactor.new(@field.type.unwrap.node_type, context, @field.resolver)
|
||||
return unless redactor.active?
|
||||
|
||||
conn.redactor = redactor if conn.respond_to?(:redactor=)
|
||||
end
|
||||
|
||||
def redact_list(list, context)
|
||||
redactor = Redactor.new(@field.type.unwrap, context)
|
||||
redactor = Redactor.new(@field.type.unwrap, context, @field.resolver)
|
||||
redactor.redact(list) if redactor.active?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27665,6 +27665,9 @@ msgstr ""
|
|||
msgid "Related merge requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "Related to #%{issue_id}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Relates to"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe "User views incident" do
|
|||
it 'shows the merge request and incident actions', :js, :aggregate_failures do
|
||||
click_button 'Incident actions'
|
||||
|
||||
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }))
|
||||
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
|
||||
expect(page).to have_button('Create merge request')
|
||||
expect(page).to have_button('Close incident')
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe "User views issue" do
|
|||
it 'shows the merge request and issue actions', :js, :aggregate_failures do
|
||||
click_button 'Issue actions'
|
||||
|
||||
expect(page).to have_link('New issue', href: new_project_issue_path(project))
|
||||
expect(page).to have_link('New issue', href: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }))
|
||||
expect(page).to have_button('Create merge request')
|
||||
expect(page).to have_button('Close issue')
|
||||
end
|
||||
|
|
106
spec/finders/groups/user_groups_finder_spec.rb
Normal file
106
spec/finders/groups/user_groups_finder_spec.rb
Normal file
|
@ -0,0 +1,106 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Groups::UserGroupsFinder do
|
||||
describe '#execute' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
|
||||
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
|
||||
let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
|
||||
let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
|
||||
|
||||
subject { described_class.new(current_user, target_user, arguments).execute }
|
||||
|
||||
let(:arguments) { {} }
|
||||
let(:current_user) { user }
|
||||
let(:target_user) { user }
|
||||
|
||||
before_all do
|
||||
guest_group.add_guest(user)
|
||||
private_maintainer_group.add_maintainer(user)
|
||||
public_developer_group.add_developer(user)
|
||||
public_maintainer_group.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns all groups where the user is a direct member' do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
guest_group
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
context 'when target_user is nil' do
|
||||
let(:target_user) { nil }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when current_user is nil' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when permission is :create_projects' do
|
||||
let(:arguments) { { permission_scope: :create_projects } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
context 'when paginatable_namespace_drop_down_for_project_creation feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
|
||||
end
|
||||
|
||||
it 'ignores project creation scope and returns all groups where the user is a direct member' do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
guest_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search is provided' do
|
||||
let(:arguments) { { permission_scope: :create_projects, search: 'maintainer' } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search is provided' do
|
||||
let(:arguments) { { search: 'maintainer' } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { GRAPHQL, LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
|
||||
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
|
||||
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
|
||||
import JobItem from '~/pipelines/components/graph/job_item.vue';
|
||||
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
|
||||
|
@ -54,9 +54,6 @@ describe('graph component', () => {
|
|||
...data,
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
dataMethod: GRAPHQL,
|
||||
},
|
||||
stubs: {
|
||||
'links-inner': true,
|
||||
'linked-pipeline': true,
|
||||
|
|
|
@ -14,7 +14,29 @@ describe('pipeline graph job item', () => {
|
|||
};
|
||||
|
||||
const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
|
||||
const delayedJobFixture = getJSONFixture('jobs/delayed.json');
|
||||
|
||||
const delayedJob = {
|
||||
__typename: 'CiJob',
|
||||
name: 'delayed job',
|
||||
scheduledAt: '2015-07-03T10:01:00.000Z',
|
||||
needs: [],
|
||||
status: {
|
||||
__typename: 'DetailedStatus',
|
||||
icon: 'status_scheduled',
|
||||
tooltip: 'delayed manual action (%{remainingTime})',
|
||||
hasDetails: true,
|
||||
detailsPath: '/root/kinder-pipe/-/jobs/5339',
|
||||
group: 'scheduled',
|
||||
action: {
|
||||
__typename: 'StatusAction',
|
||||
icon: 'time-out',
|
||||
title: 'Unschedule',
|
||||
path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule',
|
||||
buttonTitle: 'Unschedule job',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockJob = {
|
||||
id: 4256,
|
||||
name: 'test',
|
||||
|
@ -24,8 +46,8 @@ describe('pipeline graph job item', () => {
|
|||
label: 'passed',
|
||||
tooltip: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4256',
|
||||
has_details: true,
|
||||
detailsPath: '/root/ci-mock/builds/4256',
|
||||
hasDetails: true,
|
||||
action: {
|
||||
icon: 'retry',
|
||||
title: 'Retry',
|
||||
|
@ -42,8 +64,8 @@ describe('pipeline graph job item', () => {
|
|||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4257',
|
||||
has_details: false,
|
||||
detailsPath: '/root/ci-mock/builds/4257',
|
||||
hasDetails: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -58,7 +80,7 @@ describe('pipeline graph job item', () => {
|
|||
wrapper.vm.$nextTick(() => {
|
||||
const link = wrapper.find('a');
|
||||
|
||||
expect(link.attributes('href')).toBe(mockJob.status.details_path);
|
||||
expect(link.attributes('href')).toBe(mockJob.status.detailsPath);
|
||||
|
||||
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
|
||||
|
||||
|
@ -145,7 +167,7 @@ describe('pipeline graph job item', () => {
|
|||
describe('for delayed job', () => {
|
||||
it('displays remaining time in tooltip', () => {
|
||||
createWrapper({
|
||||
job: delayedJobFixture,
|
||||
job: delayedJob,
|
||||
});
|
||||
|
||||
expect(findJobWithLink().attributes('title')).toBe(
|
||||
|
|
|
@ -4,11 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
|||
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
|
||||
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
|
||||
import CiStatus from '~/vue_shared/components/ci_icon.vue';
|
||||
import mockData from './linked_pipelines_mock_data';
|
||||
|
||||
const mockPipeline = mockData.triggered[0];
|
||||
const validTriggeredPipelineId = mockPipeline.project.id;
|
||||
const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
|
||||
import mockPipeline from './linked_pipelines_mock_data';
|
||||
|
||||
describe('Linked pipeline', () => {
|
||||
let wrapper;
|
||||
|
@ -39,10 +35,10 @@ describe('Linked pipeline', () => {
|
|||
describe('rendered output', () => {
|
||||
const props = {
|
||||
pipeline: mockPipeline,
|
||||
projectId: invalidTriggeredPipelineId,
|
||||
columnTitle: 'Downstream',
|
||||
type: DOWNSTREAM,
|
||||
expanded: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -60,7 +56,7 @@ describe('Linked pipeline', () => {
|
|||
});
|
||||
|
||||
it('should render the pipeline status icon svg', () => {
|
||||
expect(wrapper.find('.ci-status-icon-failed svg').exists()).toBe(true);
|
||||
expect(wrapper.find('.ci-status-icon-success svg').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a ci-status child component', () => {
|
||||
|
@ -73,8 +69,8 @@ describe('Linked pipeline', () => {
|
|||
|
||||
it('should correctly compute the tooltip text', () => {
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.status.label);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.sourceJob.name);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
|
||||
});
|
||||
|
||||
|
@ -82,11 +78,7 @@ describe('Linked pipeline', () => {
|
|||
const titleAttr = findLinkedPipeline().attributes('title');
|
||||
|
||||
expect(titleAttr).toContain(mockPipeline.project.name);
|
||||
expect(titleAttr).toContain(mockPipeline.details.status.label);
|
||||
});
|
||||
|
||||
it('sets the loading prop to false', () => {
|
||||
expect(findButton().props('loading')).toBe(false);
|
||||
expect(titleAttr).toContain(mockPipeline.status.label);
|
||||
});
|
||||
|
||||
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
|
||||
|
@ -96,18 +88,20 @@ describe('Linked pipeline', () => {
|
|||
|
||||
describe('parent/child', () => {
|
||||
const downstreamProps = {
|
||||
pipeline: mockPipeline,
|
||||
projectId: validTriggeredPipelineId,
|
||||
pipeline: {
|
||||
...mockPipeline,
|
||||
multiproject: false,
|
||||
},
|
||||
columnTitle: 'Downstream',
|
||||
type: DOWNSTREAM,
|
||||
expanded: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const upstreamProps = {
|
||||
...downstreamProps,
|
||||
columnTitle: 'Upstream',
|
||||
type: UPSTREAM,
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
it('parent/child label container should exist', () => {
|
||||
|
@ -122,7 +116,7 @@ describe('Linked pipeline', () => {
|
|||
|
||||
it('should have the name of the trigger job on the card when it is a child pipeline', () => {
|
||||
createWrapper(downstreamProps);
|
||||
expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.source_job.name);
|
||||
expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
|
||||
});
|
||||
|
||||
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
|
||||
|
@ -132,12 +126,12 @@ describe('Linked pipeline', () => {
|
|||
|
||||
it('downstream pipeline should contain the correct link', () => {
|
||||
createWrapper(downstreamProps);
|
||||
expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
|
||||
expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
|
||||
});
|
||||
|
||||
it('upstream pipeline should contain the correct link', () => {
|
||||
createWrapper(upstreamProps);
|
||||
expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
|
||||
expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path);
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -183,11 +177,11 @@ describe('Linked pipeline', () => {
|
|||
|
||||
describe('when isLoading is true', () => {
|
||||
const props = {
|
||||
pipeline: { ...mockPipeline, isLoading: true },
|
||||
projectId: invalidTriggeredPipelineId,
|
||||
pipeline: mockPipeline,
|
||||
columnTitle: 'Downstream',
|
||||
type: DOWNSTREAM,
|
||||
expanded: false,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -202,10 +196,10 @@ describe('Linked pipeline', () => {
|
|||
describe('on click/hover', () => {
|
||||
const props = {
|
||||
pipeline: mockPipeline,
|
||||
projectId: validTriggeredPipelineId,
|
||||
columnTitle: 'Downstream',
|
||||
type: DOWNSTREAM,
|
||||
expanded: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -228,7 +222,7 @@ describe('Linked pipeline', () => {
|
|||
|
||||
it('should emit downstreamHovered with job name on mouseover', () => {
|
||||
findLinkedPipeline().trigger('mouseover');
|
||||
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]);
|
||||
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['test_c']]);
|
||||
});
|
||||
|
||||
it('should emit downstreamHovered with empty string on mouseleave', () => {
|
||||
|
@ -238,7 +232,7 @@ describe('Linked pipeline', () => {
|
|||
|
||||
it('should emit pipelineExpanded with job name and expanded state on click', () => {
|
||||
findExpandButton().trigger('click');
|
||||
expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['trigger_job', true]]);
|
||||
expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['test_c', true]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
|
|||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import {
|
||||
DOWNSTREAM,
|
||||
GRAPHQL,
|
||||
UPSTREAM,
|
||||
LAYER_VIEW,
|
||||
STAGE_VIEW,
|
||||
|
@ -52,9 +51,6 @@ describe('Linked Pipelines Column', () => {
|
|||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
dataMethod: GRAPHQL,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
106
spec/graphql/resolvers/users/groups_resolver_spec.rb
Normal file
106
spec/graphql/resolvers/users/groups_resolver_spec.rb
Normal file
|
@ -0,0 +1,106 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::Users::GroupsResolver do
|
||||
include GraphqlHelpers
|
||||
include AdminModeHelper
|
||||
|
||||
describe '#resolve' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
|
||||
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
|
||||
let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
|
||||
let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
|
||||
|
||||
subject(:resolved_items) { resolve_groups(args: group_arguments, current_user: current_user, obj: resolver_object) }
|
||||
|
||||
let(:group_arguments) { {} }
|
||||
let(:current_user) { user }
|
||||
let(:resolver_object) { user }
|
||||
|
||||
before_all do
|
||||
guest_group.add_guest(user)
|
||||
private_maintainer_group.add_maintainer(user)
|
||||
public_developer_group.add_developer(user)
|
||||
public_maintainer_group.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'when paginatable_namespace_drop_down_for_project_creation feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when resolver object is current user' do
|
||||
context 'when permission is :create_projects' do
|
||||
let(:group_arguments) { { permission_scope: :create_projects } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
guest_group
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
context 'when search is provided' do
|
||||
let(:group_arguments) { { search: 'maintainer' } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when resolver object is different from current user' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
|
||||
context 'when current_user is admin' do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
|
||||
before do
|
||||
enable_admin_mode!(current_user)
|
||||
end
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
[
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
guest_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_groups(args:, current_user:, obj:)
|
||||
resolve(described_class, args: args, ctx: { current_user: current_user }, obj: obj)&.items
|
||||
end
|
||||
end
|
|
@ -33,6 +33,7 @@ RSpec.describe GitlabSchema.types['MergeRequestReviewer'] do
|
|||
merge_request_interaction
|
||||
namespace
|
||||
timelogs
|
||||
groups
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -38,6 +38,7 @@ RSpec.describe GitlabSchema.types['User'] do
|
|||
callouts
|
||||
namespace
|
||||
timelogs
|
||||
groups
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -284,7 +284,7 @@ RSpec.describe IssuesHelper do
|
|||
iid: issue.iid,
|
||||
is_issue_author: 'false',
|
||||
issue_type: 'issue',
|
||||
new_issue_path: new_project_issue_path(project),
|
||||
new_issue_path: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }),
|
||||
project_path: project.full_path,
|
||||
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
|
||||
submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group1) { create(:group, :private).tap { |g| g.add_developer(user) } }
|
||||
let_it_be(:group2) { create(:group, :private).tap { |g| g.add_developer(user) } }
|
||||
let_it_be(:group3) { create(:group, :private) }
|
||||
|
||||
let(:max_query_regex) { /SELECT MAX\("members"\."access_level"\).+/ }
|
||||
let(:groups) { [group1, group2, group3] }
|
||||
|
||||
shared_examples 'executes N max member permission queries to the DB' do
|
||||
it 'executes the specified max membership queries' do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
groups.each { |group| user.can?(:read_group, group) }
|
||||
end
|
||||
|
||||
max_queries = queries.log.grep(max_query_regex)
|
||||
|
||||
expect(max_queries.count).to eq(expected_query_count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the preloader is used', :request_store do
|
||||
before do
|
||||
described_class.new(groups, user).execute
|
||||
end
|
||||
|
||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||
# Will query all groups where the user is not already a member
|
||||
let(:expected_query_count) { 1 }
|
||||
end
|
||||
|
||||
context 'when user has access but is not a direct member of the group' do
|
||||
let(:groups) { [group1, group2, group3, create(:group, :private, parent: group1)] }
|
||||
|
||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||
# One query for group with no access and another one where the user is not a direct member
|
||||
let(:expected_query_count) { 2 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the preloader is not used', :request_store do
|
||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||
let(:expected_query_count) { groups.count }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,8 +3,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe UserPolicy do
|
||||
let(:current_user) { create(:user) }
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:admin) { create(:user, :admin) }
|
||||
let_it_be(:regular_user) { create(:user) }
|
||||
let_it_be(:subject_user) { create(:user) }
|
||||
|
||||
let(:current_user) { regular_user }
|
||||
let(:user) { subject_user }
|
||||
|
||||
subject { described_class.new(current_user, user) }
|
||||
|
||||
|
@ -16,7 +20,7 @@ RSpec.describe UserPolicy do
|
|||
let(:token) { create(:personal_access_token, user: user) }
|
||||
|
||||
context 'when user is admin' do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:read_user_personal_access_tokens) }
|
||||
|
@ -42,7 +46,7 @@ RSpec.describe UserPolicy do
|
|||
|
||||
describe "creating a different user's Personal Access Tokens" do
|
||||
context 'when current_user is admin' do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode is enabled and current_user is not blocked', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:create_user_personal_access_token) }
|
||||
|
@ -92,7 +96,7 @@ RSpec.describe UserPolicy do
|
|||
end
|
||||
|
||||
context "when an admin user tries to destroy a regular user" do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(ability) }
|
||||
|
@ -104,7 +108,7 @@ RSpec.describe UserPolicy do
|
|||
end
|
||||
|
||||
context "when an admin user tries to destroy a ghost user" do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
let(:current_user) { admin }
|
||||
let(:user) { create(:user, :ghost) }
|
||||
|
||||
it { is_expected.not_to be_allowed(ability) }
|
||||
|
@ -132,7 +136,7 @@ RSpec.describe UserPolicy do
|
|||
|
||||
context 'disabling the two-factor authentication of another user' do
|
||||
context 'when the executor is an admin', :enable_admin_mode do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
let(:current_user) { admin }
|
||||
|
||||
it { is_expected.to be_allowed(:disable_two_factor) }
|
||||
end
|
||||
|
@ -145,7 +149,7 @@ RSpec.describe UserPolicy do
|
|||
|
||||
describe "reading a user's group count" do
|
||||
context "when current_user is an admin", :enable_admin_mode do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
let(:current_user) { admin }
|
||||
|
||||
it { is_expected.to be_allowed(:read_group_count) }
|
||||
end
|
||||
|
@ -172,4 +176,30 @@ RSpec.describe UserPolicy do
|
|||
it { is_expected.to be_allowed(:read_user_profile) }
|
||||
end
|
||||
end
|
||||
|
||||
describe ':read_user_groups' do
|
||||
context 'when user is admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:read_user_groups) }
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.not_to be_allowed(:read_user_groups) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not an admin' do
|
||||
context 'requesting their own manageable groups' do
|
||||
subject { described_class.new(current_user, current_user) }
|
||||
|
||||
it { is_expected.to be_allowed(:read_user_groups) }
|
||||
end
|
||||
|
||||
context "requesting a different user's manageable groups" do
|
||||
it { is_expected.not_to be_allowed(:read_user_groups) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
112
spec/requests/api/graphql/current_user/groups_query_spec.rb
Normal file
112
spec/requests/api/graphql/current_user/groups_query_spec.rb
Normal file
|
@ -0,0 +1,112 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Query current user groups' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
|
||||
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
|
||||
let_it_be(:public_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
|
||||
let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
|
||||
|
||||
let(:group_arguments) { {} }
|
||||
let(:current_user) { user }
|
||||
|
||||
let(:fields) do
|
||||
<<~GRAPHQL
|
||||
nodes { id path fullPath name }
|
||||
GRAPHQL
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for('currentUser', {}, query_graphql_field('groups', group_arguments, fields))
|
||||
end
|
||||
|
||||
before_all do
|
||||
guest_group.add_guest(user)
|
||||
private_maintainer_group.add_maintainer(user)
|
||||
public_developer_group.add_developer(user)
|
||||
public_maintainer_group.add_maintainer(user)
|
||||
end
|
||||
|
||||
subject { graphql_data.dig('currentUser', 'groups', 'nodes') }
|
||||
|
||||
before do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
it 'avoids N+1 queries', :request_store do
|
||||
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
|
||||
|
||||
new_group = create(:group, :private)
|
||||
new_group.add_maintainer(current_user)
|
||||
|
||||
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
|
||||
end
|
||||
|
||||
it 'returns all groups where the user is a direct member' do
|
||||
is_expected.to match(
|
||||
expected_group_hash(
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
guest_group
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when permission_scope is CREATE_PROJECTS' do
|
||||
let(:group_arguments) { { permission_scope: :CREATE_PROJECTS } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
expected_group_hash(
|
||||
public_maintainer_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when search is provided' do
|
||||
let(:group_arguments) { { permission_scope: :CREATE_PROJECTS, search: 'maintainer' } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
expected_group_hash(
|
||||
public_maintainer_group,
|
||||
private_maintainer_group
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search is provided' do
|
||||
let(:group_arguments) { { search: 'maintainer' } }
|
||||
|
||||
specify do
|
||||
is_expected.to match(
|
||||
expected_group_hash(
|
||||
public_maintainer_group,
|
||||
private_maintainer_group
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def expected_group_hash(*groups)
|
||||
groups.map do |group|
|
||||
{
|
||||
'id' => group.to_global_id.to_s,
|
||||
'name' => group.name,
|
||||
'path' => group.path,
|
||||
'fullPath' => group.full_path
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue