WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Conversation

@adamruzicka
Copy link
Contributor

@adamruzicka adamruzicka commented Sep 26, 2025

TL;DR

Additionally perform resource organization and location checks inside Authorizer, to prevent the RBAC system from allowing users to see resources outside of their organizations and locations.

Background

Foreman currently has two mechanisms, which can be used to control what users can do - the permission system and user's orgnization/location membership. These two mechanisms were originally expected to work together. In Foreman, models come with a set of modules facilitating the plumbing between the model, user's taxonomies (Taxonomix) and the RBAC system (Authorizable) and a default scope. The default scope does two things - it sets up the scope in a way that honors current organization/location selection (coming from Taxonomix) as well as limiting the view to only organizations and locations of the current user. This is the usually followed by a call to .authorized(permission) (relying on Authorizable being included in the model), which loops in the RBAC system and further tightens what the user is allowed to do. While this approach nicely separates concerns, it can give misleading results if the two parts are used separately.

This changes the meaning of what user's membership in an organization or location means. Previously, this relationship only marked the user as a resource to be managed within that organization/location, just as subnets or domains are. With this change we would shift it from "a resource to me managed within the scope" to "an actor which manages other things within the scope".

Changes done here

Resource organization and location checks were added to the RBAC system. This makes the RBAC system (Authorizable, Authorizer and #authorized(permission)) one-stop solution for user permission checking.

From the user's point of view, this change should make dealing with permissions in Foreman more consistent as well as making it more intuitive by making organization and location membership set up firmer boundaries than it did previously.

This could also simplify certain workflows. Currently delegating control over a specific organization to a user involves cloning the default "Organization admin" role, setting organization on it, assigning the role to a user and assigning that user to that organization. After this change, this could be reduced to giving the user the default "Organization admin" role without cloning it and just assigning the user to the organization.

This should also benefit Katello, which in general omits the default scope and which scopes resources to organizations differently than Foreman.

"Steps to reproduce"-styled example

Vanilla Foreman

  1. Have orgnizations org1 and org2
  2. Create a user u
  3. Assign the user to org1
  4. Create domain d1 in org1
  5. Create domain d2 in org2
  6. Create a custom role, do not set any taxonomies on the role, add view_domains permission to it, assign it to the user
  7. In rails console:
User.current = User.find_by(login: 'u')
::Domain.unscoped.authorized(:view_domains).map(&:name)

Actual results:
Both d1 and d2 domains are found.

Expected results:
Only d1 domain is found.

Note: On Foreman resources, there is a default scope that performs the organization and location scoping, hence the unscoped, but it is not part of authorization checks.

Katello-flavor

  1. Have orgnizations org1 and org2
  2. Create a user u
  3. Assign the user to org1
  4. Create product p1 in org1
  5. Create product p2 in org2
  6. Create a custom role, do not set any taxonomies on the role, add view_products permission to it, assign it to the user
  7. In rails console:
User.current = User.find_by(login: 'u')
::Katello::Product.authorized(:view_products).map(&:name)

Actual results:
Both p1 and p2 products are found.

Expected results:
Only p1 product is found.

@ofedoren
Copy link
Member

Is this a replacement for #10652?

@adamruzicka adamruzicka changed the title User tax Fixes #TBFL - User taxonomy checks during permission checks Sep 26, 2025
@adamruzicka
Copy link
Contributor Author

Is this a replacement for #10652?

No. 10652 outlines "when is page access allowed" vs "what is shown inside the page". 10652 focuses on the first problem, this PR tightens the second one.

@adamruzicka
Copy link
Contributor Author

Looking at the katello test failures, it seems katello never really relied on the user being in the same org as the resources they manage. Now the question is whether that was the intent or if it is that way just because matching user's orgs to resource's orgs wasn't necessary.

@pondrejk
Copy link
Contributor

Checked on stream + packit using the described reproducers, also with hammer:

~]# hammer domain list
---|---------------------------------
ID | NAME                            
---|---------------------------------
3  | o1 domain                       
2  | o2 domain                       
...
---|---------------------------------
[root@ip-10-0-168-113 ~]# hammer -u domain-viewer -p .. domain list
---|----------
ID | NAME     
---|----------
2  | o2 domain
---|----------

Copy link
Contributor

@pondrejk pondrejk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack from user pov

Copy link
Contributor

@adamlazik1 adamlazik1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Does the pipeline here automatically run with the test versions from the PR?

Comment on lines 47 to 55
test 'joins multiple parts with AND conjunction' do
result = QueryBuilder.join('AND', ['name = value', 'status = active'])
assert_equal '((name = value) AND (status = active))', result
end

