-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Refactor SavedObjectsClient #134395
Refactor SavedObjectsClient #134395
Conversation
b91196a
to
04bf69f
Compare
fe1d817
to
2df8716
Compare
4f562f0
to
752f07f
Compare
These are unrelated to the prior commit, but some ripple effect caused these integration tests to start failing. I had to fix them by removing a circular dependency and changing the test mocks. Leaving this in a separate commit because it's unrelated to the encryption extension.
752f07f
to
c67e6ca
Compare
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.
Author's notes for reviewers.
type SavedObjectsFindOptions = Omit< | ||
SavedObjectFindOptionsServer, | ||
'pit' | 'rootSearchFields' | 'searchAfter' | 'sortOrder' | 'typeToNamespacesMap' | ||
'pit' | 'rootSearchFields' | 'searchAfter' | 'sortOrder' |
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 typeToNamespacesMap
option existed for the server-side Security SOC wrapper to pass down authorization information to the SOR to use for building the ES query. We omitted this attribute from the options for the public SOC, as it was never meant for other consumers to use.
Now that we don't have SOC wrappers, we don't need this typeToNamespacesMap
option at all anymore, I've completely removed it.
export const ENCRYPTION_EXTENSION_ID = 'encryptedSavedObjects' as const; | ||
export const SECURITY_EXTENSION_ID = 'security' as const; | ||
export const SPACES_EXTENSION_ID = 'spaces' as const; |
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 SO extension IDs are the same as the plugins that provide these extensions. This is purely for backwards compatibility for consumers that currently create a scoped SOC and disable one of the SOC wrappers.
private getExtensions( | ||
request: KibanaRequest, | ||
excludedWrappers: string[] | ||
): SavedObjectsExtensions { | ||
const isEncryptionExtensionIncluded = | ||
!excludedWrappers.includes(ENCRYPTION_EXTENSION_ID) && !!this.encryptionExtensionFactory; | ||
const encryptionExtension = isEncryptionExtensionIncluded | ||
? this.encryptionExtensionFactory?.({ typeRegistry: this._typeRegistry, request }) | ||
: undefined; | ||
const isSecurityExtensionIncluded = | ||
!excludedWrappers.includes(SECURITY_EXTENSION_ID) && !!this.securityExtensionFactory; | ||
const securityExtension = isSecurityExtensionIncluded | ||
? this.securityExtensionFactory?.({ typeRegistry: this._typeRegistry, request }) | ||
: undefined; | ||
const isSpacesExtensionIncluded = | ||
!excludedWrappers.includes(SPACES_EXTENSION_ID) && !!this.spacesExtensionFactory; | ||
const spacesExtension = isSpacesExtensionIncluded | ||
? this.spacesExtensionFactory?.({ typeRegistry: this._typeRegistry, request }) | ||
: undefined; | ||
|
||
return { | ||
encryptionExtension, | ||
securityExtension, | ||
spacesExtension, | ||
}; | ||
} |
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 mimics the behavior of SOC wrappers in that any extension can be disabled if the extension ID is specified in the excludedWrappers
option when instantiating a scoped SOC. I didn't want to rename that option as it would be a breaking change. This approach is fully backwards compatible with existing plugin behavior.
@@ -445,4 +446,6 @@ describe('collectMultiNamespaceReferences', () => { | |||
); | |||
}); | |||
}); | |||
|
|||
test.todo('with security enabled'); |
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 added TODOs like this where unit tests need to be added to retain the code coverage that we had before this refactor.
export interface CheckAuthorizationParams<A extends string> { | ||
types: Set<string>; | ||
spaces: Set<string>; | ||
actions: A[]; | ||
options?: { | ||
allowGlobalResource?: boolean; | ||
}; | ||
} |
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.
Sorry for the lack of TSdocs here, I was trying to throw this PR together as quickly as possible 😅
Now, the authorization check queries Elasticsearch for all permutations of specified types/spaces/actions.
When Kibana registers application privileges with Elasticsearch, each space ID corresponds to a specific resource. Instead of checking privileges for individual resources, you can choose to check privileges for "*", which is the global resource. When you create a role that grants access to "All spaces", under the hood it's granting the specified feature privileges for the global resource.
It's important to know that you can't check privileges for individual spaces and the global resource at the same time. Checking privileges for the global resource supersedes any individual space.
It's also important to know that if a saved object exists in "*" (all spaces), a user doesn't need to have access to the global resource to be authorized to access that saved object, they only need to be authorized to access one space.
For most SOR functions, we don't want to risk accidentally checking privileges for the global resource. For example, when you get
an object in the Default space and it exists in namespaces: ['*']
, the SOR will check privileges for spaces: ['default', '*']
. By default, the security extension will omit '*' from that privilege check, because we are really only interested in knowing if the user has access to the Default space.
If the allowGlobalResource: true
option is included, the security extension will not omit '*'. This is only used when the user is attempting to (1) share an object to all spaces, or (2) create an object that exists in all spaces -- in both of these cases, we actually want to ensure the user has access to the global resource.
export enum AuditAction { | ||
CREATE = 'saved_object_create', | ||
GET = 'saved_object_get', | ||
RESOLVE = 'saved_object_resolve', | ||
UPDATE = 'saved_object_update', | ||
DELETE = 'saved_object_delete', | ||
FIND = 'saved_object_find', | ||
REMOVE_REFERENCES = 'saved_object_remove_references', | ||
OPEN_POINT_IN_TIME = 'saved_object_open_point_in_time', | ||
CLOSE_POINT_IN_TIME = 'saved_object_close_point_in_time', | ||
COLLECT_MULTINAMESPACE_REFERENCES = 'saved_object_collect_multinamespace_references', // this is separate from 'saved_object_get' because the user is only accessing an object's metadata | ||
UPDATE_OBJECTS_SPACES = 'saved_object_update_objects_spaces', // this is separate from 'saved_object_update' because the user is only updating an object's metadata | ||
} | ||
|
||
export interface AddAuditEventParams { | ||
action: AuditAction; | ||
outcome?: EcsEventOutcome; | ||
savedObject?: { type: string; id: string }; | ||
addToSpaces?: readonly string[]; | ||
deleteFromSpaces?: readonly string[]; | ||
error?: Error; | ||
} |
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.
SO audit event parameters and audit actions need to be defined in Core now, since the SOR is responsible for calling the security extension to add audit events.
/** | ||
* @internal | ||
*/ | ||
export interface SavedObjectsFindInternalOptions { | ||
/** This is used for internal consumers that need to use a PIT finder but want to prevent extensions from functioning. */ | ||
disableExtensions?: boolean; | ||
} |
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.
We use the SOR's PointInTimeFinder internally when searching for aliases and shared origins for saved objects, but we need to disable the extensions for that to function correctly.
Before, when we had SOC wrappers, the SOR's PointInTimeFinder did not have any of the wrapper functionality applied. This disableExtensions
internal option preserves that behavior.
]); | ||
}); | ||
}); | ||
|
||
test.todo('with security extension'); |
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 security extension actually filters the object graph in addition to redacting space information. See the old Secure SOC wrapper unit tests for details. You should be able to largely copy/paste those tests here.
this.auditLogger.log( | ||
savedObjectEvent({ | ||
action: isOnlySpace | ||
? SavedObjectAction.DELETE | ||
: SavedObjectAction.UPDATE_OBJECTS_SPACES, | ||
outcome: 'unknown', | ||
savedObject: { type: savedObject.type, id: savedObject.id }, | ||
deleteFromSpaces: [id], | ||
}) | ||
); | ||
this.securityExtension!.addAuditEvent({ | ||
action: isOnlySpace | ||
? SavedObjectAction.DELETE | ||
: SavedObjectAction.UPDATE_OBJECTS_SPACES, | ||
outcome: 'unknown', | ||
savedObject: { type: savedObject.type, id: savedObject.id }, | ||
...(!isOnlySpace && { deleteFromSpaces: [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.
The old audit event included the deleteFromSpaces
field whether the object was being updated or deleted.
Now, if the object is being deleted, we omit the deleteFromSpaces
field.
const expectSavedObjectForbidden = expectResponses.forbiddenTypes('get'); | ||
const expectSavedObjectForbidden = expectResponses.forbiddenTypes('bulk_get'); |
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 Secure SOC wrapper used to check privileges differently for SOC.resolve
and SOC.bulkResolve
(it checked 'get' and 'bulk_get' privilege actions, respectively).
Now, the internalBulkResolve
module is initiating authorization checks directly using the security extension, but it is used for the implementation of both the SOC.resolve
and SOC.bulkResolve
functions.
The cleanest change was to make internalBulkResolve
always check for the 'bulk_get' privilege action, and change the resolve
integration test to reflect that.
Forgot to do this in the spaces extension commit
Pinging @elastic/kibana-security (Team:Security) |
Pinging @elastic/kibana-core (Team:Core) |
Buildkite, test this |
This comment was marked as outdated.
This comment was marked as outdated.
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.
ResponseOps changes LGTM
Pinging @elastic/fleet (Team:Fleet) |
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.
First pass through (including some manual testing), the tests look really great overall! Most of my comments are minor so far.
I didn't quite appreciate how much additional logic the repository would need in order to support calling these extensions 🙈
src/core/server/saved_objects/service/lib/extensions/encryption.ts
Outdated
Show resolved
Hide resolved
src/core/server/saved_objects/service/lib/extensions/encryption.ts
Outdated
Show resolved
Hide resolved
src/core/server/saved_objects/service/lib/extensions/encryption.ts
Outdated
Show resolved
Hide resolved
const objectOriginsToSearchFor = objectsWithContext | ||
.filter(({ spaces }) => spaces.length !== 0) | ||
.map(({ type, id, originId }) => ({ type, origin: originId || id })); | ||
const objectOriginsToSearchFor = foundObjects.map(({ type, id, originId }) => ({ |
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.
Nice, thanks for removing the redundant filter
call!
}); | ||
}); | ||
|
||
describe('#bulkCreate', () => { |
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.
Similar question from create
: Do you think it's worthwhile to include tests for multi-namespace types, since they influence what we pass into the security extension?
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.
We do test the initial namespaces path. Do you mean adding a case where we mock an existing multi-space object with a different set of spaces than what is passed in for initial spaces? This would be applicable to create, but I think bulkCreate will just resort to using the namespace option/active space in that case.
Are there more cases outside of initial "namespaces"?
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.
It looks like this code block isn't exercised by the current test suite, which is for existing multi-namespace types, both with and without initialNamespaces
provided:
for (const [objType, entry] of preAuthorizationResult.typeMap) { | ||
if (!entry.find) continue; | ||
// This ensures that the query DSL can filter only for object types that the user is authorized to access for a given space | ||
const { authorizedSpaces, isGloballyAuthorized } = entry.find; |
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 we're missing a test for this case within repository.security_extension.test.ts
. Do you think it's wortwhile to add one?
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 catch. I will add a test that executes find within 2 namespaces, and mock a checkAuthorization return simulating authz in only one of those spaces and validate the ES call is made with only the authorizes space. Is that what you had in mind?
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 think that'll do the trick!
return { message: () => `expected type and id to match without error`, pass: false }; | ||
} | ||
}, | ||
}); | ||
const expectSuccess = ({ type, id }: { type: string; id: string }) => { | ||
// @ts-expect-error TS is not aware of the extension | ||
return expect.toBeDocumentWithoutError(type, 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.
optional: this appears to be the only place where toBeDocumentWithoutError
is called. Do you think it's worth keeping this as a custom expect
function within repository.test.common.ts
, or should we inline the logic here instead?
…e array 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.
Fleet change LGTM
Closing this PR to replace with #142878 which manually merges these changes into the new packages structure. |
Merges the changes of #134395 into the new packages structure. Resolves #133835 ### Description This PR represents a fully manual merge of the saved objects refactor of client wrapper system into repository extensions. These changes are being manually merged due to significant changes of the saved objects implementation in the main branch, specifically the migration to the new packages structure. ### Other changes - Bulk Delete: bulk delete was implemented in parallel to #134395 being completed and this PR will refactor that API to utilize the new extensions Co-authored-by: Jeramy Soucy <jeramy.soucy@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
POC for #133835.
Overview
This PR removes the three Saved Objects Client (SOC) wrappers in favor of bespoke extensions that are consumed directly by the Saved Objects Repository (SOR).
This approach allows us to be more efficient and flexible for authorization in particular.
Notable changes
The three SOC wrappers for encryption, security, and spaces have been removed and replaced with extensions that are called directly from the SOR itself. Most of the behavior is functionally identical.
All the extensions can still be disabled when instantiating a scoped SOC by using the
excludedWrappers
option (see comment below).Security: before, all SOC functions checked authorization before the SOR checked to see if parameters were valid. It wasn’t exactly wrong before, but it did lead to confusing integration tests where a "fully privileged" user would get different results from a "superuser". This would happen because a superuser would be authorized to access resources that aren’t registered with Elasticsearch (such as an invalid object type), passing through authorization in the Security wrapper and then getting a validation error in the Repository. On the other hand, a non-superuser Kibana administrator would get a 403 error instead. Now, each Repository function checks validation first and only checks authorization for object(s)/request(s) that are valid, and the integration tests have been updated to reflect that.
Unit tests for the new extensions -- these reside alongside the extensions themselves
Unit tests for the SOR with extensions enabled -- these reside in independent files alongside the existing repository unit tests. There are additional tests added to update_objects_spaces.test, internal_bulk_resolve.test, and collect_multi_namespace_references.test.