Commit 386abf71 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0ec5d3f1 1ebcfec5
......@@ -247,16 +247,6 @@ $gl-line-height-42: px-to-rem(42px);
max-width: 50%;
}
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
.gl-popover {
.popover-header {
.gl-button.close {
margin-top: -$gl-spacing-scale-3;
margin-right: -$gl-spacing-scale-4;
}
}
}
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1490
.gl-w-grid-size-28 {
width: $grid-size * 28;
......
......@@ -9,10 +9,17 @@ module Resolvers
delegate :project, to: :agent
argument :status, Types::Clusters::AgentTokenStatusEnum,
required: false,
description: 'Status of the token.'
def resolve(**args)
return ::Clusters::AgentToken.none unless can_read_agent_tokens?
agent.last_used_agent_tokens
tokens = agent.last_used_agent_tokens
tokens = tokens.with_status(args[:status]) if args[:status].present?
tokens
end
private
......
# frozen_string_literal: true
module Types
module Clusters
class AgentTokenStatusEnum < BaseEnum
graphql_name 'AgentTokenStatus'
description 'Agent token statuses'
::Clusters::AgentToken.statuses.keys.each do |status|
value status.upcase, value: status, description: "#{status.titleize} agent token."
end
end
end
end
......@@ -45,7 +45,7 @@ module Types
description: 'Name given to the token.'
field :status,
GraphQL::Types::String,
Types::Clusters::AgentTokenStatusEnum,
null: true,
description: 'Current status of the token.'
......
......@@ -22,6 +22,7 @@ module Clusters
validates :name, presence: true, length: { maximum: 255 }
scope :order_last_used_at_desc, -> { order(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
scope :with_status, -> (status) { where(status: status) }
enum status: {
active: 0,
......
......@@ -12,9 +12,6 @@ class NamespaceSetting < ApplicationRecord
validate :allow_mfa_for_group
validate :allow_resource_access_token_creation_for_group
before_save :set_prevent_sharing_groups_outside_hierarchy, if: -> { user_cap_enabled? }
after_save :disable_project_sharing!, if: -> { user_cap_enabled? }
before_validation :normalize_default_branch_name
enum jobs_to_be_done: { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5 }, _suffix: true
......@@ -59,18 +56,6 @@ class NamespaceSetting < ApplicationRecord
errors.add(:resource_access_token_creation_allowed, _('is not allowed since the group is not top-level group.'))
end
end
def set_prevent_sharing_groups_outside_hierarchy
self.prevent_sharing_groups_outside_hierarchy = true
end
def disable_project_sharing!
namespace.update_attribute(:share_with_group_lock, true)
end
def user_cap_enabled?
new_user_signups_cap.present? && namespace.root?
end
end
NamespaceSetting.prepend_mod_with('NamespaceSetting')
......@@ -9027,10 +9027,27 @@ GitLab CI/CD configuration template.
| <a id="clusteragentid"></a>`id` | [`ID!`](#id) | ID of the cluster agent. |
| <a id="clusteragentname"></a>`name` | [`String`](#string) | Name of the cluster agent. |
| <a id="clusteragentproject"></a>`project` | [`Project`](#project) | Project this cluster agent is associated with. |
| <a id="clusteragenttokens"></a>`tokens` | [`ClusterAgentTokenConnection`](#clusteragenttokenconnection) | Tokens associated with the cluster agent. (see [Connections](#connections)) |
| <a id="clusteragentupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp the cluster agent was updated. |
| <a id="clusteragentwebpath"></a>`webPath` | [`String`](#string) | Web path of the cluster agent. |
#### Fields with arguments
##### `ClusterAgent.tokens`
Tokens associated with the cluster agent.
Returns [`ClusterAgentTokenConnection`](#clusteragenttokenconnection).
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="clusteragenttokensstatus"></a>`status` | [`AgentTokenStatus`](#agenttokenstatus) | Status of the token. |
### `ClusterAgentActivityEvent`
#### Fields
......@@ -9056,7 +9073,7 @@ GitLab CI/CD configuration template.
| <a id="clusteragenttokenid"></a>`id` | [`ClustersAgentTokenID!`](#clustersagenttokenid) | Global ID of the token. |
| <a id="clusteragenttokenlastusedat"></a>`lastUsedAt` | [`Time`](#time) | Timestamp the token was last used. |
| <a id="clusteragenttokenname"></a>`name` | [`String`](#string) | Name given to the token. |
| <a id="clusteragenttokenstatus"></a>`status` | [`String`](#string) | Current status of the token. |
| <a id="clusteragenttokenstatus"></a>`status` | [`AgentTokenStatus`](#agenttokenstatus) | Current status of the token. |
### `CodeCoverageActivity`
......@@ -16150,6 +16167,15 @@ Access level to a resource.
| <a id="accesslevelenumowner"></a>`OWNER` | Owner access. |
| <a id="accesslevelenumreporter"></a>`REPORTER` | Reporter access. |
### `AgentTokenStatus`
Agent token statuses.
| Value | Description |
| ----- | ----------- |
| <a id="agenttokenstatusactive"></a>`ACTIVE` | Active agent token. |
| <a id="agenttokenstatusrevoked"></a>`REVOKED` | Revoked agent token. |
### `AlertManagementAlertSort`
Values for sorting alerts.
......@@ -523,6 +523,14 @@ module EE
user_cap <= members_count
end
def shared_externally?
strong_memoize(:shared_externally) do
invited_group_in_groups
.where.not(group_group_links: { shared_with_group_id: ::Group.groups_including_descendants_by([self]) })
.limit(1).any?
end
end
private
override :post_create_hook
......
......@@ -4,14 +4,47 @@ module EE
module NamespaceSetting
extend ActiveSupport::Concern
delegate :root_ancestor, to: :namespace
prepended do
validate :user_cap_allowed, if: -> { enabling_user_cap? }
def prevent_forking_outside_group?
saml_setting = root_ancestor.saml_provider&.prohibited_outer_forks?
before_save :set_prevent_sharing_groups_outside_hierarchy, if: -> { user_cap_enabled? }
after_save :disable_project_sharing!, if: -> { user_cap_enabled? }
return saml_setting unless namespace.feature_available?(:group_forking_protection)
delegate :root_ancestor, to: :namespace
saml_setting || root_ancestor.namespace_settings&.prevent_forking_outside_group
def prevent_forking_outside_group?
saml_setting = root_ancestor.saml_provider&.prohibited_outer_forks?
return saml_setting unless namespace.feature_available?(:group_forking_protection)
saml_setting || root_ancestor.namespace_settings&.prevent_forking_outside_group
end
private
def enabling_user_cap?
return false unless persisted? && new_user_signups_cap_changed?
new_user_signups_cap_was.nil?
end
def user_cap_allowed
return if namespace.user_cap_available? && namespace.root? && !namespace.shared_externally?
errors.add(:new_user_signups_cap, _("cannot be enabled"))
end
def set_prevent_sharing_groups_outside_hierarchy
self.prevent_sharing_groups_outside_hierarchy = true
end
def disable_project_sharing!
namespace.update_attribute(:share_with_group_lock, true)
end
def user_cap_enabled?
new_user_signups_cap.present? && namespace.root?
end
end
end
end
......@@ -629,6 +629,15 @@ RSpec.describe GroupsController do
end
context 'authenticated as group owner' do
before do
allow_next_found_instance_of(Group) do |group|
allow(group).to receive(:user_cap_available?).and_return(true)
end
group.add_owner(user)
sign_in(user)
end
where(:new_user_signups_cap, :result, :status) do
nil | nil | :found
10 | 10 | :found
......@@ -639,11 +648,6 @@ RSpec.describe GroupsController do
{ id: group.to_param, group: { new_user_signups_cap: new_user_signups_cap } }
end
before do
group.add_owner(user)
sign_in(user)
end
it_behaves_like 'updates the attribute'
end
end
......
......@@ -7,6 +7,7 @@ exports[`Corpus upload modal corpus modal uploading state does show the upload p
>
<span
class="gl-spinner-container"
role="status"
>
<span
aria-label="Loading"
......
......@@ -5,6 +5,7 @@ exports[`Security Dashboard default states sets up group-level 1`] = `
<div>
<div
class="gl-spinner-container gl-mt-6"
role="status"
>
<span
aria-label="Loading"
......@@ -20,6 +21,7 @@ exports[`Security Dashboard default states sets up instance-level 1`] = `
<div>
<div
class="gl-spinner-container gl-mt-6"
role="status"
>
<span
aria-label="Loading"
......
......@@ -1521,6 +1521,7 @@ RSpec.describe Group do
shared_examples 'returning the right value for user_cap_reached?' do
before do
allow(root_group).to receive(:user_cap_available?).and_return(true)
root_group.namespace_settings.update!(new_user_signups_cap: new_user_signups_cap)
end
......@@ -1571,6 +1572,43 @@ RSpec.describe Group do
end
end
describe '#shared_externally?' do
let_it_be(:group, refind: true) { create(:group) }
let_it_be(:subgroup_1) { create(:group, parent: group) }
let_it_be(:subgroup_2) { create(:group, parent: group) }
let_it_be(:external_group) { create(:group) }
subject(:shared_externally?) { group.shared_externally? }
it 'returns false when the group is not shared outside of the namespace hierarchy' do
expect(shared_externally?).to be false
end
it 'returns true when the group is shared outside of the namespace hierarchy' do
create(:group_group_link, shared_group: group, shared_with_group: external_group)
expect(shared_externally?).to be true
end
it 'returns false when the group is shared internally within the namespace hierarchy' do
create(:group_group_link, shared_group: subgroup_1, shared_with_group: subgroup_2)
expect(shared_externally?).to be false
end
it 'returns true when a subgroup is shared outside of the namespace hierarchy' do
create(:group_group_link, shared_group: subgroup_1, shared_with_group: external_group)
expect(shared_externally?).to be true
end
it 'returns false when the only shared groups are outside of the namespace hierarchy' do
create(:group_group_link)
expect(shared_externally?).to be false
end
end
it_behaves_like 'can move repository storage' do
let_it_be(:container) { create(:group, :wiki_repo) }
......
......@@ -96,4 +96,128 @@ RSpec.describe NamespaceSetting do
end
end
end
context 'validating new_user_signup_cap' do
using RSpec::Parameterized::TableSyntax
where(:feature_available, :old_value, :new_value, :expectation) do
true | nil | 10 | true
true | 0 | 10 | true
true | 0 | 0 | true
false | nil | 10 | false
false | 10 | 10 | true
end
with_them do
let(:setting) { build(:namespace_settings, new_user_signups_cap: old_value) }
let(:group) { create(:group, namespace_settings: setting) }
before do
allow(group).to receive(:user_cap_available?).and_return feature_available
setting.new_user_signups_cap = new_value
end
it 'returns the expected response' do
expect(setting.valid?).to be expectation
expect(setting.errors.messages[:new_user_signups_cap]).to include("cannot be enabled") unless expectation
end
end
context 'when enabling the setting' do
let(:feature_available) { true }
before do
allow(group).to receive(:user_cap_available?).and_return feature_available
setting.new_user_signups_cap = 10
end
shared_examples 'user cap is not available' do
it 'is invalid' do
expect(setting.valid?).to be false
expect(setting.errors.messages[:new_user_signups_cap]).to include("cannot be enabled")
end
end
context 'when the group is a subgroup' do
before do
group.parent = build(:group)
end
it_behaves_like 'user cap is not available'
end
context 'when the group is shared externally' do
before do
create(:group_group_link, shared_group: group)
end
it_behaves_like 'user cap is not available'
end
context 'when the namespace is a user' do
let(:user) { create(:user) }
let(:setting) { user.namespace.namespace_settings }
it_behaves_like 'user cap is not available'
end
end
end
context 'hooks related to group user cap update' do
let(:group) { create(:group) }
let(:settings) { group.namespace_settings }
before do
allow(group).to receive(:root?).and_return(true)
allow(group).to receive(:user_cap_available?).and_return(true)
group.namespace_settings.update!(new_user_signups_cap: user_cap)
end
context 'when updating a group with a user cap' do
let(:user_cap) { nil }
it 'also sets share_with_group_lock and prevent_sharing_groups_outside_hierarchy to true' do
expect(group.new_user_signups_cap).to be_nil
expect(group.share_with_group_lock).to be_falsey
expect(settings.prevent_sharing_groups_outside_hierarchy).to be_falsey
settings.update!(new_user_signups_cap: 10)
group.reload
expect(group.new_user_signups_cap).to eq(10)
expect(group.share_with_group_lock).to be_truthy
expect(settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
end
it 'has share_with_group_lock and prevent_sharing_groups_outside_hierarchy returning true for descendent groups' do
descendent = create(:group, parent: group)
desc_settings = descendent.namespace_settings
expect(descendent.share_with_group_lock).to be_falsey
expect(desc_settings.prevent_sharing_groups_outside_hierarchy).to be_falsey
settings.update!(new_user_signups_cap: 10)
expect(descendent.reload.share_with_group_lock).to be_truthy
expect(desc_settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
end
end
context 'when removing a user cap from namespace settings' do
let(:user_cap) { 10 }
it 'leaves share_with_group_lock and prevent_sharing_groups_outside_hierarchy set to true to the related group' do
expect(group.share_with_group_lock).to be_truthy
expect(settings.prevent_sharing_groups_outside_hierarchy).to be_truthy
settings.update!(new_user_signups_cap: nil)
expect(group.reload.share_with_group_lock).to be_truthy
expect(settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
end
end
end
end
......@@ -41541,6 +41541,9 @@ msgstr ""
msgid "cannot be changed if shared runners are enabled"
msgstr ""
msgid "cannot be enabled"
msgstr ""
msgid "cannot be enabled because parent group does not allow it"
msgstr ""
......
......@@ -7,5 +7,9 @@ FactoryBot.define do
token_encrypted { Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(50)) }
sequence(:name) { |n| "agent-token-#{n}" }
trait :revoked do
status { :revoked }
end
end
end
......@@ -7,6 +7,7 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
it { expect(described_class.type).to eq(Types::Clusters::AgentTokenType) }
it { expect(described_class.null).to be_truthy }
it { expect(described_class.arguments.keys).to contain_exactly('status') }
describe '#resolve' do
let(:agent) { create(:cluster_agent) }
......@@ -23,6 +24,14 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
expect(subject).to eq([matching_token2, matching_token1])
end
context 'token status is specified' do
let!(:revoked_token) { create(:cluster_agent_token, :revoked, agent: agent) }
subject { resolve(described_class, obj: agent, ctx: ctx, args: { status: 'revoked' }) }
it { is_expected.to contain_exactly(revoked_token) }
end
context 'user does not have permission' do
let(:user) { create(:user, developer_projects: [agent.project]) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Clusters::AgentTokenStatusEnum do
it { expect(described_class.graphql_name).to eq('AgentTokenStatus') }
it { expect(described_class.values.keys).to match_array(Clusters::AgentToken.statuses.keys.map(&:upcase)) }
end
......@@ -13,15 +13,25 @@ RSpec.describe Clusters::AgentToken do
describe 'scopes' do
describe '.order_last_used_at_desc' do
let_it_be(:token_1) { create(:cluster_agent_token, last_used_at: 7.days.ago) }
let_it_be(:token_2) { create(:cluster_agent_token, last_used_at: nil) }
let_it_be(:token_3) { create(:cluster_agent_token, last_used_at: 2.days.ago) }
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:token_1) { create(:cluster_agent_token, agent: agent, last_used_at: 7.days.ago) }
let_it_be(:token_2) { create(:cluster_agent_token, agent: agent, last_used_at: nil) }
let_it_be(:token_3) { create(:cluster_agent_token, agent: agent, last_used_at: 2.days.ago) }
it 'sorts by last_used_at descending, with null values at last' do
expect(described_class.order_last_used_at_desc)
.to eq([token_3, token_1, token_2])
end
end
describe '.with_status' do
let!(:active_token) { create(:cluster_agent_token) }
let!(:revoked_token) { create(:cluster_agent_token, :revoked) }
subject { described_class.with_status(:active) }
it { is_expected.to contain_exactly(active_token) }
end
end
describe '#token' do
......
......@@ -126,57 +126,4 @@ RSpec.describe NamespaceSetting, type: :model do
end
end
end
describe 'hooks related to group user cap update' do
let(:settings) { create(:namespace_settings, new_user_signups_cap: user_cap) }
let(:group) { create(:group, namespace_settings: settings) }
before do
allow(group).to receive(:root?).and_return(true)
end
context 'when updating a group with a user cap' do
let(:user_cap) { nil }
it 'also sets share_with_group_lock and prevent_sharing_groups_outside_hierarchy to true' do
expect(group.new_user_signups_cap).to be_nil
expect(group.share_with_group_lock).to be_falsey
expect(settings.prevent_sharing_groups_outside_hierarchy).to be_falsey
settings.update!(new_user_signups_cap: 10)
group.reload
expect(group.new_user_signups_cap).to eq(10)
expect(group.share_with_group_lock).to be_truthy
expect(settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
end
it 'has share_with_group_lock and prevent_sharing_groups_outside_hierarchy returning true for descendent groups' do
descendent = create(:group, parent: group)
desc_settings = descendent.namespace_settings
expect(descendent.share_with_group_lock).to be_falsey
expect(desc_settings.prevent_sharing_groups_outside_hierarchy).to be_falsey
settings.update!(new_user_signups_cap: 10)
expect(descendent.reload.share_with_group_lock).to be_truthy
expect(desc_settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
end
end
context 'when removing a user cap from namespace settings' do
let(:user_cap) { 10 }
it 'leaves share_with_group_lock and prevent_sharing_groups_outside_hierarchy set to true to the related group' do
expect(group.share_with_group_lock).to be_truthy
expect(settings.prevent_sharing_groups_outside_hierarchy).to be_truthy
settings.update!(new_user_signups_cap: nil)
expect(group.reload.share_with_group_lock).to be_truthy
expect(settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
end
end
end
end
......@@ -914,20 +914,20 @@
stylelint-declaration-strict-value "1.7.7"
stylelint-scss "3.18.0"
"@gitlab/svgs@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.0.0.tgz#06af5e91c36498ccf7e3e30e432eefcb3b1276c2"
integrity sha512-kBq7RZ0N+h41b4JbPOmwzx1X++fD+tz8HhaBmHTkOmRFY/7Ygvt2A8GodUUtpFK/NxRxy8O+knZvLNdfMLAIoQ==
"@gitlab/svgs@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.2.0.tgz#95cf58d6ae634d535145159f08f5cff6241d4013"
integrity sha512-mCwR3KfNPsxRoojtTjMIZwdd4FFlBh5DlR9AeodP+7+k8rILdWGYxTZbJMPNXoPbZx16R94nG8c5bR7toD4QBw==
"@gitlab/tributejs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@32.51.3":
version "32.51.3"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.51.3.tgz#1ba4802a16ecf5465774f4083e5ec1ed6adc4579"
integrity sha512-PWC0FtpsW9SM3O935XPU2el/JtLtvHQCL9qblKCeR98eJNYmIpjrw45ow+01jsqpjufcUI49n4id/sYHq8b6og==
"@gitlab/ui@32.54.0":
version "32.54.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.54.0.tgz#86002a6796bdd68fd54cd01e11292d2b29f361f7"
integrity sha512-a1QUbQ3KQtmmenOaSGMp8clwi0o8H/u2c8Q1s9EbWHuXN3UwVnhxP+0ncNUpdTlHyEPn4UbYpAZOowtnikXn8Q==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "2.20.1"
......
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