Skip to content
Snippets Groups Projects
Commit 09fded61 authored by pshutsin's avatar pshutsin
Browse files

Introduce group level API for devops adoption

Now not only admins can enable devops adoption
but also reporter+ for any group
parent 69b760c0
No related branches found
No related tags found
No related merge requests found
Showing
with 244 additions and 201 deletions
Loading
Loading
@@ -86,8 +86,10 @@ Returns [`DevopsAdoptionSegmentConnection`](#devopsadoptionsegmentconnection).
| ---- | ---- | ----------- |
| `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. |
| `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. |
| `directDescendantsOnly` | [`Boolean`](#boolean) | Limits segments to direct descendants of specified parent. |
| `first` | [`Int`](#int) | Returns the first _n_ elements from the list. |
| `last` | [`Int`](#int) | Returns the last _n_ elements from the list. |
| `parentNamespaceId` | [`NamespaceID`](#namespaceid) | Filter by ancestor namespace. |
 
### `echo`
 
Loading
Loading
# frozen_string_literal: true
module Analytics
module DevopsAdoption
class SegmentsFinder
attr_reader :params, :current_user
def initialize(current_user, params:)
@current_user = current_user
@params = params
end
def execute
scope = ::Analytics::DevopsAdoption::Segment.ordered_by_name
scope = scope.for_namespaces(namespaces_scope) if namespaces_scope
scope
end
private
def namespaces_scope
@namespaces_scope ||= begin
parent_namespace = params[:parent_namespace]
if params[:direct_descendants_only]
parent_namespace ? [parent_namespace] + parent_namespace.children : ::Group.top_most
else
parent_namespace ? parent_namespace.self_and_descendants : nil
end
end
end
end
end
end
Loading
Loading
@@ -60,9 +60,9 @@ module MutationType
mount_mutation ::Mutations::DastSiteTokens::Create
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
mount_mutation ::Mutations::QualityManagement::TestCases::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
Loading
Loading
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Create < BaseMutation
include Mixins::CommonMethods
graphql_name 'CreateDevopsAdoptionSegment'
argument :namespace_id, ::Types::GlobalIDType[::Namespace],
required: true,
description: 'Namespace ID to set for the segment.'
field :segment,
Types::Admin::Analytics::DevopsAdoption::SegmentType,
null: true,
description: 'The segment after mutation.'
def resolve(namespace_id:, **)
namespace = namespace_id.find
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
response = service.execute
resolve_segment(response)
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Delete < BaseMutation
include Mixins::CommonMethods
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: "One or many IDs of the segments to delete."
def resolve(id:, **)
segments = GlobalID::Locator.locate_many(id)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
module Mixins
# This module ensures that the mutations are admin only
module CommonMethods
ADMIN_MESSAGE = 'You must be an admin to use this mutation'
FEATURE_UNAVAILABLE_MESSAGE = 'Feature is not available'
def ready?(**args)
unless License.feature_available?(:instance_level_devops_adoption)
raise_resource_not_available_error!(FEATURE_UNAVAILABLE_MESSAGE)
end
super
end
private
def resolve_segment(response)
segment = response.payload.fetch(:segment)
{
segment: response.success? ? response.payload.fetch(:segment) : nil,
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!(ADMIN_MESSAGE)
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class Create < BaseMutation
include Mixins::CommonMethods
graphql_name 'CreateDevopsAdoptionSegment'
argument :namespace_id, ::Types::GlobalIDType[::Namespace],
required: true,
description: 'Namespace ID to set for the segment.'
field :segment,
Types::Admin::Analytics::DevopsAdoption::SegmentType,
null: true,
description: 'The segment after mutation.'
def resolve(namespace_id:, **)
namespace = namespace_id.find
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
response = service.execute
resolve_segment(response)
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class Delete < BaseMutation
include Mixins::CommonMethods
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: 'One or many IDs of the segments to delete.'
def resolve(id:, **)
segments = GlobalID::Locator.locate_many(id)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
module Mixins
module CommonMethods
private
def resolve_segment(response)
segment = response.payload.fetch(:segment)
{
segment: response.success? ? response.payload.fetch(:segment) : nil,
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!
end
end
end
end
end
end
end
Loading
Loading
@@ -6,31 +6,44 @@ module Analytics
module DevopsAdoption
class SegmentsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include Gitlab::Allowable
 
type Types::Admin::Analytics::DevopsAdoption::SegmentType, null: true
 
def resolve
authorize!
argument :parent_namespace_id, ::Types::GlobalIDType[::Namespace],
required: false,
description: 'Filter by ancestor namespace.'
 
if segments_feature_available?
::Analytics::DevopsAdoption::Segment.ordered_by_name
else
::Analytics::DevopsAdoption::Segment.none
end
argument :direct_descendants_only, ::GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Limits segments to direct descendants of specified parent.'
def resolve(parent_namespace_id: nil, direct_descendants_only: false, **)
parent = GlobalID::Locator.locate(parent_namespace_id) if parent_namespace_id
authorize!(parent)
::Analytics::DevopsAdoption::SegmentsFinder.new(current_user, params: {
parent_namespace: parent, direct_descendants_only: direct_descendants_only
}).execute
end
 
private
 
def segments_feature_available?
License.feature_available?(:instance_level_devops_adoption)
def authorize!(parent)
parent ? authorize_with_namespace!(parent) : authorize_global!
end
 
def authorize!
admin? || raise_resource_not_available_error!
def authorize_global!
unless can?(current_user, :view_instance_devops_adoption)
raise_resource_not_available_error!
end
end
 
def admin?
context[:current_user].present? && context[:current_user].admin?
def authorize_with_namespace!(parent)
unless can?(current_user, :view_group_devops_adoption, parent)
raise_resource_not_available_error!
end
end
end
end
Loading
Loading
Loading
Loading
@@ -12,4 +12,5 @@ class Analytics::DevopsAdoption::Segment < ApplicationRecord
validates :namespace, uniqueness: true, presence: true
 
scope :ordered_by_name, -> { includes(:namespace).order('"namespaces"."name" ASC') }
scope :for_namespaces, -> (namespaces) { where(namespace_id: namespaces) }
end
Loading
Loading
@@ -26,6 +26,7 @@ class License < ApplicationRecord
group_webhooks
group_level_devops_adoption
instance_level_devops_adoption
group_level_devops_adoption
issuable_default_templates
issue_weights
iterations
Loading
Loading
Loading
Loading
@@ -21,13 +21,21 @@ module GlobalPolicy
::License.feature_available?(:export_user_permissions)
end
 
condition(:instance_devops_adoption_available) do
::License.feature_available?(:instance_level_devops_adoption)
end
rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
 
rule { admin & instance_devops_adoption_available }.policy do
enable :manage_devops_adoption_segments
enable :view_instance_devops_adoption
end
rule { admin }.policy do
enable :read_licenses
enable :destroy_licenses
enable :read_all_geo
enable :manage_devops_adoption_segments
enable :manage_subscription
end
 
Loading
Loading
Loading
Loading
@@ -197,6 +197,7 @@ module GroupPolicy
end
 
rule { reporter & group_devops_adoption_available }.policy do
enable :manage_devops_adoption_segments
enable :view_group_devops_adoption
end
 
Loading
Loading
Loading
Loading
@@ -4,21 +4,19 @@ module Analytics
module DevopsAdoption
module Segments
class BulkDeleteService
include CommonMethods
def initialize(segments:, current_user:)
@segments = segments
@current_user = current_user
end
 
def execute
authorize!
deletion_services.map(&:authorize!)
 
result = nil
 
ActiveRecord::Base.transaction do
segments.each do |segment|
response = delete_segment(segment)
deletion_services.each do |service|
response = service.execute
 
if response.error?
result = ServiceResponse.error(message: response.message, payload: response_payload)
Loading
Loading
@@ -40,8 +38,10 @@ def response_payload
{ segments: segments }
end
 
def delete_segment(segment)
DeleteService.new(current_user: current_user, segment: segment).execute
def deletion_services
@deletion_services ||= segments.map do |segment|
DeleteService.new(current_user: current_user, segment: segment)
end
end
end
end
Loading
Loading
Loading
Loading
@@ -4,8 +4,6 @@ module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreateService
include CommonMethods
def initialize(params: {}, current_user:)
@params = params
@current_user = current_user
Loading
Loading
@@ -14,20 +12,26 @@ def initialize(params: {}, current_user:)
def execute
authorize!
 
segments = params[:namespaces].map do |namespace|
response = FindOrCreateService
.new(current_user: current_user, params: { namespace: namespace })
.execute
response.payload[:segment]
segments = services.map do |service|
service.execute.payload[:segment]
end
 
ServiceResponse.success(payload: { segments: segments })
end
 
def authorize!
services.each(&:authorize!)
end
private
 
attr_reader :params, :current_user
def services
@services ||= params[:namespaces].map do |namespace|
FindOrCreateService.new(current_user: current_user, params: { namespace: namespace })
end
end
end
end
end
Loading
Loading
Loading
Loading
@@ -7,7 +7,7 @@ module CommonMethods
include Gitlab::Allowable
 
def authorize!
unless can?(current_user, :manage_devops_adoption_segments, :global)
unless can?(current_user, :manage_devops_adoption_segments, namespace)
raise AuthorizationError.new(self, 'Forbidden')
end
end
Loading
Loading
Loading
Loading
@@ -15,7 +15,7 @@ def initialize(segment: Analytics::DevopsAdoption::Segment.new, params: {}, curr
def execute
authorize!
 
segment.assign_attributes(attributes)
segment.assign_attributes(namespace: namespace)
 
if segment.save
Analytics::DevopsAdoption::CreateSnapshotWorker.perform_async(segment.id)
Loading
Loading
@@ -34,8 +34,8 @@ def response_payload
{ segment: segment }
end
 
def attributes
params.slice(:namespace, :namespace_id)
def namespace
params[:namespace]
end
end
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment