Skip to content

Commit

Permalink
[Saved Objects] adds support for including hidden types in saved obje…
Browse files Browse the repository at this point in the history
…cts client (elastic#66879)

As part of the work needed for RBAC & Feature Controls support in Alerting (elastic#43994) we've identified a need to make the Alert Saved Object type a _hidden_ type.

As we still need support for Security and Spaces, we wish to use the standard SavedObjectsClient and its middleware, but currently this isn't possible with _hidden_ types.

To address that, this PR adds support for creating a client which includes hidden types.
  • Loading branch information
gmmorris authored May 20, 2020
1 parent 42d21bc commit dfa22d1
Show file tree
Hide file tree
Showing 44 changed files with 615 additions and 185 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Describes the factory used to create instances of the Saved Objects Client.
<b>Signature:</b>

```typescript
export declare type SavedObjectsClientFactory = ({ request, }: {
export declare type SavedObjectsClientFactory = ({ request, includedHiddenTypes, }: {
request: KibanaRequest;
includedHiddenTypes?: string[];
}) => SavedObjectsClientContract;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) &gt; [includedHiddenTypes](./kibana-plugin-core-server.savedobjectsclientprovideroptions.includedhiddentypes.md)

## SavedObjectsClientProviderOptions.includedHiddenTypes property

<b>Signature:</b>

```typescript
includedHiddenTypes?: string[];
```
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface SavedObjectsClientProviderOptions
| Property | Type | Description |
| --- | --- | --- |
| [excludedWrappers](./kibana-plugin-core-server.savedobjectsclientprovideroptions.excludedwrappers.md) | <code>string[]</code> | |
| [includedHiddenTypes](./kibana-plugin-core-server.savedobjectsclientprovideroptions.includedhiddentypes.md) | <code>string[]</code> | |

Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsre
<b>Signature:</b>

```typescript
createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository;
createInternalRepository: (includedHiddenTypes?: string[]) => ISavedObjectsRepository;
```
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsre
<b>Signature:</b>

```typescript
createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository;
createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository;
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export interface SavedObjectsRepositoryFactory

| Property | Type | Description |
| --- | --- | --- |
| [createInternalRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createinternalrepository.md) | <code>(extraTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. |
| [createScopedRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createscopedrepository.md) | <code>(req: KibanaRequest, extraTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. |
| [createInternalRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createinternalrepository.md) | <code>(includedHiddenTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. |
| [createScopedRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createscopedrepository.md) | <code>(req: KibanaRequest, includedHiddenTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. |

Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsre
<b>Signature:</b>

```typescript
createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository;
createInternalRepository: (includedHiddenTypes?: string[]) => ISavedObjectsRepository;
```
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsre
<b>Signature:</b>

```typescript
createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository;
createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository;
```

## Remarks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export interface SavedObjectsServiceStart

| Property | Type | Description |
| --- | --- | --- |
| [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) | <code>(extraTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. |
| [createScopedRepository](./kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | <code>(req: KibanaRequest, extraTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. |
| [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) | <code>(includedHiddenTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. |
| [createScopedRepository](./kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | <code>(req: KibanaRequest, includedHiddenTypes?: string[]) =&gt; ISavedObjectsRepository</code> | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. |
| [createSerializer](./kibana-plugin-core-server.savedobjectsservicestart.createserializer.md) | <code>() =&gt; SavedObjectsSerializer</code> | Creates a [serializer](./kibana-plugin-core-server.savedobjectsserializer.md) that is aware of all registered types. |
| [getScopedClient](./kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | <code>(req: KibanaRequest, options?: SavedObjectsClientProviderOptions) =&gt; SavedObjectsClientContract</code> | Creates a [Saved Objects client](./kibana-plugin-core-server.savedobjectsclientcontract.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. If other plugins have registered Saved Objects client wrappers, these will be applied to extend the functionality of the client.<!-- -->A client that is already scoped to the incoming request is also exposed from the route handler context see [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md)<!-- -->. |
| [getTypeRegistry](./kibana-plugin-core-server.savedobjectsservicestart.gettyperegistry.md) | <code>() =&gt; ISavedObjectTypeRegistry</code> | Returns the [registry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) containing all registered [saved object types](./kibana-plugin-core-server.savedobjectstype.md) |
Expand Down
85 changes: 85 additions & 0 deletions src/core/server/saved_objects/saved_objects_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import { legacyServiceMock } from '../legacy/legacy_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { SavedObjectsClientFactoryProvider } from './service/lib';
import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version';
import { SavedObjectsRepository } from './service/lib/repository';
import { KibanaRequest } from '../http';

jest.mock('./service/lib/repository');

describe('SavedObjectsService', () => {
const createCoreContext = ({
Expand Down Expand Up @@ -269,5 +273,86 @@ describe('SavedObjectsService', () => {
expect(getTypeRegistry()).toBe(typeRegistryInstanceMock);
});
});

describe('#createScopedRepository', () => {
it('creates a respository scoped to the user', async () => {
const coreContext = createCoreContext({ skipMigration: false });
const soService = new SavedObjectsService(coreContext);
const coreSetup = createSetupDeps();
await soService.setup(coreSetup);
const { createScopedRepository } = await soService.start({});

const req = {} as KibanaRequest;
createScopedRepository(req);

expect(coreSetup.elasticsearch.adminClient.asScoped).toHaveBeenCalledWith(req);

const [
{
value: { callAsCurrentUser },
},
] = coreSetup.elasticsearch.adminClient.asScoped.mock.results;

const [
[, , , callCluster, includedHiddenTypes],
] = (SavedObjectsRepository.createRepository as jest.Mocked<any>).mock.calls;

expect(callCluster).toBe(callAsCurrentUser);
expect(includedHiddenTypes).toEqual([]);
});

it('creates a respository including hidden types when specified', async () => {
const coreContext = createCoreContext({ skipMigration: false });
const soService = new SavedObjectsService(coreContext);
const coreSetup = createSetupDeps();
await soService.setup(coreSetup);
const { createScopedRepository } = await soService.start({});

const req = {} as KibanaRequest;
createScopedRepository(req, ['someHiddenType']);

const [
[, , , , includedHiddenTypes],
] = (SavedObjectsRepository.createRepository as jest.Mocked<any>).mock.calls;

expect(includedHiddenTypes).toEqual(['someHiddenType']);
});
});

describe('#createInternalRepository', () => {
it('creates a respository using the admin user', async () => {
const coreContext = createCoreContext({ skipMigration: false });
const soService = new SavedObjectsService(coreContext);
const coreSetup = createSetupDeps();
await soService.setup(coreSetup);
const { createInternalRepository } = await soService.start({});

createInternalRepository();

const [
[, , , callCluster, includedHiddenTypes],
] = (SavedObjectsRepository.createRepository as jest.Mocked<any>).mock.calls;

expect(coreSetup.elasticsearch.adminClient.callAsInternalUser).toBe(callCluster);
expect(callCluster).toBe(coreSetup.elasticsearch.adminClient.callAsInternalUser);
expect(includedHiddenTypes).toEqual([]);
});

it('creates a respository including hidden types when specified', async () => {
const coreContext = createCoreContext({ skipMigration: false });
const soService = new SavedObjectsService(coreContext);
const coreSetup = createSetupDeps();
await soService.setup(coreSetup);
const { createInternalRepository } = await soService.start({});

createInternalRepository(['someHiddenType']);

const [
[, , , , includedHiddenTypes],
] = (SavedObjectsRepository.createRepository as jest.Mocked<any>).mock.calls;

expect(includedHiddenTypes).toEqual(['someHiddenType']);
});
});
});
});
38 changes: 22 additions & 16 deletions src/core/server/saved_objects/saved_objects_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,20 +198,23 @@ export interface SavedObjectsServiceStart {
* Elasticsearch.
*
* @param req - The request to create the scoped repository from.
* @param extraTypes - A list of additional hidden types the repository should have access to.
* @param includedHiddenTypes - A list of additional hidden types the repository should have access to.
*
* @remarks
* Prefer using `getScopedClient`. This should only be used when using methods
* not exposed on {@link SavedObjectsClientContract}
*/
createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository;
createScopedRepository: (
req: KibanaRequest,
includedHiddenTypes?: string[]
) => ISavedObjectsRepository;
/**
* Creates a {@link ISavedObjectsRepository | Saved Objects repository} that
* uses the internal Kibana user for authenticating with Elasticsearch.
*
* @param extraTypes - A list of additional hidden types the repository should have access to.
* @param includedHiddenTypes - A list of additional hidden types the repository should have access to.
*/
createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository;
createInternalRepository: (includedHiddenTypes?: string[]) => ISavedObjectsRepository;
/**
* Creates a {@link SavedObjectsSerializer | serializer} that is aware of all registered types.
*/
Expand Down Expand Up @@ -246,16 +249,19 @@ export interface SavedObjectsRepositoryFactory {
* uses the credentials from the passed in request to authenticate with
* Elasticsearch.
*
* @param extraTypes - A list of additional hidden types the repository should have access to.
* @param includedHiddenTypes - A list of additional hidden types the repository should have access to.
*/
createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository;
createScopedRepository: (
req: KibanaRequest,
includedHiddenTypes?: string[]
) => ISavedObjectsRepository;
/**
* Creates a {@link ISavedObjectsRepository | Saved Objects repository} that
* uses the internal Kibana user for authenticating with Elasticsearch.
*
* @param extraTypes - A list of additional hidden types the repository should have access to.
* @param includedHiddenTypes - A list of additional hidden types the repository should have access to.
*/
createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository;
createInternalRepository: (includedHiddenTypes?: string[]) => ISavedObjectsRepository;
}

/** @internal */
Expand Down Expand Up @@ -417,26 +423,26 @@ export class SavedObjectsService
await migrator.runMigrations();
}

const createRepository = (callCluster: APICaller, extraTypes: string[] = []) => {
const createRepository = (callCluster: APICaller, includedHiddenTypes: string[] = []) => {
return SavedObjectsRepository.createRepository(
migrator,
this.typeRegistry,
kibanaConfig.index,
callCluster,
extraTypes
includedHiddenTypes
);
};

const repositoryFactory: SavedObjectsRepositoryFactory = {
createInternalRepository: (extraTypes?: string[]) =>
createRepository(adminClient.callAsInternalUser, extraTypes),
createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) =>
createRepository(adminClient.asScoped(req).callAsCurrentUser, extraTypes),
createInternalRepository: (includedHiddenTypes?: string[]) =>
createRepository(adminClient.callAsInternalUser, includedHiddenTypes),
createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) =>
createRepository(adminClient.asScoped(req).callAsCurrentUser, includedHiddenTypes),
};

const clientProvider = new SavedObjectsClientProvider({
defaultClientFactory({ request }) {
const repository = repositoryFactory.createScopedRepository(request);
defaultClientFactory({ request, includedHiddenTypes }) {
const repository = repositoryFactory.createScopedRepository(request, includedHiddenTypes);
return new SavedObjectsClient(repository);
},
typeRegistry: this.typeRegistry,
Expand Down
6 changes: 3 additions & 3 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,22 @@ export class SavedObjectsRepository {
typeRegistry: SavedObjectTypeRegistry,
indexName: string,
callCluster: APICaller,
extraTypes: string[] = [],
includedHiddenTypes: string[] = [],
injectedConstructor: any = SavedObjectsRepository
): ISavedObjectsRepository {
const mappings = migrator.getActiveMappings();
const allTypes = Object.keys(getRootPropertiesObjects(mappings));
const serializer = new SavedObjectsSerializer(typeRegistry);
const visibleTypes = allTypes.filter(type => !typeRegistry.isHidden(type));

const missingTypeMappings = extraTypes.filter(type => !allTypes.includes(type));
const missingTypeMappings = includedHiddenTypes.filter(type => !allTypes.includes(type));
if (missingTypeMappings.length > 0) {
throw new Error(
`Missing mappings for saved objects types: '${missingTypeMappings.join(', ')}'`
);
}

const allowedTypes = [...new Set(visibleTypes.concat(extraTypes))];
const allowedTypes = [...new Set(visibleTypes.concat(includedHiddenTypes))];

return new injectedConstructor({
index: indexName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,23 @@ test(`allows all wrappers to be excluded`, () => {
expect(firstClientWrapperFactoryMock).not.toHaveBeenCalled();
expect(secondClientWrapperFactoryMock).not.toHaveBeenCalled();
});

test(`allows hidden typed to be included`, () => {
const defaultClient = Symbol();
const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient);
const clientProvider = new SavedObjectsClientProvider({
defaultClientFactory: defaultClientFactoryMock,
typeRegistry: typeRegistryMock.create(),
});
const request = Symbol();

const actualClient = clientProvider.getClient(request, {
includedHiddenTypes: ['task'],
});

expect(actualClient).toBe(defaultClient);
expect(defaultClientFactoryMock).toHaveBeenCalledWith({
request,
includedHiddenTypes: ['task'],
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ export type SavedObjectsClientWrapperFactory = (
*/
export type SavedObjectsClientFactory = ({
request,
includedHiddenTypes,
}: {
request: KibanaRequest;
includedHiddenTypes?: string[];
}) => SavedObjectsClientContract;

/**
Expand All @@ -64,6 +66,7 @@ export type SavedObjectsClientFactoryProvider = (
*/
export interface SavedObjectsClientProviderOptions {
excludedWrappers?: string[];
includedHiddenTypes?: string[];
}

/**
Expand Down Expand Up @@ -121,14 +124,13 @@ export class SavedObjectsClientProvider {

getClient(
request: KibanaRequest,
options: SavedObjectsClientProviderOptions = {}
{ includedHiddenTypes, excludedWrappers = [] }: SavedObjectsClientProviderOptions = {}
): SavedObjectsClientContract {
const client = this._clientFactory({
request,
includedHiddenTypes,
});

const excludedWrappers = options.excludedWrappers || [];

return this._wrapperFactories
.toPrioritizedArray()
.reduceRight((clientToWrap, { id, factory }) => {
Expand Down
Loading

0 comments on commit dfa22d1

Please sign in to comment.