-
Notifications
You must be signed in to change notification settings - Fork 1k
Fixes #38731 - User taxonomy checks during permission checks #10701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
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. |
7c5e43b to
5918d48
Compare
|
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. |
7803a87 to
93fad2c
Compare
|
Checked on stream + packit using the described reproducers, also with hammer: |
pondrejk
left a comment
There was a problem hiding this 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
8c0e2cc to
bd0cf98
Compare
adamlazik1
left a comment
There was a problem hiding this 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?
test/unit/query_builder_test.rb
Outdated
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
test/unit/query_builder_test.rb
Outdated
| 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 |
There was a problem hiding this comment.
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
test/unit/query_builder_test.rb
Outdated
| 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 |
There was a problem hiding this comment.
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.
app/services/query_builder.rb
Outdated
| when :nil | ||
| nil | ||
| when :block | ||
| parenthesize("#{key} = 1 AND #{key} != 1") |
There was a problem hiding this comment.
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)'?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 (...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
test/unit/query_builder_test.rb
Outdated
| 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 |
There was a problem hiding this comment.
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'
test/models/filter_test.rb
Outdated
| end | ||
|
|
||
| test 'also covers nested locations' do | ||
| sub_loc = FactoryBot.create(:organization, parent_id: @loc2.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| sub_loc = FactoryBot.create(:organization, parent_id: @loc2.id) | |
| sub_loc = FactoryBot.create(:location, parent_id: @loc2.id) |
test/models/filter_test.rb
Outdated
| 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 |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| # 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?
It does |
adamlazik1
left a comment
There was a problem hiding this 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.
|
Oh come on katello tests, you were green just a moment ago on the other end |
|
I would like to rerun the tests but it appears I don't have permissions to do that. |
|
Finally green |
|
Thanks @adamruzicka! Feel free to merge as I am unable to do it myself. |
|
squashed to make the check happy |
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 fromTaxonomix) 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 onAuthorizablebeing 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,Authorizerand#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
org1andorg2uorg1d1inorg1d2inorg2view_domainspermission to it, assign it to the userActual results:
Both
d1andd2domains are found.Expected results:
Only
d1domain 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
org1andorg2uorg1p1inorg1p2inorg2view_productspermission to it, assign it to the userActual results:
Both
p1andp2products are found.Expected results:
Only
p1product is found.