-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Optimize preflight checks for SavedObjectsClient wrappers #102957
Comments
Pinging @elastic/kibana-core (Team:Core) |
Pinging @elastic/kibana-security (Team:Security) |
This could be a very long discussion, but: To be honest, in my opinion, the correct solution would probably be to have distinct APIs between the exposed API of the SOR/SOC, and the internal API between the SOR and its wrappers. That would allow to follow what was done for However this is way easier said than done, as the wrappers are optional, so we would need some sort of facade on top of the SOR when accessing it directly without wrappers, which seems basically like a full rewrite of a lot of stuff, which doesn't sound very realistic right now. So, if we exclude this option, Please correct me if anything is wrong hereIf
Not a big fan of that thing (already said so when we did implement the workaround for
However, the main upside I see is that the preflight checks are effectively performed only once. OTOH,
I would be fine exposing (yet another) kinda-internal API such as
Overall, I don't like either option that much, but I don't see anything else that wouldn't be introducing major changes. As already said, Imho the correct design would be to change to wrapper system to have distinct exposed APIs. The base idea is something like type PublicGetOptions {
// ...
}
type InternalGetOptions = PublicGetOptions & {
// ...
}
interface PublicRepository {
get(options: PublicGetOptions): GetResponse;
}
interface InternalRepository {
get(options: InternalGetOptions): GetResponse;
} However these are massive changes in the So, as much as I hate it, I would go with a mix of the two suggested options:
WDYT? |
Agreed
Sorry, I worded that poorly, what I should have said was: Change the SOR to allow the SOC wrappers to pass down a callback to handle the pre-flight check results. Now I've edited the issue description to reflect that.
Agreed
The implementation that I'm envisioning would solve the duplication and would ensure that only a single preflight check is performed for each SOC method call. Click to expand and see exampleFor example, add the following option to export interface SavedObjectsBulkCreateOptions extends SavedObjectsBaseOptions {
...
preflightCallbacks?: Array<(preflightResults: Array<SavedObject<T>>) => Array<SavedObject<T>>>
} SavedObjectsRepository async bulkCreate<T = unknown>(...) => {
// do validation
let preflightResult = await this.client.mget(...);
if (options.preflightCallbacks) {
for (callback of preflightCallbacks) {
preflightResult = callback([...preflightResult]);
}
}
// do create
} SavedObjectsClient async bulkCreate<T = unknown>(...) => {
const preflightCallback = (preflightResult: Array<SavedObject<T>>) => {
// do authZ
if (authorized) {
return preflightResult;
}
throw new Error("Not authorized");
}
const preflightCallbacks = [...(options.preflightCallbacks ?? []), preflightCallback];
return this.baseClient.bulkCreate(objects, { ...options, preflightCallbacks }));
} I know that adding callbacks like this isn't the most desirable pattern to use, but it might be appropriate here for the following reasons:
That implementation is for non-bulk operations. But yes, as long as we have all of the root fields for the saved object, that is enough for us to do all additional checks we will need for
Yeah, we could expose the preflight APIs to the wrapper and allow each wrapper to pass down an optional
Responding to this out of order, but: I think we should consider biting the bullet and doing this now. It appears it would only affect Core code and the three wrappers, so it wouldn't be that bad. |
Ok, it's a bit clearer now. Still a few questions though async bulkCreate<T = unknown>(...) => {
const preflightCallback = (preflightResult: Array<SavedObject<T>>) => {
// do authZ
if (authorized) {
return preflightResult;
}
throw new Error("Not authorized");
}
const preflightCallbacks = [...(options.preflightCallbacks ?? []), preflightCallback];
return this.baseClient.bulkCreate(objects, { ...options, preflightCallbacks }));
} Not sure to get how the
I agree with the 3 reasons you enumerated here. However I feel like adding internal callbacks parameters to the public API is even worse than adding internal 'plain' parameters to it, so you may be right with
I would love to do that. It would also allow to solve some quite old issue (e.g the I'm still wondering if, with the license change and all, if this wrapper pattern that was introduced only for isolation of concerns between xpack and oss, still is the best approach, and if some hook-based enhancement wouldn't be more powerful and appropriate. However totally changing the enhancement API of the SOR/SOC seems like even more work, and may even be plainly unrealistic. So, if we try to look at the design changes that this public/private distinction would cause. My main concern is that the wrapper currently shared the exact same API as the client and the client IS the public API. As the wrapper currently act as 'delegates', we can't easily change that. ATM, the SOW and SOC shares the same interface. However, we can, or not, have one, or more, wrappers on top of the client, which makes the initial public-to-internal parameters conversion hard to do: Which is why I think we'll need some kind of facade / additional front layer in top of the SOC/SOW/SOR chain to handle this conversion in a single place. This new facade will also be the public API, allowing us to have a distinct internal API for the actual SOC/SOW/SOR communication and chaining. That way, we could have: interface SavedObjectClientFacade {
get(opts: PublicGetOptions)
}
interface InternalSavedObjectClient {
get(opts: InternalGetOptions)
} Note that we would need to have a way to convert these Note that this facade pattern could also allow us to have public and internal return types, if we add a similar internal->public return conversion similar to what we'll need to do with the input. interface SavedObjectClientFacade {
get(opts: PublicGetOptions): PublicGetResponse
}
interface InternalSavedObjectClientContract {
get(opts: InternalGetOptions): InternalGetResponse
} with the wrapper factory being export type SavedObjectsClientWrapperFactory = (
options: SavedObjectsClientWrapperOptions
) => InternalSavedObjectClientContract; this could potentially ease the communication between wrappers and the client if we need some kind of This is a very naive draft, but do you think the approach looks correct? |
🤦 Sorry about that, when I wrote the example for "SavedObjectsClient", I meant it was an example for "SecureSavedObjectsClientWrapper".
Agreed, and we'll likely need a totally different approach anyway when implementing #102177 anyway
Agreed
Perhaps this feeling is a bit of Saved Objects PTSD 😅 Honestly, for the things we want to enable now and in the future (deduplicate preflight checks with SOC wrappers, improve authorization checks when creating objects using
I don't think we have a need for this, but I'm interested to hear what @legrego and @azasypkin think as well.
Yes I think it looks correct and it makes sense to me. Shouldn't be too painful at all! |
Great! then it's almost done, just have to implement the whole thing now.
Oh, definitely. I mean, what could possibly go wrong here 😅 Seriously though, this doesn't sounds like that much changes (for a SOR features impacting all its APIs, that is) Do you know the current parameters that should be converted to |
In addition to the
This kibana/src/core/server/saved_objects/types.ts Lines 142 to 149 in 59d7f33
This We could also optionally get rid of the distinction between SOR's |
Superseded by #133835 |
Currently, the SavedObjectsRepository (SOR) conducts "pre-flight" checks on saved objects for write operations:
create
bulkCreate
update
bulkUpdate
delete
incrementCounter
updateObjectsSpaces
Only multi-namespace saved object types will trigger a pre-flight check, because we need to ensure that the target object exists in the current space before allowing the operation to proceed. Single-namespace types do not require such a check because ID serialization ensures that changes are isolated to the current space.
In #102950, we have identified a need to add pre-flight checks to the Secure SavedObjectsClient (SOC) wrapper too. We should consider optimizing pre-flight checks so that a single check can be used for the SOR and its SOC wrappers. We have already implemented a workaround for this problem in the
updateObjectsSpaces
method, but there may be a better way to handle this across the board.Benefits from such optimization will also be realized when implementing #82725.
Some ideas:
updateObjectsSpaces
.Change the SOR to expose a callback with the pre-flight check results for the SOC wrappers to consumeEdit: this was worded poorly, what I should have said was: Change the SOR to allow the SOC wrappers to pass down a callback to handle the pre-flight check results.The text was updated successfully, but these errors were encountered: