-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[ES-UI] Authorization lib to control access to feature & app sections #40150
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
[ES-UI] Authorization lib to control access to feature & app sections #40150
Conversation
|
Pinging @elastic/es-ui |
| </HashRouter> | ||
| </I18nContext> | ||
| <AuthorizationProvider | ||
| permissionEndpoint={httpService.addBasePath(`${API_BASE_PATH}permissions`)} |
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.
Maybe, instead of having each app creating their handler for the permissions, we could have a single handler in our shared plugin, that would expose something like:
GET /<our-shared-plugin_name>/authorization/permissions?app=snapshot_restore
We could then remove the need of this permissionEndpoint prop here.
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.
a single handler may get complex since I think the needs of each app will differ from each other. I like this approach for now
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.
I don't think it would be complex. 75% of the handler code is generic and very little specific to the app. We would simply need to create 1 extension file / app for what is specific.
The benefit would be that we could then simply inject the AuthroizatioProvider anywhere, add an extension to the shared plugin "/privileges" endpoint handler and be done.
We can do that as a second pass 😊
💔 Build Failed |
|
retest |
💔 Build Failed |
|
retest |
💔 Build Failed |
|
retest |
💚 Build Succeeded |
jen-huang
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.
Overall I like this proposal, and I think it makes a lot of sense to build something re-usable. I have left some thoughts and comments that I think should be looked at before giving my final stamp 🙂 Thanks for doing this!
| <section data-test-subj="restoreList">{content}</section> | ||
| ) : ( | ||
| <NotAuthorizedSection | ||
| sectionName="snapshot restore status" |
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 needs to be localized
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.
my comments in NotAuthorizedSection invalidates the above
| ) : ( | ||
| <EuiPageContent> | ||
| <NotAuthorizedSection | ||
| sectionName="Snapshot and Restore" |
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 needs to be localized
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.
my comments in NotAuthorizedSection invalidates the above
| return false; | ||
| }; | ||
|
|
||
| export const WithPrivileges = ({ privileges, children }: Props) => { |
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.
could we consider renaming this to something like PrivilegesProvider or something else that makes it clear that the usage is a HOC?
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.
I prefer to keep the "Provider" term for our context Provider and not mix both. As, indeed, it is not an HOC, I renamed it <AuthPrivileges></AuthPrivileges>. Let me know what you think
| id="xpack.snapshotRestore.restoreList.deniedPermissionTitle" | ||
| defaultMessage="You're missing {privilegeType} privileges" | ||
| values={{ | ||
| privilegeType, |
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 will run into localization problems since cluster/index won't be part of the translation. see my next comment
| <p> | ||
| <FormattedMessage | ||
| id="xpack.snapshotRestore.restoreList.deniedPermissionDescription" | ||
| defaultMessage="To view {sectionName}, you must have {privilegesCount, |
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.
the messaging for missing index-level privileges is slightly different: relevant LOC
how about changing this component so that the title and body are part of Props? this would make it similar to our SectionError and SectionLoading components
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.
Good point, I will update the component
| </div> | ||
| <WithPrivileges privileges="cluster"> | ||
| {({ isLoading, hasPrivileges, missingPrivileges }) => ( | ||
| <Fragment> |
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.
I think this Fragment isn't needed
| </HashRouter> | ||
| </I18nContext> | ||
| <AuthorizationProvider | ||
| permissionEndpoint={httpService.addBasePath(`${API_BASE_PATH}permissions`)} |
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.
a single handler may get complex since I think the needs of each app will differ from each other. I like this approach for now
| return error ? ( | ||
| <SectionError | ||
| title={ | ||
| <FormattedMessage |
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.
not loving having to pass FormattedMessage just for this error message. what do you think about returning error as part of value so that the consumer can handle the error UI instead? this will also let us be more flexible with the error message, for example displaying Error checking Snapshot and Restore permissions
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.
Yeah I was not very happy with it either. I updated the code to let the consumer deal with the error.
Note: I think we need, in a separate PR, to create our own <CoreProvider> that would just provide the core object, so we can use translation in our reusable components. I already needed it in a few places. So we would have a CoreProvider + AppDependencies (other plugins)
| }: Props) => { | ||
| const { error, data: permissionsData } = useLoadPermissions(permissionEndpoint); | ||
|
|
||
| const isLoaded = typeof permissionsData !== 'undefined'; |
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.
why not use loading that is returned from useLoadPermissions?
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.
Not sure why, I will use it 😊
|
|
||
| // Note: here we have to serialize the HTTP response | ||
| // If we move forward with proposal, we would need to update the SR server response | ||
| // to implement the interface below so we don't need the serialization. |
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.
the permissions contract needs to be worked through more carefully. for SR, access to the entire app requires all cluster privileges, and access to Restore status requires all index privileges. the implementation logic in this PR works great for that use case, but what about for more granular UI details?
for example, maybe we want to check if user has access to just read snapshots/repositories and allow them overall app access, but disallow delete/edit/restore through UI if they don't have more admin privileges. read and admin privileges are both cluster type privileges in this case, so the current implementation wouldn't allow for this type of granular checking.
btw this comment does not block this PR! it is just some thoughts, after all I was the one who structured this app's permission response this way 😄 I am working on SLM UI for 7.4, which will also live inside Snapshot and Restore. I'm sure part of that work will be adjusting the app permissions to account for this new section of the app, so I am happy to iterate and improve the current implementation when I get there
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.
I updated the logic to support this use case and be able to be granular.
Now, if we require all the privileges we need to provide cluster.*
If we need only some privileges we can provide cluster.someSpecifiPrivilege
cjcenizal
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.
Great work @sebelga! I agree with Jen's suggestions about changing NotAuthorizedSection to accept title and body props, and to change the AuthorizationProvider to delegate handling of the error state to the consumer.
In addition to this, can we change "authorization" and "privileges" to "permissions" in the code and file names so that this concern is identified by one term instead of three?
| title={ | ||
| <FormattedMessage | ||
| id="xpack.snapshotRestore.app.checkingPermissionsErrorMessage" | ||
| defaultMessage="Error checking permissions" |
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 standardize on language, e.g. use "permissions" or "authorization" everywhere? For example, we could rename this file permissions_provider or change this message to "Error checking authorization."
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.
I agree that 3 terms are too many 😊 But one thing is the Authorization service (which is the feature we are bringing here) and then what this feature provides. The Authorization provider can return different types of data: user roles, user privileges, user access, user permissions, ...
Jen seems to have used "privilege" and "permission" interchangeably. The API path is "/permissions" but privileges are returned 😊 I will stick with "permissions" and use that word everywhere.
[EDIT]: But then... Elasticsearch uses the word "privilege". :/ I'll use "privileges" to simplify things.
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.
| return false; | ||
| }; | ||
|
|
||
| export const WithPrivileges = ({ privileges, children }: Props) => { |
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.
Same with this file. Instead of "privileges" can we use "permissions" or "authorization"? By keeping the terms consistent we can reduce mental overhead for people figuring out what the code is doing.
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.
I decided to align all the term to Elasticsearch API "privilege" naming. The provider is still the Authorization as explained above.
|
Thanks for the review @jen-huang and @cjcenizal I addressed all the feedback and made some comments. Could you have another look? Cheers! @jen-huang I also updated the Server handler response to match the contract of the AuthorizationProvider. |
💔 Build Failed |
💔 Build Failed |
💔 Build Failed |
💚 Build Succeeded |
|
Just for interest, I thought I'd mention what Cloud has done for permissions. Our permissions map 1-to-1 with Swagger operation IDs, so for the UI we generate a TypeScript enum of all the possible permissions. Then, we get type completion when checking the user's permissions, as well as typo prevention. |
|
Thanks for sharing @pugnascotia Although we don't have yet a centralized service for privileges. I'll keep it in mind if we decide to do so. cheers! |
jen-huang
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 for making the changes Seb, LGTM!
I also tested locally to make sure there were no regressions with accessing the app with a fully privileged user and users of varying privileges.
Looking forward to trying this out in my new work for SR!
|
Cool, thanks for the review @jen-huang ! |
💚 Build Succeeded |
|
@cjcenizal Do you still have concerns about naming? cheers |
|
@sebelga I can re-review this today, thanks for the ping! |
cjcenizal
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.
I did a grep for "permission" and found many references still in the code. Did you want to replace these with "privilege"? In a few places both terms refer to the same thing, which is a little confusing. Since this doesn't affect functionality I'm approving the PR to unblock it but I think it would be great if we could align on a single term.
| const toArray = (value: string | string[]): string[] => | ||
| Array.isArray(value) ? (value as string[]) : ([value] as string[]); | ||
|
|
||
| export const AuthPrivileges = ({ requiredPrivileges, children }: Props) => { |
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.
Nit: AuthorizationPrivileges or simply Authorization would be more consistent with our other names, and removes any ambiguity around what auth stands for (authorization or authentication).
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.
Ok, I will use AuthorizationPrivileges.
"Authorization" and "Privileges" are 2 different things 😊 The "Authorization" is the mechanism in place that will handle a user "privileges". It could, for example, also manage "roles" and would still be called "Authorization".
Here, the component is handling "privileges" so I'll change it to AuthorizationPrivileges.
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.
Edit: I went back to my original <WithPrivileges> as it is what best describes this component. I just saw this convention used by the app search team.
<WithPrivileges privileges="index.*">
{/* anything here requires the above privileges */}
</WithPrivileges>cc @jen-huang
| */ | ||
|
|
||
| import React, { createContext } from 'react'; | ||
| import { useLoadPermissions } from '../services'; |
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.
Nit: Would useLoadPrivileges be more consistent?
| export const AuthorizationContext = createContext<Authorization>(initialValue); | ||
|
|
||
| interface Props { | ||
| permissionEndpoint: 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.
Nit: privilegesEndpoint?
| } | ||
|
|
||
| export const AuthorizationProvider = ({ permissionEndpoint, children }: Props) => { | ||
| const { loading, error, data: permissionsData } = useLoadPermissions(permissionEndpoint); |
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.
Nit: privilegesData?
| * you may not use this file except in compliance with the Elastic License. | ||
| */ | ||
|
|
||
| export interface AppPermissions { |
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.
Nit: should this be AppPrivileges or perhaps AppAuthorization? I learn towards the latter if that's overarching concept we're dealing with, of which "privileges" are a part.
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.
Good point, I'll change it to AppAuthorization.
|
Thanks for the review @cjcenizal I updated all references to use the word "privilege" instead of "permission". |
…ssion-state # Conflicts: # x-pack/legacy/plugins/snapshot_restore/public/app/app.tsx
| permissionsResult.missingClusterPrivileges = extractMissingPrivileges(cluster || {}); | ||
| permissionsResult.hasPermission = hasPermission; | ||
| privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); | ||
| privilegesResult.hasAllPrivileges = hasPermission; |
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.
Did you want to change this to privileges too? I did a ctrl-f for "permission" on the diff of this PR and there are also quite a few references to privileges in the localization keys.
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.
Not sure I follow here. Yes I did want to change "permission" to "privileges" here. Isn't that what you also wanted?
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.
Ah you mean the "hasPermission" name? 😊
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.
Ah you mean the "hasPermission" name
Yes, you can do a ctrl-f and you'll see all of the instances I have in mind as candidates for renaming.
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.
++ I just updated everything that had the word "permission".
💔 Build Failed |
💚 Build Succeeded |

I had a few comments on Jen's PR https://github.com/elastic/kibana/pull/39966/files/e20aefbac90dbc4048630f52694d73dc6108e734
So as it is merged I decided to create this proposal. 😊
My main point was that I thought it overkills to add an app state with a reducer just to make the permission available through context. Aside, a state implies IMO that it will change, and here we load the permissions when the app loads and never change them afterward.
This PR bring a proposal of a small (reusable) "lib" (never know how to call those) that, if we agree on the architecture, will have to find its place in our future "x-pack/plugins/elasticsearch-ui-shared" plugin (in that plugin we will also put my current work for the forms, our
useRequest()hook, and all our reusable static code.@cjcenizal @jen-huang @alisonelizabeth looking forward to hearing your thoughts on this, cheers!