-
Notifications
You must be signed in to change notification settings - Fork 1k
Fixes #37665 - Context-based frontend permission management #10338
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
base: develop
Are you sure you want to change the base?
Fixes #37665 - Context-based frontend permission management #10338
Conversation
webpack/assets/javascripts/react_app/Root/Context/Hooks/useRefreshedContext.js
Outdated
Show resolved
Hide resolved
Does this mean that |
My programming professor from university approves the use of constants. I just wonder about the plugin use case. Can we make it easy for plugins to do the same? |
Yes, exactly! As I wrote in the docs, this is not really needed (yet) in regards to permissions, but it may be of use for |
|
Tests (at least some) are failing since we mock the context in webpack/test_setup.js, so you can move the mock from import { useForemanPermissions } from './assets/javascripts/react_app/Root/Context/ForemanContext';
import { allPermissions } from './assets/javascripts/react_app/common/hooks/Permissions/permissionHooks.fixtures';
...
getHostsPageUrl: displayNewHostsPage =>
displayNewHostsPage ? '/new/hosts' : '/hosts',
useForemanPermissions: jest.fn(),
...
useForemanPermissions.mockReturnValue(allPermissions); |
webpack/assets/javascripts/react_app/components/Permitted/Permitted.js
Outdated
Show resolved
Hide resolved
|
Also, I tried the wrapper and it works nicely! const ReportsTabWrapper = props => (
<Permitted
requiredPermissions={['view_config_reports']}
unpermittedComponent={
<PermissionDenied missingPermissions={['TESTview_config_reports']} />
}
>
<ReportsTab {...props} />
</Permitted>
);
export default ReportsTabWrapper; |
f3f38d0 to
cfe151a
Compare
|
Wow, talk about response time... I made the following changes:
|
jeremylenz
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.
Thanks @Thorben-D! This looks like a great addition. My only concern is the API use case - The whole point of ForemanContext (well, maybe not the whole point, but one of the huge advantages) is to avoid the frontend-backend API call. I'm curious your reasoning for it?
|
@jeremylenz To be perfectly honest, I can't really find a use-case for the |
|
How can we proceed here? |
|
I would prefer that Other than that, I am good with moving this forward. I see a CI failure; I didn't check if it's related but perhaps a rebase will help? |
|
@jeremylenz I'll have the PR fixed up by the end of the week. I'm responding now though because you mentioned --- a/app/registries/foreman/plugin/app_metadata_registry.rb
+++ b/app/registries/foreman/plugin/app_metadata_registry.rb
@@ -6,16 +6,35 @@ module Foreman
end
def register(plugin_name, data = {})
- unless data.is_a?(Hash) || data.respond_to?(:to_h)
+ unless data.is_a?(Hash) || data.respond_to?(:to_h) || data.respond_to?(:call)
Rails.logger.warn "AppMetadataRegistry: data for plugin #{plugin_name} is not a hash or compatible type; registration skipped."
return
end
- @plugin_metadata[plugin_name] = data.is_a?(Hash) ? data : data.to_h
+ @plugin_metadata[plugin_name] = data.is_a?(Hash) || data.respond_to?(:call) ? data : data.to_h
Rails.logger.info "Registered app metadata for plugin #{plugin_name}"
end
def all_plugin_metadata
- @plugin_metadata
+ render_hash @plugin_metadata
+ end
+
+ private
+
+ def render_hash(hash)
+ if hash.respond_to?(:call)
+ return render_hash(hash.call) # Recurse in case lambda returns a hash
+ elsif !hash.is_a?(Hash)
+ return hash
+ end
+
+ sub_hash = {}
+
+ hash.each_pair do |key, value|
+ new_key = key.respond_to?(:call) ? render_hash(key.call) : key
+ sub_hash[new_key] = render_hash(value)
+ end
+
+ sub_hash
end
end
endFar from perfect, but maybe it can serve you as a start. |
cfe151a to
f38ae60
Compare
|
I have removed the |
Introduce a faster alternative to API based permission management in the frontend based on ForemanContext - Add Permitted component - Add permission hooks - Add ContextController - Add JS permission constants - Add rake task to export permissions - Add permission management page to developer docs
f38ae60 to
6c5880b
Compare
|
Great... I removed the new compact UI mode. |
Why remove that? don't we need it? |
Definitely! |
|
@Thorben-D Check out #10814, fyi :) |
jeremylenz
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.
Can we include actual use of the Permitted component?
It seems to work well for me if we replace the PermissionDenied pattern in webpack/assets/javascripts/react_app/routes/RegistrationCommands/RegistrationCommandsPage/index.js :
return (
<Permitted
requiredPermissions={['register_hosts']}
unpermittedComponent={
<PermissionDenied missingPermissions={['register_hosts']} />
}
>
<PageLayout
...|
I get where you're coming from and I can do that of course, but that seems a bit out of the scope of this PR to me. |
|
@MariaAga thoughts? (I have tested it locally using my suggestion above, and it works) |
Redmine issue
Community-forum post this implementation is based on
This PR implements a faster way to check user permissions in Foreman core/plugin frontend code. This speed advantage is gained by leveraging the ForemanContext to store the current user's permissions.
Changes made:
ForemanContextAdded permissions field to context. Contains the current user's permissions - Implmenented as a set of permission names.
PermittedA component that abstracts the conditional rendering scheme of "Render if <permission> is granted"
usePermission/sTwo custom hooks that allow checking whether a is granted a single or multiple permissions
useForemanPermissionsContext hook that provides a reference to the actual permissions set
useRefreshedContextA custom hook that refreshes the ForemanContext by requesting the up-to-date application state from the backend
ContextControlleratapi/v2/contextController that interfaces with the application context on the backend
view_contextpermissionPerformance
Five samples taken on a decently fast system using FireFox developer edition with cache disabled and no background tasks running.
Different approaches tested using this component.
API-based approach
PermittedcomponentPermittedcomponent in conjunction withuseRefreshedContext()Discussion
Although it may seem like the difference between
PermittedandPermitted+useRefreshedContextis negligible, this is not the case, as the actual time to mount the component differs by at least the API request duration when usinguseRefreshedContext. Unfortunately, I don't have any values there because the React profiler is ...not great.Even with a full page-reload, a speed difference of ~7% may be observed. This will be amplified on weaker / busier systems than mine. Furthermore, navigation between frontend-rendered pages will benefit a lot more, as comparatively little data needs to be requested from the backend.
Unfortunately, I nuked my dev-environment shortly after collecting the data above, so I will have to amend the results for client-rendered -> client-rendered later :)
The case for permission constants in JS
Although not strictly in the scope of this issue, I believe that this presents a good opportunity to introduce permission constants for JS, similar to API status constants. These constants mainly offer the following benefits:
To aid developers in creating these constants, I created a rake task,
export_permissions, that automatically generates these JS constants from the permission seed file.TODO
I tried a lot of things, but I didn't manage to mock the
app_metadatafunction, so the test is useless at the momentSince this hook interfaces pretty deeply with the context and the API, testing it is rather difficult. I got pretty far with
renderHookand mocks but am running into issues with the setForemanContext function.