Skip to content
Snippets Groups Projects
Verified Commit 2acf4411 authored by Abdul Wadood's avatar Abdul Wadood :two: Committed by GitLab
Browse files

Add accessLevel & isLastOrganizationOwner fields to organizationUser

We are adding these fields to the `organizationUser` GraphQL query so
check if the current user is the last owner of the organization to see
if the user can leave the organization or not.

Changelog: added
parent 118c1e07
No related branches found
No related tags found
1 merge request!148148Add accessLevel & isLastOrganizationOwner fields to organizationUser
Showing
with 242 additions and 5 deletions
# frozen_string_literal: true
module Types
module Organizations
class OrganizationUserAccessLevelEnum < BaseEnum
graphql_name 'OrganizationUserAccessLevel'
description 'Access level of an organization user'
value 'DEFAULT', value: Gitlab::Access::GUEST, description: 'Guest access.', alpha: { milestone: '16.11' }
value 'OWNER', value: Gitlab::Access::OWNER, description: 'Owner access.', alpha: { milestone: '16.11' }
end
end
end
# frozen_string_literal: true
module Types
module Organizations
# rubocop:disable Graphql/AuthorizeTypes -- -- Already authorized in parent OrganizationUserType.
class OrganizationUserAccessLevelType < Types::BaseObject
graphql_name 'OrganizationUserAccess'
description 'Represents the access level of a relationship between a User and Organization that it is related to'
field :integer_value, GraphQL::Types::Int,
description: 'Integer representation of access level.',
alpha: { milestone: '16.11' },
method: :to_i
field :string_value, Types::Organizations::OrganizationUserAccessLevelEnum,
description: 'String representation of access level.',
alpha: { milestone: '16.11' },
method: :to_i
end
# rubocop:enable Graphql/AuthorizeTypes
end
end
......@@ -12,6 +12,14 @@ class OrganizationUserType < BaseObject
alias_method :organization_user, :object
expose_permissions Types::PermissionTypes::OrganizationUser
field :access_level,
::Types::Organizations::OrganizationUserAccessLevelType,
null: false,
description: 'Access level of the user in the organization.',
alpha: { milestone: '16.11' },
method: :access_level_before_type_cast
field :badges,
[::Types::Organizations::OrganizationUserBadgeType],
null: true,
......@@ -22,6 +30,12 @@ class OrganizationUserType < BaseObject
null: false,
description: 'ID of the organization user.',
alpha: { milestone: '16.4' }
field :is_last_owner,
GraphQL::Types::Boolean,
null: false,
description: 'Whether the user is the last owner of the organization.',
alpha: { milestone: '16.11' },
method: :last_owner?
field :user,
::Types::UserType,
null: false,
......
# frozen_string_literal: true
module Types
module PermissionTypes
class OrganizationUser < BasePermissionType
graphql_name 'OrganizationUserPermissions'
abilities :remove_user
end
end
end
......@@ -19,6 +19,7 @@ class OrganizationUser < ApplicationRecord
scope :owners, -> { where(access_level: Gitlab::Access::OWNER) }
scope :in_organization, ->(organization) { where(organization: organization) }
scope :with_active_users, -> { joins(:user).merge(User.active) }
def self.create_default_organization_record_for(user_id, user_is_admin:)
upsert(
......@@ -69,6 +70,16 @@ def self.create_organization_record_for(user_id, organization_id)
)
end
def last_owner?
return false unless owner?
other_owners = organization.organization_users.owners.id_not_in(id)
# Try to keep the last active user as owner
return other_owners.with_active_users.empty? if user.active?
other_owners.empty?
end
private
def ensure_user_has_an_organization
......
......@@ -3,5 +3,9 @@
module Organizations
class OrganizationUserPolicy < BasePolicy
delegate :organization
condition(:last_owner?) { @subject.last_owner? }
rule { ~last_owner? }.enable :remove_user
end
end
......@@ -24653,9 +24653,23 @@ A user with access to the organization.
 
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="organizationuseraccesslevel"></a>`accessLevel` **{warning-solid}** | [`OrganizationUserAccess!`](#organizationuseraccess) | **Introduced** in GitLab 16.11. **Status**: Experiment. Access level of the user in the organization. |
| <a id="organizationuserbadges"></a>`badges` **{warning-solid}** | [`[OrganizationUserBadge!]`](#organizationuserbadge) | **Introduced** in GitLab 16.4. **Status**: Experiment. Badges describing the user within the organization. |
| <a id="organizationuserid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in GitLab 16.4. **Status**: Experiment. ID of the organization user. |
| <a id="organizationuserislastowner"></a>`isLastOwner` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Whether the user is the last owner of the organization. |
| <a id="organizationuseruser"></a>`user` **{warning-solid}** | [`UserCore!`](#usercore) | **Introduced** in GitLab 16.4. **Status**: Experiment. User that is associated with the organization. |
| <a id="organizationuseruserpermissions"></a>`userPermissions` | [`OrganizationUserPermissions!`](#organizationuserpermissions) | Permissions for the current user on the resource. |
### `OrganizationUserAccess`
Represents the access level of a relationship between a User and Organization that it is related to.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="organizationuseraccessintegervalue"></a>`integerValue` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 16.11. **Status**: Experiment. Integer representation of access level. |
| <a id="organizationuseraccessstringvalue"></a>`stringValue` **{warning-solid}** | [`OrganizationUserAccessLevel`](#organizationuseraccesslevel) | **Introduced** in GitLab 16.11. **Status**: Experiment. String representation of access level. |
 
### `OrganizationUserBadge`
 
......@@ -24668,6 +24682,14 @@ An organization user badge.
| <a id="organizationuserbadgetext"></a>`text` | [`String!`](#string) | Badge text. |
| <a id="organizationuserbadgevariant"></a>`variant` | [`String!`](#string) | Badge variant. |
 
### `OrganizationUserPermissions`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="organizationuserpermissionsremoveuser"></a>`removeUser` | [`Boolean!`](#boolean) | If `true`, the user can perform `remove_user` on this resource. |
### `Package`
 
Represents a package with pipelines in the Package Registry.
......@@ -32627,6 +32649,15 @@ Values for sorting organizations.
| <a id="organizationsortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `UPDATED_ASC`. |
| <a id="organizationsortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `UPDATED_DESC`. |
 
### `OrganizationUserAccessLevel`
Access level of an organization user.
| Value | Description |
| ----- | ----------- |
| <a id="organizationuseraccessleveldefault"></a>`DEFAULT` **{warning-solid}** | **Introduced** in GitLab 16.11. **Status**: Experiment. Guest access. |
| <a id="organizationuseraccesslevelowner"></a>`OWNER` **{warning-solid}** | **Introduced** in GitLab 16.11. **Status**: Experiment. Owner access. |
### `PackageDependencyType`
 
| Value | Description |
......@@ -8,5 +8,7 @@
trait :owner do
access_level { Gitlab::Access::OWNER }
end
factory :organization_owner, traits: [:owner]
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Organizations::OrganizationUserAccessLevelEnum, feature_category: :cell do
specify { expect(described_class.graphql_name).to eq('OrganizationUserAccessLevel') }
it 'exposes all the existing access levels' do
expect(described_class.values.keys).to include(*%w[DEFAULT OWNER])
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['OrganizationUserAccess'], feature_category: :cell do
specify { expect(described_class.graphql_name).to eq('OrganizationUserAccess') }
specify { expect(described_class).to require_graphql_authorizations(nil) }
it 'has expected fields' do
expected_fields = [:integer_value, :string_value]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['OrganizationUser'], feature_category: :cell do
let(:expected_fields) { %w[badges id user] }
let(:expected_fields) { %w[access_level badges id is_last_owner user user_permissions] }
specify { expect(described_class.graphql_name).to eq('OrganizationUser') }
specify { expect(described_class).to require_graphql_authorizations(:read_organization_user) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::PermissionTypes::OrganizationUser, feature_category: :cell do
it do
expected_permissions = [
:remove_user
]
expected_permissions.each do |permission|
expect(described_class).to have_graphql_field(permission)
end
end
end
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Organizations::OrganizationUser, type: :model, feature_category: :cell do
using RSpec::Parameterized::TableSyntax
describe 'associations' do
it { is_expected.to belong_to(:organization).inverse_of(:organization_users).required }
it { is_expected.to belong_to(:user).inverse_of(:organization_users).required }
......@@ -91,6 +93,15 @@
it { is_expected.to match_array(organization_users) }
end
describe '#with_active_users' do
let_it_be(:active_organization_user) { create(:organization_user) }
let_it_be(:inactive_organization_user) { create(:organization_user) { |org_user| org_user.user.block! } }
subject(:active_user) { described_class.with_active_users }
it { is_expected.to include(active_organization_user).and exclude(inactive_organization_user) }
end
end
it_behaves_like 'having unique enum values'
......@@ -250,4 +261,43 @@
end
end
end
describe '#last_owner?' do
subject(:last_owner?) { organization_user.last_owner? }
context 'when user is not the owner' do
let(:organization_user) { build(:organization_user) }
it { is_expected.to eq(false) }
end
context 'when user is the owner' do
let_it_be(:organization_user, reload: true) { create(:organization_owner) }
let_it_be(:organization) { organization_user.organization }
context 'when another owner does not exist' do
it { is_expected.to eq(true) }
end
context 'when another owner exists' do
let_it_be(:another_owner, reload: true) { create(:organization_owner, organization: organization) }
where(:current_owner_active?, :another_owner_active?, :last_owner?) do
true | true | false
true | false | true
false | true | false
false | false | false
end
with_them do
before do
organization_user.user.block! unless current_owner_active?
another_owner.user.block! unless another_owner_active?
end
it { is_expected.to eq(last_owner?) }
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Organizations::OrganizationUserPolicy, feature_category: :cell do
let_it_be(:organization) { create(:organization) }
let_it_be(:current_user) { create :user }
subject(:policy) { described_class.new(current_user, organization_user) }
context 'when the user is not an owner' do
let(:organization_user) { build(:organization_user, organization: organization, user: current_user) }
it { is_expected.to be_allowed(:remove_user) }
end
context 'when the user is last owner' do
let(:organization_user) { build(:organization_user, :owner, organization: organization, user: current_user) }
it { is_expected.to be_disallowed(:remove_user) }
end
context 'when the user is not last owner' do
let(:organization_user) { build(:organization_user, :owner, organization: organization, user: current_user) }
before do
create(:organization_user, :owner, organization: organization)
end
it { is_expected.to be_allowed(:remove_user) }
end
end
......@@ -20,9 +20,9 @@
FIELDS
end
let_it_be(:organization_user) { create(:organization_user) }
let_it_be(:organization) { organization_user.organization }
let_it_be(:user) { organization_user.user }
let_it_be(:organization_owner) { create(:organization_owner) }
let_it_be(:organization) { organization_owner.organization }
let_it_be(:user) { organization_owner.user }
let_it_be(:project) { create(:project, organization: organization) { |p| p.add_developer(user) } }
let_it_be(:other_group) do
create(:group, name: 'other-group', organization: organization) { |g| g.add_developer(user) }
......@@ -64,11 +64,16 @@
<<~FIELDS
organizationUsers {
nodes {
accessLevel {
integerValue
stringValue
}
badges {
text
variant
}
id
isLastOwner
user {
id
}
......@@ -82,8 +87,10 @@
organization_user_nodes = graphql_data_at(:organization, :organizationUsers, :nodes)
expected_attributes = {
"accessLevel" => { "integerValue" => 50, "stringValue" => "OWNER" },
"badges" => [{ "text" => "It's you!", "variant" => 'muted' }],
"id" => organization_user.to_global_id.to_s,
"id" => organization_owner.to_global_id.to_s,
"isLastOwner" => true,
"user" => { "id" => user.to_global_id.to_s }
}
expect(organization_user_nodes).to include(expected_attributes)
......
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