test 'joins multiple parts with OR conjunction' do
result = QueryBuilder.join('OR', ['name ~ test*', 'name ~ prod*'])
assert_equal '((name ~ test*) OR (name ~ prod*))', result
end
Copy link
Contributor

@adamlazik1 adamlazik1 Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: one of these tests is probably enough because the form of the conjunction does not impact anything. Or alternatively both asserts can go into single test case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, merged a couple and dropped some others.

Comment on lines 67 to 70
test 'joins three or more parts' do
result = QueryBuilder.join('OR', ['a = 1', 'b = 2', 'c = 3'])
assert_equal '((a = 1) OR (b = 2) OR (c = 3))', result
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick no. 2: This assert could also go to the 'joins multiple parts' testcase

Comment on lines 94 to 97
test 'returns nil for empty array when on_empty is explicitly :nil' do
result = QueryBuilder.key_value_in('organization_id', [], :nil)
assert_nil result
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick no. 3: probably redundant test.

when :nil
nil
when :block
parenthesize("#{key} = 1 AND #{key} != 1")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably doesn't matter, but couldn't we just return '(FALSE)'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly not, scoped search would treat that just as a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah, this should be changed somehow. This might break in some cases

> ForemanTasks::Task.search_for("id = 1 and id != 1").count
ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR:  invalid input syntax for type uuid: "1"
LINE 1: ...asks_tasks" WHERE ((("foreman_tasks_tasks"."id" = '1') AND (...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

Comment on lines 127 to 134
test 'builds query with OR conditions' do
condition1 = 'name ~ prod*'
condition2 = 'name ~ test*'

result = QueryBuilder.join('OR', [condition1, condition2])
expected = '((name ~ prod*) OR (name ~ test*))'
assert_equal expected, result
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test appears to be a duplicate of test 'joins multiple parts with OR conjunction'

end

test 'also covers nested locations' do
sub_loc = FactoryBot.create(:organization, parent_id: @loc2.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sub_loc = FactoryBot.create(:organization, parent_id: @loc2.id)
sub_loc = FactoryBot.create(:location, parent_id: @loc2.id)

Comment on lines 231 to 249
context '#build_search_query_from_parts' do
test 'joins single part with parentheses' do
FactoryBot.build_stubbed(:filter)
result = QueryBuilder.join('AND', ['name ~ test*'])
assert_equal '(name ~ test*)', result
end

test 'joins multiple parts with AND and parentheses' do
FactoryBot.build_stubbed(:filter)
parts = ['name ~ test*', 'organization_id = 1', 'location_id = 2']
result = QueryBuilder.join('AND', parts)
assert_equal '((name ~ test*) AND (organization_id = 1) AND (location_id = 2))', result
end

test 'returns empty string for empty array' do
FactoryBot.build_stubbed(:filter)
result = QueryBuilder.join('AND', [])
assert_nil result
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These appear to be just QueryBuilder tests, unless I am mistaken.


switch_form_tab('Organizations')
ensure_selected_option_of_multiselect(@org1.name, select_id: 'domain_organization_ids')
# ensure_selected_option_of_multiselect(@org1.name, select_id: 'domain_organization_ids')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# ensure_selected_option_of_multiselect(@org1.name, select_id: 'domain_organization_ids')

Can this be deleted or is it kept here as a hint of sorts?

@adamruzicka
Copy link
Contributor Author

Does the pipeline here automatically run with the test versions from the PR?

It does

Copy link
Contributor

@adamlazik1 adamlazik1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The katello counterpart should be merged first so that the tests are green here.

@adamruzicka
Copy link
Contributor Author

Oh come on katello tests, you were green just a moment ago on the other end

@adamlazik1
Copy link
Contributor

I would like to rerun the tests but it appears I don't have permissions to do that.

@adamruzicka adamruzicka changed the title Fixes #TBFL - User taxonomy checks during permission checks Fixes #38731 - User taxonomy checks during permission checks Oct 24, 2025
@adamruzicka
Copy link
Contributor Author

Finally green

@adamlazik1
Copy link
Contributor

Thanks @adamruzicka! Feel free to merge as I am unable to do it myself.

@adamruzicka
Copy link
Contributor Author

squashed to make the check happy

@stejskalleos stejskalleos merged commit 3a2b959 into theforeman:develop Oct 30, 2025
58 of 60 checks passed
@adamruzicka adamruzicka deleted the user-tax branch October 30, 2025 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants