Commit 8f6e28e5 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 8f122839 53db7654
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
components: {
DropdownButton,
GlIcon,
GlLoadingIcon,
},
props: {
......@@ -85,7 +86,7 @@ export default {
type="search"
class="dropdown-input-field qa-dropdown-filter-input"
/>
<i aria-hidden="true" class="fa fa-search dropdown-input-search"></i>
<gl-icon name="search" class="dropdown-input-search" aria-hidden="true" />
</div>
<div class="dropdown-content">
<gl-loading-icon v-if="showLoading" size="lg" />
......
......@@ -743,3 +743,22 @@ export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
return endDateInMS - startDateInMS;
};
/**
* A utility which returns a new date at the first day of the month for any given date.
*
* @param {Date} date
*
* @return {Date} the date at the first day of the month
*/
export const dateAtFirstDayOfMonth = date => new Date(newDate(date).setDate(1));
/**
* A utility function which checks if two dates match.
*
* @param {Date|Int} date1 Can be either a date object or a unix timestamp.
* @param {Date|Int} date2 Can be either a date object or a unix timestamp.
*
* @return {Boolean} true if the dates match
*/
export const datesMatch = (date1, date2) => differenceInMilliseconds(date1, date2) === 0;
<script>
import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
placeholderText: {
type: String,
......@@ -41,5 +45,6 @@ export default {
autocomplete="off"
/>
<i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i>
<gl-icon name="search" class="dropdown-input-search" aria-hidden="true" data-hidden="true" />
</div>
</template>
......@@ -230,13 +230,12 @@ export default {
@keydown="onKeydown($event)"
@keyup="onKeyup($event)"
/>
<i
:class="{
hidden: showClearInputButton,
}"
<gl-icon
name="search"
class="dropdown-input-search"
:class="{ hidden: showClearInputButton }"
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
/>
<gl-icon
name="close"
class="dropdown-input-clear"
......
......@@ -431,10 +431,6 @@
margin-left: 0;
border-left: 0;
}
.file-actions .dropdown {
height: 28px;
}
}
table.code {
......
......@@ -17,9 +17,8 @@ class GroupMembersFinder < UnionFinder
@params = params
end
# rubocop: disable CodeReuse/ActiveRecord
def execute(include_relations: [:inherited, :direct])
group_members = group.members
group_members = group_members_list
relations = []
return group_members if include_relations == [:direct]
......@@ -27,17 +26,13 @@ class GroupMembersFinder < UnionFinder
relations << group_members if include_relations.include?(:direct)
if include_relations.include?(:inherited) && group.parent
parents_members = GroupMember.non_request.non_minimal_access
.where(source_id: group.ancestors.select(:id))
.where.not(user_id: group.users.select(:id))
parents_members = relation_group_members(group.ancestors)
relations << parents_members
end
if include_relations.include?(:descendants)
descendant_members = GroupMember.non_request.non_minimal_access
.where(source_id: group.descendants.select(:id))
.where.not(user_id: group.users.select(:id))
descendant_members = relation_group_members(group.descendants)
relations << descendant_members
end
......@@ -47,7 +42,6 @@ class GroupMembersFinder < UnionFinder
members = find_union(relations, GroupMember)
filter_members(members)
end
# rubocop: enable CodeReuse/ActiveRecord
private
......@@ -67,6 +61,22 @@ class GroupMembersFinder < UnionFinder
def can_manage_members
Ability.allowed?(user, :admin_group_member, group)
end
def group_members_list
group.members
end
def relation_group_members(relation)
all_group_members(relation).non_minimal_access
end
# rubocop: disable CodeReuse/ActiveRecord
def all_group_members(relation)
GroupMember.non_request
.where(source_id: relation.select(:id))
.where.not(user_id: group.users.select(:id))
end
# rubocop: enable CodeReuse/ActiveRecord
end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
......@@ -4,6 +4,7 @@ module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation
prepend Gitlab::Graphql::Authorize::AuthorizeResource
prepend Gitlab::Graphql::CopyFieldDescription
prepend ::Gitlab::Graphql::GlobalIDCompatibility
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
......
......@@ -3,13 +3,18 @@
module Mutations
module Ci
class Base < BaseMutation
argument :id, ::Types::GlobalIDType[::Ci::Pipeline],
PipelineID = ::Types::GlobalIDType[::Ci::Pipeline]
argument :id, PipelineID,
required: true,
description: 'The id of the pipeline to mutate'
private
def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = PipelineID.coerce_isolated_input(id)
GlobalID::Locator.locate(id)
end
end
......
......@@ -29,11 +29,18 @@ module Mutations
private
def parameters(**args)
args.transform_values { |id| GitlabSchema.find_by_gid(id) }.transform_values(&:sync).tap do |hash|
args.transform_values { |id| find_design(id) }.transform_values(&:sync).tap do |hash|
hash.each { |k, design| not_found(args[k]) unless current_user.can?(:read_design, design) }
end
end
def find_design(id)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = DesignID.coerce_isolated_input(id)
GitlabSchema.object_from_id(id)
end
def not_found(gid)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{gid}"
end
......
......@@ -4,6 +4,7 @@ module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::GlobalIDCompatibility
def self.single
@single ||= Class.new(self) do
......
# frozen_string_literal: true
module GraphQLExtensions
module ScalarExtensions
# Allow ID to unify with GlobalID Types
def ==(other)
if name == 'ID' && other.is_a?(self.class) &&
other.type_class.ancestors.include?(::Types::GlobalIDType)
return true
end
super
end
end
end
::GraphQL::ScalarType.prepend(GraphQLExtensions::ScalarExtensions)
module Types
class GlobalIDType < BaseScalar
graphql_name 'GlobalID'
......
......@@ -49,8 +49,7 @@ module Types
field :milestone, ::Types::MilestoneType,
null: true,
description: 'Find a milestone',
resolve: -> (_obj, args, _ctx) { GitlabSchema.find_by_gid(args[:id]) } do
description: 'Find a milestone' do
argument :id, ::Types::GlobalIDType[Milestone],
required: true,
description: 'Find a milestone by its ID'
......@@ -86,7 +85,17 @@ module Types
end
def issue(id:)
GitlabSchema.object_from_id(id, expected_type: ::Issue)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Issue].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def milestone(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
......
......@@ -10,7 +10,7 @@ module Groups::GroupMembersHelper
end
def render_invite_member_for_group(group, default_access_level)
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: group.access_level_roles, default_access_level: default_access_level
end
def linked_groups_data_json(group_links)
......
......@@ -356,6 +356,7 @@ class Group < Namespace
end
group_hierarchy_members = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(source_id: source_ids)
GroupMember.from_union([group_hierarchy_members,
......@@ -550,6 +551,14 @@ class Group < Namespace
owners.first || parent&.default_owner || owner
end
def access_level_roles
GroupMember.access_level_roles
end
def access_level_values
access_level_roles.values
end
private
def update_two_factor_requirement
......
......@@ -132,6 +132,8 @@ class User < ApplicationRecord
-> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
source: :group
has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, source: 'GroupMember', class_name: 'GroupMember'
has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
# Projects
has_many :groups_projects, through: :groups, source: :projects
......
......@@ -113,7 +113,7 @@
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.gl-mt-3
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
= select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
%hr
= button_tag _('Add users to group'), class: "btn btn-success"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
......
---
title: Add No Access Role for top group members
merge_request: 40942
author:
type: added
---
title: Replace fa-search fontawesome icons with GitLab SVG in Vue components
merge_request: 43879
author:
type: changed
---
name: minimal_access_role
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40942
rollout_issue_url:
group: group::access
type: licensed
default_enabled: true
......@@ -49,6 +49,20 @@ See also:
- [Exposing Global IDs](#exposing-global-ids).
- [Mutation arguments](#object-identifier-arguments).
We have a custom scalar type (`Types::GlobalIDType`) which should be used as the
type of input and output arguments when the value is a `GlobalID`. The benefits
of using this type instead of `ID` are:
- it validates that the value is a `GlobalID`
- it parses it into a `GlobalID` before passing it to user code
- it can be parameterized on the type of the object (e.g.
`GlobalIDType[Project]`) which offers even better validation and security.
Consider using this type for all new arguments and result types. Remember that
it is perfectly possible to parameterize this type with a concern or a
supertype, if you want to accept a wider range of objects (e.g.
`GlobalIDType[Issuable]` vs `GlobalIDType[Issue]`).
## Types
We use a code-first schema, and we declare what type everything is in Ruby.
......
......@@ -58,9 +58,9 @@ export default {
<audit-events-export-button v-if="hasExportUrl" :export-href="exportHref" />
</div>
</header>
<div class="row-content-block second-block pb-0">
<div class="d-flex justify-content-between audit-controls row">
<div class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8">
<div class="row-content-block second-block gl-pb-0">
<div class="gl-display-flex gl-justify-content-space-between audit-controls gl-flex-wrap">
<div class="gl-mb-5 gl-w-full">
<audit-events-filter
:filter-token-options="filterTokenOptions"
:value="filterValue"
......@@ -68,9 +68,9 @@ export default {
@submit="searchForAuditEvents"
/>
</div>
<div class="d-flex col-lg-auto flex-wrap pl-lg-0">
<div class="gl-display-flex gl-flex-wrap gl-w-full">
<div
class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0"
class="audit-controls gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between gl-px-0 gl-w-full"
>
<date-range-field
:start-date="startDate"
......
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { n__, s__ } from '~/locale';
import { datesMatch, dateAtFirstDayOfMonth, getDateInPast } from '~/lib/utils/datetime_utility';
import { CURRENT_DATE } from '../constants';
const DATE_RANGE_OPTIONS = [
{
text: n__('Last %d day', 'Last %d days', 7),
startDate: getDateInPast(CURRENT_DATE, 7),
endDate: CURRENT_DATE,
},
{
text: n__('Last %d day', 'Last %d days', 14),
startDate: getDateInPast(CURRENT_DATE, 14),
endDate: CURRENT_DATE,
},
{
text: s__('AuditLogs|This month'),
startDate: dateAtFirstDayOfMonth(CURRENT_DATE),
endDate: CURRENT_DATE,
},
];
export default {
components: {
GlButton,
GlButtonGroup,
},
props: {
dateRange: {
type: Object,
required: true,
},
},
methods: {
onDateRangeClicked({ startDate, endDate }) {
this.$emit('input', { startDate, endDate });
},
isCurrentDateRange({ startDate, endDate }) {
const { dateRange } = this;
return datesMatch(startDate, dateRange.startDate) && datesMatch(endDate, dateRange.endDate);
},
},
DATE_RANGE_OPTIONS,
};
</script>
<template>
<gl-button-group>
<gl-button
v-for="(dateRangeOption, idx) in $options.DATE_RANGE_OPTIONS"
:key="idx"
:selected="isCurrentDateRange(dateRangeOption)"
@click="onDateRangeClicked(dateRangeOption)"
>{{ dateRangeOption.text }}</gl-button
>
</gl-button-group>
</template>
<script>
import { GlDaterangePicker } from '@gitlab/ui';
import { dateAtFirstDayOfMonth } from '~/lib/utils/datetime_utility';
import { CURRENT_DATE, MAX_DATE_RANGE } from '../constants';
import DateRangeButtons from './date_range_buttons.vue';
export default {
components: {
DateRangeButtons,
GlDaterangePicker,
},
props: {
......@@ -17,21 +21,43 @@ export default {
default: null,
},
},
computed: {
defaultStartDate() {
return this.startDate || dateAtFirstDayOfMonth(CURRENT_DATE);
},
defaultEndDate() {
return this.endDate || CURRENT_DATE;
},
defaultDateRange() {
return { startDate: this.defaultStartDate, endDate: this.defaultEndDate };
},
},
methods: {
onInput(dates) {
this.$emit('selected', dates);
},
},
CURRENT_DATE,
MAX_DATE_RANGE,
};
</script>
<template>
<gl-daterange-picker
class="d-flex flex-wrap flex-sm-nowrap"
:default-start-date="startDate"
:default-end-date="endDate"
start-picker-class="form-group align-items-lg-center mr-0 mr-sm-1 d-flex flex-column flex-lg-row"
end-picker-class="form-group align-items-lg-center mr-0 mr-sm-2 d-flex flex-column flex-lg-row"
@input="onInput"
/>
<div
class="gl-display-flex gl-align-items-flex-end gl-xs-align-items-baseline gl-xs-flex-direction-column"
>
<div class="gl-pr-5 gl-mb-5">
<date-range-buttons :date-range="defaultDateRange" @input="onInput" />
</div>
<gl-daterange-picker
class="gl-display-flex gl-pl-0 gl-w-full"
:default-start-date="defaultStartDate"
:default-end-date="defaultEndDate"
:default-max-date="$options.CURRENT_DATE"
:max-date-range="$options.MAX_DATE_RANGE"
start-picker-class="gl-mb-5 gl-pr-5 gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-flex-fill-1"
end-picker-class="gl-mb-5 gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-flex-fill-1"
@input="onInput"
/>
</div>
</template>
......@@ -47,7 +47,7 @@ export default {
<template>
<div>
<gl-dropdown :text="selectedOption.text" class="w-100 flex-column flex-lg-row form-group">
<gl-dropdown :text="selectedOption.text" class="w-100 flex-column flex-lg-row gl-mb-5">
<gl-dropdown-section-header> {{ $options.SORTING_TITLE }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="option in $options.SORTING_OPTIONS"
......
......@@ -56,3 +56,7 @@ export const AUDIT_FILTER_CONFIGS = [
];
export const AVAILABLE_TOKEN_TYPES = AUDIT_FILTER_CONFIGS.map(token => token.type);
export const MAX_DATE_RANGE = 31;
export const CURRENT_DATE = new Date();
......@@ -31,7 +31,7 @@ export default {
class="dropdown-input-field"
@keyup="handleInputChange"
/>
<i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"></i>
<gl-icon name="search" class="dropdown-input-search" aria-hidden="true" data-hidden="true" />
<gl-icon
name="close"
class="dropdown-input-clear"
......
.audit-controls .gl-dropdown-toggle {
border-color: $gray-100;
border-radius: 0.25rem;
box-shadow: inset 0 0 0 0.0625rem $gray-200;
padding: 0.5rem 0.75rem;
.audit-controls {
.gl-dropdown-toggle {
border-color: $gray-100;
border-radius: 0.25rem;
box-shadow: inset 0 0 0 0.0625rem $gray-200;
padding: 0.5rem 0.75rem;
}
.gl-daterange-picker > div {
@include media-breakpoint-up(lg) {
align-items: baseline;
}
}
}
......@@ -4,6 +4,8 @@
module EE
module Admin
module GroupsController
extend ::Gitlab::Utils::Override
def reset_runners_minutes
group
......@@ -25,6 +27,13 @@ module EE
]
end
override :group_members
def group_members
return @group.all_group_members if @group.minimal_access_role_allowed?
@group.members
end
def groups
super.with_deletion_schedule
end
......
......@@ -2,6 +2,7 @@
module EE::GroupMembersFinder
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
attr_reader :group
......@@ -12,4 +13,18 @@ module EE::GroupMembersFinder
group.group_members.non_owners.joins(:user).merge(User.not_managed(group: group))
end
# rubocop: enable CodeReuse/ActiveRecord
override :group_members_list
def group_members_list
return group.all_group_members if group.minimal_access_role_allowed?
super
end
override :relation_group_members
def relation_group_members(relation)
return all_group_members(relation) if group.minimal_access_role_allowed?
super
end
end
......@@ -8,8 +8,10 @@ module EE
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
EpicID = ::Types::GlobalIDType[::Epic]
prepended do
argument :epic_id, ::Types::GlobalIDType[::Epic],
argument :epic_id, EpicID,
required: false,
description: 'The ID of the parent epic. NULL when removing the association'
end
......@@ -28,10 +30,12 @@ module EE
override :move_arguments
def move_arguments(args)
allowed_args = super
allowed_args[:epic_id] = args[:epic_id]&.model_id if args.has_key?(:epic_id)
# TODO: remove this line once the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
coerce_global_id_arguments!(args)
epic_arguments = args.slice(:epic_id).transform_values { |id| id&.model_id }
allowed_args
super.merge!(epic_arguments)
end
end
end
......
......@@ -87,7 +87,10 @@ module EE
::Types::DastSiteProfileType,
null: true,
resolve: -> (obj, args, _ctx) do
DastSiteProfilesFinder.new(project_id: obj.id, id: args[:id].model_id).execute.first
# TODO: remove this coercion when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
gid = ::Types::GlobalIDType[::DastSiteProfile].coerce_isolated_input(args[:id])
DastSiteProfilesFinder.new(project_id: obj.id, id: gid.model_id).execute.first
end,
description: 'DAST Site Profile associated with the project' do
argument :id, ::Types::GlobalIDType[::DastSiteProfile], required: true, description: 'ID of the site profile'
......
......@@ -8,7 +8,6 @@ module EE
prepended do
field :iteration, ::Types::IterationType,
null: true,
resolve: -> (_obj, args, _ctx) { ::GitlabSchema.find_by_gid(args[:id]) },
description: 'Find an iteration' do
argument :id, ::Types::GlobalIDType[::Iteration],
required: true,
......@@ -24,8 +23,7 @@ module EE
field :vulnerability,
::Types::VulnerabilityType,
null: true,
description: "Find a vulnerability",
resolve: -> (_obj, args, _ctx) { ::GitlabSchema.find_by_gid(args[:id]) } do
description: "Find a vulnerability" do
argument :id, ::Types::GlobalIDType[::Vulnerability],
required: true,
description: 'The Global ID of the Vulnerability'
......@@ -54,6 +52,20 @@ module EE
resolver: ::Resolvers::InstanceSecurityDashboardResolver,
description: 'Fields related to Instance Security Dashboard'
end
def vulnerability(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Vulnerability].coerce_isolated_input(id)
::GitlabSchema.find_by_gid(id)
end
def iteration(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[Iteration].coerce_isolated_input(id)
::GitlabSchema.find_by_gid(id)
end
end
end
end
......@@ -8,8 +8,9 @@ module Mutations
authorize :create_cluster
argument :cluster_agent_id,
::Types::GlobalIDType[::Clusters::Agent],
ClusterAgentID = ::Types::GlobalIDType[::Clusters::Agent]
argument :cluster_agent_id, ClusterAgentID,
required: true,
description: 'Global ID of the cluster agent that will be associated with the new token'
......@@ -42,6 +43,9 @@ module Mutations
private
def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ClusterAgentID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
......
......@@ -8,8 +8,9 @@ module Mutations
authorize :admin_cluster
argument :id,
::Types::GlobalIDType[::Clusters::AgentToken],
TokenID = ::Types::GlobalIDType[::Clusters::AgentToken]
argument :id, TokenID,
required: true,
description: 'Global ID of the cluster agent token that will be deleted'
......@@ -23,6 +24,9 @@ module Mutations
private
def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = TokenID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
......
......@@ -8,8 +8,9 @@ module Mutations
authorize :admin_cluster
argument :id,
::Types::GlobalIDType[::Clusters::Agent],
AgentID = ::Types::GlobalIDType[::Clusters::Agent]
argument :id, AgentID,
required: true,
description: 'Global id of the cluster agent that will be deleted'
......@@ -27,6 +28,9 @@ module Mutations
private
def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = AgentID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
......
......@@ -30,6 +30,10 @@ module Mutations
def resolve(full_path:, dast_site_profile_id:, **args)
project = authorized_find_project!(full_path: full_path)
# TODO: remove explicit coercion once compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
dast_site_profile_id = ::Types::GlobalIDType[::DastSiteProfile].coerce_isolated_input(dast_site_profile_id)
dast_site_profile = find_dast_site_profile(project: project, dast_site_profile_id: dast_site_profile_id)
dast_site = dast_site_profile.dast_site
dast_scanner_profile = find_dast_scanner_profile(project: project, dast_scanner_profile_id: args[:dast_scanner_profile_id])
......@@ -63,6 +67,11 @@ module Mutations
def find_dast_scanner_profile(project:, dast_scanner_profile_id:)
return unless dast_scanner_profile_id
# TODO: remove explicit coercion once compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
dast_scanner_profile_id = ::Types::GlobalIDType[::DastScannerProfile]
.coerce_isolated_input(dast_scanner_profile_id)
project
.dast_scanner_profiles
.find(dast_scanner_profile_id.model_id)
......
......@@ -7,17 +7,23 @@ module Mutations
graphql_name 'DastScannerProfileDelete'
ScannerProfileID = ::Types::GlobalIDType[::DastScannerProfile]
argument :full_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path for the project the scanner profile belongs to.'
argument :id, ::Types::GlobalIDType[::DastScannerProfile],
argument :id, ScannerProfileID,
required: true,
description: 'ID of the scanner profile to be deleted.'
authorize :create_on_demand_dast_scan
def resolve(full_path:, id:)
# TODO: remove this line once the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ScannerProfileID.coerce_isolated_input(id)
project = authorized_find_project!(full_path: full_path)
service = ::DastScannerProfiles::DestroyService.new(project, current_user)
......
......@@ -34,10 +34,14 @@ module Mutations
authorize :create_on_demand_dast_scan
def resolve(full_path:, **service_args)
# TODO: remove this explicit coercion once the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
gid = ::Types::GlobalIDType[::DastScannerProfile].coerce_isolated_input(service_args[:id])
project = authorized_find!(full_path: full_path)
service = ::DastScannerProfiles::UpdateService.new(project, current_user)
result = service.execute({ **service_args, id: service_args[:id].model_id })
result = service.execute({ **service_args, id: gid.model_id })
if result.success?
{ id: result.payload.to_global_id, errors: [] }
......
......@@ -19,6 +19,10 @@ module Mutations
def resolve(full_path:, id:)
project = authorized_find_project!(full_path: full_path)
# TODO: remove explicit coercion once compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::DastSiteProfile].coerce_isolated_input(id)
dast_site_profile = find_dast_site_profile(project: project, global_id: id)
return { errors: dast_site_profile.errors.full_messages } unless dast_site_profile.destroy
......
......@@ -29,7 +29,10 @@ module Mutations
authorize :create_on_demand_dast_scan
def resolve(full_path:, **service_args)
def resolve(full_path:, id:, **service_args)
# TODO: remove explicit coercion once compatibility layer has been removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
service_args[:id] = ::Types::GlobalIDType[::DastSiteProfile].coerce_isolated_input(id).model_id
project = authorized_find_project!(full_path: full_path)
service = ::DastSiteProfiles::UpdateService.new(project, current_user)
......
......@@ -33,6 +33,9 @@ module Mutations
end
def find_object(id:)
# TODO: remove this line once the compatibility layer is removed.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Vulnerability].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
......
......@@ -33,6 +33,9 @@ module Mutations
end
def find_object(id:)
# TODO: remove this line once the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Vulnerability].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
......
......@@ -408,6 +408,21 @@ module EE
minimal_access_role_allowed? ? ::Gitlab::Access::MINIMAL_ACCESS : ::Gitlab::Access::GUEST
end
override :access_level_roles
def access_level_roles
levels = ::GroupMember.access_level_roles
return levels unless minimal_access_role_allowed?
levels.merge(::Gitlab::Access::MINIMAL_ACCESS_HASH)
end
override :users_count
def users_count
return all_group_members.count unless minimal_access_role_allowed?
members.count
end
private
def custom_project_templates_group_allowed
......
......@@ -3,9 +3,9 @@
module EE
module GroupMember
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
extend ::Gitlab::Utils::Override
include UsageStatistics
validate :sso_enforcement, if: :group
......@@ -80,6 +80,14 @@ module EE
private
override :access_level_inclusion
def access_level_inclusion
levels = source.access_level_values
return if access_level.in?(levels)
errors.add(:access_level, "is not included in the list")
end
def email_does_not_match_any_allowed_domains(email)
_("email '%{email}' does not match the allowed domains of %{email_domains}" %
{ email: email, email_domains: ::Gitlab::Utils.to_exclusive_sentence(group_allowed_email_domains.map(&:domain)) })
......
......@@ -44,6 +44,9 @@ module EE
has_many :approvals, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :approvers, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :minimal_access_group_members, -> { where(access_level: [::Gitlab::Access::MINIMAL_ACCESS]) }, source: 'GroupMember', class_name: 'GroupMember'
has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
has_many :users_ops_dashboard_projects
has_many :ops_dashboard_projects, through: :users_ops_dashboard_projects, source: :project
has_many :users_security_dashboard_projects
......@@ -353,6 +356,18 @@ module EE
owns_paid_namespace?(plans: [::Plan::BRONZE, ::Plan::SILVER])
end
# Returns the groups a user has access to, either through a membership or a project authorization
override :authorized_groups
def authorized_groups
::Group.unscoped do
::Group.from_union([
groups,
available_minimal_access_groups,
authorized_projects.joins(:namespace).select('namespaces.*')
])
end
end
protected
override :password_required?
......@@ -383,5 +398,12 @@ module EE
highest_role > ::Gitlab::Access::GUEST
end
def available_minimal_access_groups
return ::Group.none unless License.feature_available?(:minimal_access_role)
return minimal_access_groups unless ::Gitlab::CurrentSettings.should_check_namespace_plan?
minimal_access_groups.with_feature_available_in_plan(:minimal_access_role)
end
end
end
......@@ -9,7 +9,8 @@ class SamlProvider < ApplicationRecord
validates :group, presence: true, top_level_group: true
validates :sso_url, presence: true, addressable_url: { schemes: %w(https), ascii_only: true }
validates :certificate_fingerprint, presence: true, certificate_fingerprint: true
validates :default_membership_role, presence: true, inclusion: { in: Gitlab::Access.values }
validates :default_membership_role, presence: true
validate :access_level_inclusion
after_initialize :set_defaults, if: :new_record?
......@@ -82,6 +83,15 @@ class SamlProvider < ApplicationRecord
private
def access_level_inclusion
return errors.add(:default_membership_role, "is dependent on a group") unless group
levels = group.access_level_values
return if default_membership_role.in?(levels)
errors.add(:default_membership_role, "is not included in the list")
end
def set_defaults
self.enabled = true
end
......
......@@ -2,6 +2,8 @@
module EE
module GroupMemberPresenter
extend ::Gitlab::Utils::Override
def group_sso?
member.user.group_sso?(source)
end
......@@ -10,6 +12,11 @@ module EE
member.user.group_managed_account?
end
override :access_level_roles
def access_level_roles
member.source.access_level_roles
end
private
def override_member_permission
......
......@@ -30,7 +30,7 @@ module DastSiteProfiles
# rubocop: disable CodeReuse/ActiveRecord
def find_dast_site_profile!(id)
DastSiteProfilesFinder.new(project_id: project.id, id: id.model_id).execute.first!
DastSiteProfilesFinder.new(project_id: project.id, id: id).execute.first!
end
# rubocop: enable CodeReuse/ActiveRecord
end
......
......@@ -39,6 +39,10 @@ class FetchSubscriptionPlansService
end
def cache_key
"subscription-plans-#{@plan}"
if Feature.enabled?(:subscription_plan_cache_key)
"subscription-plan-#{@plan}"
else
"subscription-plans-#{@plan}"
end
end
end
......@@ -50,7 +50,7 @@
.well-segment.borderless.gl-mb-3.col-12.col-lg-9.gl-p-0
= f.label :default_membership_role, class: 'label-bold' do
= s_('GroupSAML|Default membership role')
= f.select :default_membership_role, options_for_select(::Gitlab::Access.options, saml_provider.default_membership_role), {}, class: 'form-control', data: { qa_selector: 'default_membership_role_dropdown' }
= f.select :default_membership_role, options_for_select(group.access_level_roles, saml_provider.default_membership_role), {}, class: 'form-control', data: { qa_selector: 'default_membership_role_dropdown' }
.form-text.text-muted
= s_('GroupSAML|This will be set as the access level of users added to the group.')
......
---
title: Add quick select date options to audit events filter
merge_request: 42711
author:
type: added
---
name: subscription_plan_cache_key
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43973
rollout_issue_url:
type: development
group: group::fulfillment
default_enabled: false
......@@ -10,16 +10,26 @@ module EE
module Access
extend ActiveSupport::Concern
ADMIN = 60
MINIMAL_ACCESS_HASH = { "Minimal Access" => ::Gitlab::Access::MINIMAL_ACCESS }.freeze
class_methods do
extend ::Gitlab::Utils::Override
def vulnerability_access_levels
@vulnerability_access_levels ||= options_with_owner.except('Guest')
end
def options_with_minimal_access
options_with_owner.merge(
"Minimal Access" => ::Gitlab::Access::MINIMAL_ACCESS
)
options_with_owner.merge(MINIMAL_ACCESS_HASH)
end
def values_with_minimal_access
options_with_minimal_access.values
end
override :human_access
def human_access(access)
options_with_minimal_access.key(access)
end
end
end
......
......@@ -19,4 +19,35 @@ RSpec.describe GroupMembersFinder do
expect(finder.not_managed).to eq([group_member_membership])
end
end
describe '#execute' do
let_it_be(:group_minimal_access_membership) do
create(:group_member, :minimal_access, source: group, user: create(:user))
end
context 'when group does not allow minimal access members' do
before do
stub_licensed_features(minimal_access_role: false)
end
it 'returns only members with full access' do
result = finder.execute(include_relations: [:direct, :descendants])
expect(result.to_a).to match_array([group_owner_membership, group_member_membership, dedicated_member_account_membership])
end
end
context 'when group allows minimal access members' do
before do
group.clear_memoization(:feature_available)
stub_licensed_features(minimal_access_role: true)
end
it 'also returns members with minimal access' do
result = finder.execute(include_relations: [:direct, :descendants])
expect(result.to_a).to match_array([group_owner_membership, group_member_membership, dedicated_member_account_membership, group_minimal_access_membership])
end
end
end
end
......@@ -13,13 +13,13 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
</header>
<div
class="row-content-block second-block pb-0"
class="row-content-block second-block gl-pb-0"
>
<div
class="d-flex justify-content-between audit-controls row"
class="gl-display-flex gl-justify-content-space-between audit-controls gl-flex-wrap"
>
<div
class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8"
class="gl-mb-5 gl-w-full"
>
<div
class="input-group bg-white flex-grow-1"
......@@ -37,10 +37,10 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
</div>
<div
class="d-flex col-lg-auto flex-wrap pl-lg-0"
class="gl-display-flex gl-flex-wrap gl-w-full"
>
<div
class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0"
class="audit-controls gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between gl-px-0 gl-w-full"
>
<date-range-field-stub
enddate="Sun Feb 02 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
......
import { shallowMount } from '@vue/test-utils';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import DateRangeButtons from 'ee/audit_events/components/date_range_buttons.vue';
import { CURRENT_DATE } from 'ee/audit_events/constants';
import { getDateInPast } from '~/lib/utils/datetime_utility';
describe('DateRangeButtons component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(DateRangeButtons, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows the selected the option that matches the provided dateRange property', () => {
createComponent({
dateRange: { startDate: getDateInPast(CURRENT_DATE, 7), endDate: CURRENT_DATE },
});
expect(
wrapper
.find(GlButtonGroup)
.find('[selected="true"]')
.text(),
).toBe('Last 7 days');
});
it('shows no date range as selected when the dateRange property does not match any option', () => {
createComponent({
dateRange: {
startDate: getDateInPast(CURRENT_DATE, 5),
endDate: getDateInPast(CURRENT_DATE, 2),
},
});
expect(
wrapper
.find(GlButtonGroup)
.find('[selected="true"]')
.exists(),
).toBe(false);
});
it('emits an "input" event with the dateRange when a new date range is selected', async () => {
createComponent({
dateRange: { startDate: getDateInPast(CURRENT_DATE, 1), endDate: CURRENT_DATE },
});
wrapper
.find(GlButtonGroup)
.find(GlButton)
.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted().input[0]).toEqual([
{
startDate: getDateInPast(CURRENT_DATE, 7),
endDate: CURRENT_DATE,
},
]);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlDaterangePicker } from '@gitlab/ui';
import DateRangeButtons from 'ee/audit_events/components/date_range_buttons.vue';
import DateRangeField from 'ee/audit_events/components/date_range_field.vue';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import { CURRENT_DATE, MAX_DATE_RANGE } from 'ee/audit_events/constants';
import { dateAtFirstDayOfMonth, parsePikadayDate } from '~/lib/utils/datetime_utility';
describe('DateRangeField component', () => {
let wrapper;
......@@ -10,6 +12,9 @@ describe('DateRangeField component', () => {
const startDate = parsePikadayDate('2020-03-13');
const endDate = parsePikadayDate('2020-03-14');
const findDatePicker = () => wrapper.find(GlDaterangePicker);
const findDateRangeButtons = () => wrapper.find(DateRangeButtons);
const createComponent = (props = {}) => {
wrapper = shallowMount(DateRangeField, {
propsData: { ...props },
......@@ -21,39 +26,74 @@ describe('DateRangeField component', () => {
wrapper = null;
});
it('passes the startDate to the date picker as defaultStartDate', () => {
createComponent({ startDate });
describe('default behaviour', () => {
it('sets the max date range on the date picker', () => {
createComponent();
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: startDate,
defaultEndDate: null,
expect(findDatePicker().props('maxDateRange')).toBe(MAX_DATE_RANGE);
});
it("sets the max selectable date to today's date on the date picker", () => {
createComponent();
expect(
findDatePicker()
.props('defaultMaxDate')
.toDateString(),
).toBe(CURRENT_DATE.toDateString());
});
});
it('passes the endDate to the date picker as defaultEndDate', () => {
createComponent({ endDate });
it('sets the default start date to the start of the month', () => {
createComponent();
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: null,
defaultEndDate: endDate,
expect(
findDatePicker()
.props('defaultStartDate')
.toDateString(),
).toBe(dateAtFirstDayOfMonth(CURRENT_DATE).toDateString());
});
it("sets the default end date to today's date", () => {
createComponent();
expect(
findDatePicker()
.props('defaultEndDate')
.toDateString(),
).toBe(CURRENT_DATE.toDateString());
});
it('passes both startDate and endDate to the date picker as default dates', () => {
createComponent({ startDate, endDate });
expect(findDatePicker().props()).toMatchObject({
defaultStartDate: startDate,
defaultEndDate: endDate,
});
});
});
it('passes both startDate and endDate to the date picker as default dates', () => {
createComponent({ startDate, endDate });
describe('when a new date range is picked', () => {
it('emits the "selected" event with the picked startDate and endDate', async () => {
createComponent();
findDatePicker().vm.$emit('input', { startDate, endDate });
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: startDate,
defaultEndDate: endDate,
await wrapper.vm.$nextTick();
expect(wrapper.emitted().selected[0]).toEqual([
{
startDate,
endDate,
},
]);
});
});
it('should emit the "selected" event with startDate and endDate on input change', () => {
createComponent();
wrapper.find(GlDaterangePicker).vm.$emit('input', { startDate, endDate });
describe('when a date range button is pressed', () => {
it('emits the "selected" event with the picked startDate and endDate', async () => {
createComponent();
findDateRangeButtons().vm.$emit('input', { startDate, endDate });
return wrapper.vm.$nextTick(() => {
expect(wrapper.emitted().selected).toBeTruthy();
await wrapper.vm.$nextTick();
expect(wrapper.emitted().selected[0]).toEqual([
{
startDate,
......
......@@ -74,7 +74,7 @@ describe('ListFilter', () => {
});
it('renders search input icons', () => {
expect(wrapper.find('i.fa.fa-search.dropdown-input-search').exists()).toBe(true);
expect(wrapper.find('.dropdown-input-search').exists()).toBe(true);
expect(wrapper.find('.dropdown-input-clear').exists()).toBe(true);
});
});
......
......@@ -52,7 +52,7 @@ RSpec.describe Mutations::Clusters::AgentTokens::Create do
subject { mutation.resolve(cluster_agent_id: cluster_agent.id) }
it 'generates an error message when id invalid', :aggregate_failures do
expect { subject }.to raise_error(NoMethodError)
expect { subject }.to raise_error(::GraphQL::CoercionError)
end
end
end
......
......@@ -44,7 +44,7 @@ RSpec.describe Mutations::Clusters::AgentTokens::Delete do
let(:global_id) { token.id }
it 'raises an error if the cluster agent id is invalid', :aggregate_failures do
expect { subject }.to raise_error(NoMethodError)
expect { subject }.to raise_error(::GraphQL::CoercionError)
expect { token.reload }.not_to raise_error
end
end
......
......@@ -24,7 +24,7 @@ RSpec.describe Mutations::Clusters::Agents::Delete do
context 'without user permissions' do
it 'fails to delete the cluster agent', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect { cluster_agent.reload }.not_to raise_error(ActiveRecord::RecordNotFound)
expect { cluster_agent.reload }.not_to raise_error
end
end
......@@ -43,8 +43,8 @@ RSpec.describe Mutations::Clusters::Agents::Delete do
subject { mutation.resolve(id: cluster_agent.id) }
it 'raises an error if the cluster agent id is invalid', :aggregate_failures do
expect { subject }.to raise_error(NoMethodError)
expect { cluster_agent.reload }.not_to raise_error(ActiveRecord::RecordNotFound)
expect { subject }.to raise_error(::GraphQL::CoercionError)
expect { cluster_agent.reload }.not_to raise_error
end
end
end
......
......@@ -107,6 +107,50 @@ RSpec.describe GroupMember do
end
end
end
describe 'access level inclusion' do
let(:group) { create(:group) }
context 'when minimal access user feature switched on' do
before do
stub_licensed_features(minimal_access_role: true)
end
it 'users can have access levels from minimal access to owner' do
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::NO_ACCESS)).to be_invalid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::MINIMAL_ACCESS)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::GUEST)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::REPORTER)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::DEVELOPER)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::MAINTAINER)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::OWNER)).to be_valid
end
context 'when group is a subgroup' do
let(:subgroup) { create(:group, parent: group) }
it 'users cannot have minimal access level' do
expect(build(:group_member, group: subgroup, user: create(:user), access_level: ::Gitlab::Access::MINIMAL_ACCESS)).to be_invalid
end
end
end
context 'when minimal access user feature switched off' do
before do
stub_licensed_features(minimal_access_role: false)
end
it 'users can have access levels from guest to owner' do
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::NO_ACCESS)).to be_invalid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::MINIMAL_ACCESS)).to be_invalid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::GUEST)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::REPORTER)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::DEVELOPER)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::MAINTAINER)).to be_valid
expect(build(:group_member, group: group, user: create(:user), access_level: ::Gitlab::Access::OWNER)).to be_valid
end
end
end
end
describe 'scopes' do
......
......@@ -705,7 +705,7 @@ RSpec.describe Group do
context 'with `minimal_access_role` not licensed' do
before do
stub_licensed_features(minimal_access_role: false)
create(:group_member, :minimal_access, user: user, group: group)
create(:group_member, :minimal_access, user: user, source: group)
end
it { is_expected.to be_falsey }
......
......@@ -62,6 +62,42 @@ RSpec.describe SamlProvider do
expect(subject).to allow_value(group).for(:group)
expect(subject).not_to allow_value(nested_group).for(:group)
end
describe 'access level inclusion' do
let(:group) { create(:group) }
context 'when minimal access user feature is switched on' do
before do
stub_licensed_features(minimal_access_role: true)
end
it 'default membership role can have access levels from minimal access to owner' do
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::NO_ACCESS)).to be_invalid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::MINIMAL_ACCESS)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::GUEST)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::REPORTER)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::DEVELOPER)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::MAINTAINER)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::OWNER)).to be_valid
end
end
context 'when minimal access user feature switched off' do
before do
stub_licensed_features(minimal_access_role: false)
end
it 'default membership role can have access levels from guest to owner' do
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::NO_ACCESS)).to be_invalid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::MINIMAL_ACCESS)).to be_invalid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::GUEST)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::REPORTER)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::DEVELOPER)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::MAINTAINER)).to be_valid
expect(build(:saml_provider, group: group, default_membership_role: ::Gitlab::Access::OWNER)).to be_valid
end
end
end
end
describe 'Default values' do
......
......@@ -1147,6 +1147,56 @@ RSpec.describe User do
end
end
describe '#authorized_groups' do
let_it_be(:user) { create(:user) }
let_it_be(:private_group) { create(:group) }
let_it_be(:child_group) { create(:group, parent: private_group) }
let_it_be(:minimal_access_group) { create(:group) }
let_it_be(:project_group) { create(:group) }
let_it_be(:project) { create(:project, group: project_group) }
before do
private_group.add_user(user, Gitlab::Access::MAINTAINER)
project.add_maintainer(user)
create(:group_member, :minimal_access, user: user, source: minimal_access_group)
end
subject { user.authorized_groups }
context 'with minimal access role feature unavailable' do
it { is_expected.to contain_exactly private_group, project_group }
end
context 'with minimal access feature available' do
before do
stub_licensed_features(minimal_access_role: true)
end
context 'feature turned on for all groups' do
before do
allow(Gitlab::CurrentSettings)
.to receive(:should_check_namespace_plan?)
.and_return(false)
end
it { is_expected.to contain_exactly private_group, project_group, minimal_access_group }
end
context 'feature available for specific groups only' do
before do
allow(Gitlab::CurrentSettings)
.to receive(:should_check_namespace_plan?)
.and_return(true)
create(:gitlab_subscription, :gold, namespace: minimal_access_group)
create(:group_member, :minimal_access, user: user, source: create(:group))
end
it { is_expected.to contain_exactly private_group, project_group, minimal_access_group }
end
end
end
describe '#active_for_authentication?' do
subject { user.active_for_authentication? }
......
......@@ -60,4 +60,26 @@ RSpec.describe GroupMemberPresenter do
it { expect(presenter.can_update?).to eq(false) }
end
end
describe '#valid_level_roles?' do
context 'with minimal access role feature switched on' do
before do
allow(group_member).to receive(:highest_group_member)
allow(group_member).to receive_message_chain(:class, :access_level_roles).and_return(::Gitlab::Access.options_with_owner)
expect(group).to receive(:access_level_roles).and_return(::Gitlab::Access.options_with_minimal_access)
end
it { expect(presenter.valid_level_roles).to eq(::Gitlab::Access.options_with_minimal_access) }
end
context 'with minimal access role feature switched off' do
it_behaves_like '#valid_level_roles', :group do
let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Owner' => 50, 'Reporter' => 20 } }
before do
entity.parent = group
end
end
end
end
end
......@@ -17,7 +17,7 @@ RSpec.describe DastSiteProfiles::UpdateService do
describe '#execute' do
subject do
described_class.new(project, user).execute(
id: dast_site_profile.to_global_id,
id: dast_site_profile.id,
profile_name: new_profile_name,
target_url: new_target_url
)
......
# frozen_string_literal: true
module Gitlab
module Graphql
module GlobalIDCompatibility
# TODO: remove this module once the compatibility layer is no longer needed.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
def coerce_global_id_arguments!(args)
global_id_arguments = self.class.arguments.values.select do |arg|
arg.type.is_a?(Class) && arg.type <= ::Types::GlobalIDType
end
global_id_arguments.each do |arg|
k = arg.keyword
args[k] &&= arg.type.coerce_isolated_input(args[k])
end
end
end
end
end
......@@ -3669,6 +3669,9 @@ msgstr ""
msgid "AuditLogs|Target"
msgstr ""
msgid "AuditLogs|This month"
msgstr ""
msgid "AuditLogs|User Events"
msgstr ""
......
......@@ -667,3 +667,26 @@ describe('differenceInMilliseconds', () => {
expect(datetimeUtility.differenceInMilliseconds(startDate, endDate)).toBe(expected);
});
});
describe('dateAtFirstDayOfMonth', () => {
const date = new Date('2019-07-16T12:00:00.000Z');
it('returns the date at the first day of the month', () => {
const startDate = datetimeUtility.dateAtFirstDayOfMonth(date);
const expectedStartDate = new Date('2019-07-01T12:00:00.000Z');
expect(startDate).toStrictEqual(expectedStartDate);
});
});
describe('datesMatch', () => {
const date = new Date('2019-07-17T00:00:00.000Z');
it.each`
date1 | date2 | expected
${date} | ${new Date('2019-07-17T00:00:00.000Z')} | ${true}
${date} | ${new Date('2019-07-17T12:00:00.000Z')} | ${false}
`('returns $expected for $date1 matches $date2', ({ date1, date2, expected }) => {
expect(datetimeUtility.datesMatch(date1, date2)).toBe(expected);
});
});
......@@ -99,8 +99,6 @@ RSpec.describe Types::GlobalIDType do
end
describe 'compatibility' do
# Simplified schema to test compatibility
def query(doc, vars)
GraphQL::Query.new(schema, document: doc, context: {}, variables: vars)
end
......@@ -112,6 +110,7 @@ RSpec.describe Types::GlobalIDType do
all_types = [::GraphQL::ID_TYPE, ::Types::GlobalIDType, ::Types::GlobalIDType[::Project]]
shared_examples 'a working query' do
# Simplified schema to test compatibility
let!(:schema) do
# capture values so they can be closed over
arg_type = argument_type
......@@ -135,10 +134,21 @@ RSpec.describe Types::GlobalIDType do
argument :id, arg_type, required: true
end
# This is needed so that all types are always registered as input types
field :echo, String, null: true do
argument :id, ::GraphQL::ID_TYPE, required: false
argument :gid, ::Types::GlobalIDType, required: false
argument :pid, ::Types::GlobalIDType[::Project], required: false
end
def project_by_id(id:)
gid = ::Types::GlobalIDType[::Project].coerce_isolated_input(id)
gid.model_class.find(gid.model_id)
end
def echo(id: nil, gid: nil, pid: nil)
"id: #{id}, gid: #{gid}, pid: #{pid}"
end
end)
end
end
......@@ -152,7 +162,7 @@ RSpec.describe Types::GlobalIDType do
end
end
context 'when the argument is declared as ID' do
context 'when the client declares the argument as ID the actual argument can be any type' do
let(:document) do
<<-GRAPHQL
query($projectId: ID!){
......@@ -163,16 +173,16 @@ RSpec.describe Types::GlobalIDType do
GRAPHQL
end
let(:argument_type) { ::GraphQL::ID_TYPE }
where(:result_type) { all_types }
where(:result_type, :argument_type) do
all_types.flat_map { |arg_type| all_types.zip([arg_type].cycle) }
end
with_them do
it_behaves_like 'a working query'
end
end
context 'when the argument is declared as GlobalID' do
context 'when the client passes the argument as GlobalID' do
let(:document) do
<<-GRAPHQL
query($projectId: GlobalID!) {
......@@ -192,7 +202,7 @@ RSpec.describe Types::GlobalIDType do
end
end
context 'when the argument is declared as ProjectID' do
context 'when the client passes the argument as ProjectID' do
let(:document) do
<<-GRAPHQL
query($projectId: ProjectID!) {
......
......@@ -25,7 +25,7 @@ RSpec.shared_examples 'allowed user IDs are cached' do
expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
expect(subject).to be_truthy
end.not_to exceed_query_limit(2)
end.not_to exceed_query_limit(3)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment