Skip to content
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

Security - Reorder Saved Objects Client wrappers #38444

Closed
wants to merge 50 commits into from

Conversation

legrego
Copy link
Member

@legrego legrego commented Jun 7, 2019

Summary

Previously, the wrapped saved objects client worked in the following manner:

  • Security Plugin's wrapper authorized the saved objects operation, deriving the namespace to authorize against from the spacesService.getSpaceId() function exposed from the Spaces plugin.
  • Spaces Plugin's wrapper populated the namespace attribute which is used by the base Saved Objects Client/Repository for the actual operation.

With this change, the order of the wrappers has been switched, so now the wrapped saved objects client works in the following manner:

  • Spaces Plugin's wrapper populates the namespace attribute for use by downstream wrappers, as well as the base Saved Objects Client/Repository for the actual operation.
  • Security Plugin's wrapper authorizes the saved objects operation, using the namespace provided by the Spaces plugin's wrapper. This has a side benefit of removing the hard dependency between the security plugin's wrapper and the spaces plugin.

This also further abstracts the authorization logic from the client wrapper. Previously, the wrapper used checkPrivilegesDynamicallyWithRequest, which was responsible for using the correct authz method based on whether the Spaces plugin was enabled or not.

We've introduced a new checkSavedObjectsPrivileges function, which behaves in much the same way as checkPrivilegesDynamicallyWithRequest, but it's designed to work specifically with the namespace, converting to a space id when required.

@@ -26,6 +27,10 @@ export function createSpacesService(server: any): SpacesService {
return spaceId;
}

function setSpaceId(request: any, spaceId: string) {
Copy link
Member Author

@legrego legrego Jun 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove this, as it's not used yet, but this is a potential solution for allowing Spaces to dictate which space id should be considered the "current" space id for features such as #37286

If we decide to keep in this PR, then I just need to write a few more tests before merging.

This has the benefit of not modifying the base path for the in-flight request, so request.getBasePath() / http.getBasePathFor(request) will not be impacted by calls to setSpaceId, which reduces the likelihood of side-effects as a result of changing the space id mid-request.

One tradeoff is that this necessitates having a cache here (or elsewhere) to store this information, which we had planned to remove in #35429

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I always figured this would just be done for "copy to space" by allowing "consumers" to specify the namespace in the options. Otherwise, we can't use the SOC instances for different spaces concurrently and we have do so serially.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I always figured this would just be done for "copy to space" by allowing "consumers" to specify the namespace in the options. Otherwise, we can't use the SOC instances for different spaces concurrently and we have do so serially.

Yep, that's the other route we can take! I started down that path in this PR, but ended up reverting the required changes for now.

I'm happy to take either approach, I just wanted to present options. I'll revert this change in this PR, and re-introduce the ability for the spaces wrapper to accept a namespace from consumers.

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

type,
});
expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided (for now) to keep the audit logging assertions as part of this test suite. We're technically testing an implementation detail of a dependency now, but I feel like it's important to verify that it works from the service level, and the Saved Object Client is as close to an API as we can get without mocking out even more services.

We have audit logging tests as part of https://github.com/elastic/kibana/blob/764322cc7f3a6f3b082a0044af151e1b2a1da21f/x-pack/plugins/security/server/lib/authorization/saved_objects/check_saved_objects_privileges.test.ts, so I don't have a strong opposition to removing the checks here if we want to simplify this test suite a bit.


try {
if (deps.spacesEnabled) {
const spaceId = namespaceToSpaceId(namespace);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆕 no longer relies on the spaces service to get the current space id!

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

private async ensureAuthorized(
typeOrTypes: string | string[] | undefined,
action: SavedObjectsOperation,
namespace: string | undefined,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff isn't very helpful since this was converted to TypeScript, but the only material difference in this file is the introduction of this namespace parameter, and the swapping of checkPrivilegesDynamicallyWithRequest with checkSavedObjectsPrivileges.

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@legrego legrego added the Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! label Jun 10, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-security

@legrego legrego added the release_note:skip Skip the PR/issue when compiling release notes label Jun 10, 2019
@legrego legrego marked this pull request as ready for review June 10, 2019 18:19
@legrego legrego requested a review from a team as a code owner June 10, 2019 18:19
@legrego legrego added the review label Jun 10, 2019
@legrego legrego requested a review from kobelb June 10, 2019 20:01
@kobelb
Copy link
Contributor

kobelb commented Jun 10, 2019

ACK: will review next

kobelb and others added 3 commits June 12, 2019 06:13
* Using SavedObjectsClientContract['errors'] instead of any for errors

* Moving security's SOC wrapper to authorization/saved_objects

* Renaming 'action' to operation

* Renaming checkSavedObjectsPrivileges to ensureSavedObjectsPrivileges

We use "check" elsewhere when returning a "check privileges result", and
we use "ensure" elsewhere when an Error will be thrown if the user
doesn't have any privileges.

* Adding "general error" tests for ensure saved object privileges

* Removing general error tests from wrapper

* Change the SecureSavedObjectsClientWrapper tests to be strictly white-box
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@rudolf
Copy link
Contributor

rudolf commented Jun 21, 2019

retest

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@kobelb
Copy link
Contributor

kobelb commented Jun 21, 2019

Having to augment the SavedObjectNamespace definition which OSS sees for this solution feels wrong. Also, the types make it appear that you can provide a Symbol as the namespace, and then we're throwing an error if it's provided. At the risk of being pedantic, after seeing the final implementation of this solution, it just doesn't feel right.

@rudolf
Copy link
Contributor

rudolf commented Jun 21, 2019

@legrego the only reason for putting the symbol type in the OSS SavedObjectNamespace was the weirdness of our documentation tool right?

It doesn't affect the resulting documentation so I'd be happy to live with it the way it was. I haven't had a chance to play around with it, but it might also be a bug intstead of just a limitation in api-extractor.

@kobelb
Copy link
Contributor

kobelb commented Jun 21, 2019

If we could get away with not changing the OSS definition of the saved object namespace, that'd make me feel much better about this implementation. That'd allow us to change the "asserts" within the repository itself to ensure the namespace is either undefined or a string, without explicitly ensure that it's "not a symbol". Apologies if I'm being pedantic here.

@legrego
Copy link
Member Author

legrego commented Jun 24, 2019

the only reason for putting the symbol type in the OSS SavedObjectNamespace was the weirdness of our documentation tool right?

Yep, that was the only reason I made this change.

if we could get away with not changing the OSS definition of the saved object namespace, that'd make me feel much better about this implementation. That'd allow us to change the "asserts" within the repository itself to ensure the namespace is either undefined or a string, without explicitly ensure that it's "not a symbol". Apologies if I'm being pedantic here.

sure I'll revert.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

elasticmachine commented Jun 24, 2019

💔 Build Failed

^ unrelated CI failure

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

currentSpaceId: string
): string | undefined {
if (operationOptions.namespace) {
if (operationOptions.namespace === DEFAULT_SPACE_NAMESPACE) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who writes DEFAULT_SPACE_NAMESPACE in namespace field? If it serializable a symbol cannot be used. Can we use a special string instead? What is the benefit of using a separate type?
DEFAULT_SPACE_ID exists here only for backward compatibility?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who writes DEFAULT_SPACE_NAMESPACE in namespace field? If it serializable a symbol cannot be used. Can we use a special string instead? What is the benefit of using a separate type?

Consumers of the Saved Objects Client can specify this for any operation they wish to perform explicitly against the default space. You bring up a good point about serializability that I hadn't considered -- @mikecote, do you foresee this being problematic for alerting? How are you planning/hoping to record the space/namespace which the alert should run in? Are you storing the space id, and translating to namespace when required, storing the namespace directly, or storing the base path and translating?

DEFAULT_SPACE_ID exists here only for backward compatibility?

DEFAULT_SPACE_ID is mostly for backwards compatibility, but it also serves as a way to allow the spaces plugin to be completely disabled/re-enabled at any time, without breaking Kibana.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are you planning/hoping to record the space/namespace which the alert should run in? Are you storing the space id, and translating to namespace when required, storing the namespace directly, or storing the base path and translating?

This will be a conversation I'll have to have with you when we do the security phase. Right now we store the basePath and create clients with that. Though there may be a few places that forget to store the space and assumes the default space.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be a conversation I'll have to have with you when we do the security phase. Right now we store the basePath and create clients with that. Though there may be a few places that forget to store the space and assumes the default space.

Ok, then this shouldn't be an issue for alerting then.

In general, specifying a custom namespace when using the saved objects client should be a very rare event. I consider the namespace field an implementation detail of the space plugin, even though it is an OSS concept. If the spaces plugin exposes first-class methods for translating a space id to a namespace (and vice-versa), then I don't foresee Symbols being problematic here.

Can we use a special string instead? What is the benefit of using a separate type?

A special string would have to include some character that is disallowed as part of a space id, in order to guarantee that we never have a collision. It's certainly doable, so long as we don't change the set of allowed characters in the future.

A symbol makes it painfully clear that we're dealing with a special case, as opposed to a mis-typed space id or similar.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider the namespace field an implementation detail of the space plugin, even though it is an OSS concept.

ok, agree. In this case symbol is internal detail and not a part of public contract. why it leaks to sibling plugins? https://github.com/elastic/kibana/pull/38444/files#diff-9b008eb2578ead2253f9faf64b262f0eR79

If the spaces plugin exposes first-class methods for translating a space id to a namespace (and vice-versa), then I don't foresee Symbols being problematic here.

with this API can we have separate 2 types. don't we?
type SavedObjectsNamespace = string | undefined; in OSS
type SavedObjectsNamespaceInternal = string | undefined | symbol; in x-pack/spaces.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it leaks to sibling plugins?

The type definition is declared in x-pack/types, which is shared by all x-pack plugins:

declare module 'src/core/server/saved_objects' {
type SavedObjectsNamespace = string | undefined | symbol;
interface SavedObjectsBaseOptions {
namespace?: SavedObjectsNamespace;
}
}

with this API can we have separate 2 types. don't we?
type SavedObjectsNamespace = string | undefined; in OSS
type SavedObjectsNamespaceInternal = string | undefined | symbol; in x-pack/spaces.

I don't know if it's really internal though. The wrapped saved objects client has a public api that expects either string | undefined | symbol, and this is consumable by both first and third-party plugins.

Plus, having a different name for this vs the OSS version makes overriding the type very difficult, or maybe even impossible (I haven't tried yet). The current override (above) is rather straightforward.

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@legrego
Copy link
Member Author

legrego commented Jul 16, 2019

Abandoned in favor of #40275

@legrego legrego closed this Jul 16, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release_note:skip Skip the PR/issue when compiling release notes review Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants