Skip to content
Snippets Groups Projects
Commit 7738cbae authored by charlie ablett's avatar charlie ablett :tools:
Browse files

Merge branch 'refresh-user-access-authorizations' into 'master'

Persist user_access authorizations

See merge request !116902



Merged-by: charlie ablett's avatarcharlie ablett <cablett@gitlab.com>
Approved-by: default avatarTiger Watson <twatson@gitlab.com>
Approved-by: charlie ablett's avatarcharlie ablett <cablett@gitlab.com>
Approved-by: default avatarPam Artiaga <partiaga@gitlab.com>
Reviewed-by: default avatarShinya Maeda <shinya@gitlab.com>
Reviewed-by: default avatarTiger Watson <twatson@gitlab.com>
Reviewed-by: default avatarMehmet Emin INAC <minac@gitlab.com>
Reviewed-by: default avatarPam Artiaga <partiaga@gitlab.com>
Co-authored-by: default avatarShinya Maeda <shinya@gitlab.com>
parents ce6212f2 0aa6ee82
No related branches found
No related tags found
2 merge requests!118700Remove refactor_vulnerability_filters feature flag,!116902Persist user_access authorizations
Pipeline #841698527 passed
......@@ -18,6 +18,12 @@ class Agent < ApplicationRecord
has_many :ci_access_project_authorizations, class_name: 'Clusters::Agents::Authorizations::CiAccess::ProjectAuthorization'
has_many :ci_access_authorized_projects, class_name: '::Project', through: :ci_access_project_authorizations, source: :project
has_many :user_access_group_authorizations, class_name: 'Clusters::Agents::Authorizations::UserAccess::GroupAuthorization'
has_many :user_access_authorized_groups, class_name: '::Group', through: :user_access_group_authorizations, source: :group
has_many :user_access_project_authorizations, class_name: 'Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization'
has_many :user_access_authorized_projects, class_name: '::Project', through: :user_access_project_authorizations, source: :project
has_many :activity_events, -> { in_timeline_order }, class_name: 'Clusters::Agents::ActivityEvent', inverse_of: :agent
scope :ordered_by_name, -> { order(:name) }
......
......@@ -15,6 +15,16 @@ class GroupAuthorization < ApplicationRecord
def config_project
agent.project
end
class << self
def upsert_configs(configs)
upsert_all(configs, unique_by: [:agent_id, :group_id])
end
def delete_unlisted(group_ids)
where.not(group_id: group_ids).delete_all
end
end
end
end
end
......
......@@ -15,6 +15,16 @@ class ProjectAuthorization < ApplicationRecord
def config_project
agent.project
end
class << self
def upsert_configs(configs)
upsert_all(configs, unique_by: [:agent_id, :project_id])
end
def delete_unlisted(project_ids)
where.not(project_id: project_ids).delete_all
end
end
end
end
end
......
# frozen_string_literal: true
module Clusters
module Agents
module Authorizations
module UserAccess
class RefreshService
include Gitlab::Utils::StrongMemoize
AUTHORIZED_ENTITY_LIMIT = 100
delegate :project, to: :agent, private: true
delegate :root_ancestor, to: :project, private: true
def initialize(agent, config:)
@agent = agent
@config = config
end
def execute
refresh_projects!
refresh_groups!
true
end
private
attr_reader :agent, :config
def refresh_projects!
if allowed_project_configurations.present?
project_ids = allowed_project_configurations.map { |config| config.fetch(:project_id) }
agent.with_lock do
agent.user_access_project_authorizations.upsert_configs(allowed_project_configurations)
agent.user_access_project_authorizations.delete_unlisted(project_ids)
end
else
agent.user_access_project_authorizations.delete_all(:delete_all)
end
end
def refresh_groups!
if allowed_group_configurations.present?
group_ids = allowed_group_configurations.map { |config| config.fetch(:group_id) }
agent.with_lock do
agent.user_access_group_authorizations.upsert_configs(allowed_group_configurations)
agent.user_access_group_authorizations.delete_unlisted(group_ids)
end
else
agent.user_access_group_authorizations.delete_all(:delete_all)
end
end
def allowed_project_configurations
project_entries = extract_config_entries(entity: 'projects')
return unless project_entries
allowed_projects.where_full_path_in(project_entries.keys).map do |project|
{ project_id: project.id, config: user_access_as }
end
end
strong_memoize_attr :allowed_project_configurations
def allowed_group_configurations
group_entries = extract_config_entries(entity: 'groups')
return unless group_entries
allowed_groups.where_full_path_in(group_entries.keys).map do |group|
{ group_id: group.id, config: user_access_as }
end
end
strong_memoize_attr :allowed_group_configurations
def extract_config_entries(entity:)
config.dig('user_access', entity)
&.first(AUTHORIZED_ENTITY_LIMIT)
&.index_by { |config| config.delete('id').downcase }
end
def allowed_projects
root_ancestor.all_projects
end
def allowed_groups
if group_root_ancestor?
root_ancestor.self_and_descendants
else
::Group.none
end
end
def group_root_ancestor?
root_ancestor.group_namespace?
end
def user_access_as
@user_access_as ||= config['user_access']&.slice('access_as') || {}
end
end
end
end
end
end
......@@ -130,6 +130,7 @@ def increment_count_events
agent = ::Clusters::Agent.find(params[:agent_id])
::Clusters::Agents::Authorizations::CiAccess::RefreshService.new(agent, config: params[:agent_config]).execute
::Clusters::Agents::Authorizations::UserAccess::RefreshService.new(agent, config: params[:agent_config]).execute
no_content!
end
......
......@@ -147,6 +147,14 @@ def send_request(headers: {}, params: {})
projects: [
{ id: project.full_path, default_namespace: 'staging' }
]
},
user_access: {
groups: [
{ id: group.full_path }
],
projects: [
{ id: project.full_path }
]
}
}
end
......@@ -160,6 +168,8 @@ def send_request(headers: {}, params: {})
expect(response).to have_gitlab_http_status(:no_content)
expect(agent.ci_access_authorized_groups).to contain_exactly(group)
expect(agent.ci_access_authorized_projects).to contain_exactly(project)
expect(agent.user_access_authorized_groups).to contain_exactly(group)
expect(agent.user_access_authorized_projects).to contain_exactly(project)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::Agents::Authorizations::UserAccess::RefreshService, feature_category: :kubernetes_management do
describe '#execute' do
let_it_be(:root_ancestor) { create(:group) }
let_it_be(:agent_management_project) { create(:project, namespace: root_ancestor) }
let_it_be(:group_1) { create(:group, path: 'group-path-with-UPPERCASE', parent: root_ancestor) }
let_it_be(:group_2) { create(:group, parent: root_ancestor) }
let_it_be(:project_1) { create(:project, path: 'project-path-with-UPPERCASE', namespace: root_ancestor) }
let_it_be(:project_2) { create(:project, namespace: root_ancestor) }
let(:agent) { create(:cluster_agent, project: agent_management_project) }
let(:config) do
{
user_access: {
groups: [
{ id: group_2.full_path }
],
projects: [
{ id: project_2.full_path }
]
}
}.deep_merge(extra_config).deep_stringify_keys
end
let(:extra_config) { {} }
subject { described_class.new(agent, config: config).execute }
before do
agent.user_access_group_authorizations.create!(group: group_1, config: {})
agent.user_access_project_authorizations.create!(project: project_1, config: {})
end
shared_examples 'removing authorization' do
context 'when config contains no groups or projects' do
let(:config) { {} }
it 'removes all authorizations' do
expect(subject).to be_truthy
expect(authorizations).to be_empty
end
end
context 'when config contains groups or projects outside of the configuration project hierarchy' do
let_it_be(:agent_management_project) { create(:project, namespace: create(:group)) }
it 'removes all authorizations' do
expect(subject).to be_truthy
expect(authorizations).to be_empty
end
end
context 'when configuration project does not belong to a group' do
let_it_be(:agent_management_project) { create(:project) }
it 'removes all authorizations' do
expect(subject).to be_truthy
expect(authorizations).to be_empty
end
end
end
describe 'group authorization' do
it 'refreshes authorizations for the agent' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_groups).to contain_exactly(group_2)
added_authorization = agent.user_access_group_authorizations.find_by(group: group_2)
expect(added_authorization.config).to eq({})
end
context 'when config contains "access_as" keyword' do
let(:extra_config) do
{
user_access: {
access_as: {
agent: {}
}
}
}
end
it 'refreshes authorizations for the agent' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_groups).to contain_exactly(group_2)
added_authorization = agent.user_access_group_authorizations.find_by(group: group_2)
expect(added_authorization.config).to eq({ 'access_as' => { 'agent' => {} } })
end
end
context 'when config contains too many groups' do
before do
stub_const("#{described_class}::AUTHORIZED_ENTITY_LIMIT", 0)
end
it 'authorizes groups up to the limit' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_groups).to be_empty
end
end
include_examples 'removing authorization' do
let(:authorizations) { agent.user_access_authorized_groups }
end
end
describe 'project authorization' do
it 'refreshes authorizations for the agent' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_projects).to contain_exactly(project_2)
added_authorization = agent.user_access_project_authorizations.find_by(project: project_2)
expect(added_authorization.config).to eq({})
end
context 'when config contains "access_as" keyword' do
let(:extra_config) do
{
user_access: {
access_as: {
agent: {}
}
}
}
end
it 'refreshes authorizations for the agent' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_projects).to contain_exactly(project_2)
added_authorization = agent.user_access_project_authorizations.find_by(project: project_2)
expect(added_authorization.config).to eq({ 'access_as' => { 'agent' => {} } })
end
end
context 'when project belongs to a user namespace, and is in the same namespace as the agent' do
let_it_be(:root_ancestor) { create(:namespace) }
let_it_be(:agent_management_project) { create(:project, namespace: root_ancestor) }
let_it_be(:project_1) { create(:project, path: 'project-path-with-UPPERCASE', namespace: root_ancestor) }
let_it_be(:project_2) { create(:project, namespace: root_ancestor) }
it 'creates an authorization record for the project' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_projects).to contain_exactly(project_2)
end
end
context 'when project belongs to a user namespace, and is authorizing itself' do
let_it_be(:root_ancestor) { create(:namespace) }
let_it_be(:agent_management_project) { create(:project, namespace: root_ancestor) }
let_it_be(:project_1) { create(:project, path: 'project-path-with-UPPERCASE', namespace: root_ancestor) }
let_it_be(:project_2) { agent_management_project }
it 'creates an authorization record for the project' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_projects).to contain_exactly(project_2)
end
end
context 'when config contains too many projects' do
before do
stub_const("#{described_class}::AUTHORIZED_ENTITY_LIMIT", 0)
end
it 'authorizes projects up to the limit' do
expect(subject).to be_truthy
expect(agent.user_access_authorized_projects).to be_empty
end
end
include_examples 'removing authorization' do
let(:authorizations) { agent.user_access_authorized_projects }
end
end
end
end
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