Skip to content

Commit

Permalink
Introduce mechanism to request default capabilities (elastic#86473) (e…
Browse files Browse the repository at this point in the history
  • Loading branch information
legrego authored Jan 5, 2021
1 parent 76d8b49 commit 965eb07
Show file tree
Hide file tree
Showing 15 changed files with 178 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,19 @@ How to restrict some capabilities
```ts
// my-plugin/server/plugin.ts
public setup(core: CoreSetup, deps: {}) {
core.capabilities.registerSwitcher((request, capabilities) => {
core.capabilities.registerSwitcher((request, capabilities, useDefaultCapabilities) => {
// useDefaultCapabilities is a special case that switchers typically don't have to concern themselves with.
// The default capabilities are typically the ones you provide in your CapabilitiesProvider, but this flag
// gives each switcher an opportunity to change the default capabilities of other plugins' capabilities.
// For example, you may decide to flip another plugin's capability to false if today is Tuesday,
// but you wouldn't want to do this when we are requesting the default set of capabilities.
if (useDefaultCapabilities) {
return {
somePlugin: {
featureEnabledByDefault: true
}
}
}
if(myPluginApi.shouldRestrictSomePluginBecauseOf(request)) {
return {
somePlugin: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ See [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md)
<b>Signature:</b>

```typescript
export declare type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities) => Partial<Capabilities> | Promise<Partial<Capabilities>>;
export declare type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities, useDefaultCapabilities: boolean) => Partial<Capabilities> | Promise<Partial<Capabilities>>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,36 @@ describe('#start', () => {
http.post.mockReturnValue(Promise.resolve(mockedCapabilities));
});

it('requests default capabilities on anonymous paths', async () => {
http.anonymousPaths.isAnonymous.mockReturnValue(true);
const service = new CapabilitiesService();
const appIds = ['app1', 'app2', 'legacyApp1', 'legacyApp2'];
const { capabilities } = await service.start({
http,
appIds,
});

expect(http.post).toHaveBeenCalledWith('/api/core/capabilities', {
query: {
useDefaultCapabilities: true,
},
body: JSON.stringify({ applications: appIds }),
});

// @ts-expect-error TypeScript knows this shouldn't be possible
expect(() => (capabilities.foo = 'foo')).toThrowError();
});

it('only returns capabilities for given appIds', async () => {
const service = new CapabilitiesService();
const appIds = ['app1', 'app2', 'legacyApp1', 'legacyApp2'];
const { capabilities } = await service.start({
http,
appIds: ['app1', 'app2', 'legacyApp1', 'legacyApp2'],
appIds,
});

expect(http.post).toHaveBeenCalledWith('/api/core/capabilities', {
body: JSON.stringify({ applications: appIds }),
});

// @ts-expect-error TypeScript knows this shouldn't be possible
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export interface CapabilitiesStart {
*/
export class CapabilitiesService {
public async start({ appIds, http }: StartDeps): Promise<CapabilitiesStart> {
const useDefaultCapabilities = http.anonymousPaths.isAnonymous(window.location.pathname);
const capabilities = await http.post<Capabilities>('/api/core/capabilities', {
query: useDefaultCapabilities ? { useDefaultCapabilities } : undefined,
body: JSON.stringify({ applications: appIds }),
});

Expand Down
16 changes: 14 additions & 2 deletions src/core/server/capabilities/capabilities_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,19 @@ export interface CapabilitiesSetup {
* ```ts
* // my-plugin/server/plugin.ts
* public setup(core: CoreSetup, deps: {}) {
* core.capabilities.registerSwitcher((request, capabilities) => {
* core.capabilities.registerSwitcher((request, capabilities, useDefaultCapabilities) => {
* // useDefaultCapabilities is a special case that switchers typically don't have to concern themselves with.
* // The default capabilities are typically the ones you provide in your CapabilitiesProvider, but this flag
* // gives each switcher an opportunity to change the default capabilities of other plugins' capabilities.
* // For example, you may decide to flip another plugin's capability to false if today is Tuesday,
* // but you wouldn't want to do this when we are requesting the default set of capabilities.
* if (useDefaultCapabilities) {
* return {
* somePlugin: {
* featureEnabledByDefault: true
* }
* }
* }
* if(myPluginApi.shouldRestrictSomePluginBecauseOf(request)) {
* return {
* somePlugin: {
Expand Down Expand Up @@ -150,7 +162,7 @@ export class CapabilitiesService {

public start(): CapabilitiesStart {
return {
resolveCapabilities: (request) => this.resolveCapabilities(request, []),
resolveCapabilities: (request) => this.resolveCapabilities(request, [], false),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,57 @@ describe('CapabilitiesService', () => {
`);
});

it('uses the service capabilities providers', async () => {
serviceSetup.registerProvider(() => ({
it('uses the service capabilities providers and switchers', async () => {
const getInitialCapabilities = () => ({
catalogue: {
something: true,
},
}));
management: {},
navLinks: {},
});
serviceSetup.registerProvider(() => getInitialCapabilities());

const switcher = jest.fn((_, capabilities) => capabilities);
serviceSetup.registerSwitcher(switcher);

const result = await supertest(httpSetup.server.listener)
.post('/api/core/capabilities')
.send({ applications: [] })
.expect(200);

expect(switcher).toHaveBeenCalledTimes(1);
expect(switcher).toHaveBeenCalledWith(expect.anything(), getInitialCapabilities(), false);
expect(result.body).toMatchInlineSnapshot(`
Object {
"catalogue": Object {
"something": true,
},
"management": Object {},
"navLinks": Object {},
}
`);
});

it('passes useDefaultCapabilities to registered switchers', async () => {
const getInitialCapabilities = () => ({
catalogue: {
something: true,
},
management: {},
navLinks: {},
});
serviceSetup.registerProvider(() => getInitialCapabilities());

const switcher = jest.fn((_, capabilities) => capabilities);
serviceSetup.registerSwitcher(switcher);

const result = await supertest(httpSetup.server.listener)
.post('/api/core/capabilities?useDefaultCapabilities=true')
.send({ applications: [] })
.expect(200);

expect(switcher).toHaveBeenCalledTimes(1);
expect(switcher).toHaveBeenCalledWith(expect.anything(), getInitialCapabilities(), true);
expect(result.body).toMatchInlineSnapshot(`
Object {
"catalogue": Object {
Expand Down
12 changes: 6 additions & 6 deletions src/core/server/capabilities/resolve_capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('resolveCapabilities', () => {
});

it('returns the initial capabilities if no switcher are used', async () => {
const result = await resolveCapabilities(defaultCaps, [], request, []);
const result = await resolveCapabilities(defaultCaps, [], request, [], true);
expect(result).toEqual(defaultCaps);
});

Expand All @@ -55,7 +55,7 @@ describe('resolveCapabilities', () => {
A: false,
},
});
const result = await resolveCapabilities(caps, [switcher], request, []);
const result = await resolveCapabilities(caps, [switcher], request, [], true);
expect(result).toMatchInlineSnapshot(`
Object {
"catalogue": Object {
Expand Down Expand Up @@ -83,7 +83,7 @@ describe('resolveCapabilities', () => {
A: false,
},
});
await resolveCapabilities(caps, [switcher], request, []);
await resolveCapabilities(caps, [switcher], request, [], true);
expect(caps.catalogue).toEqual({
A: true,
B: true,
Expand All @@ -105,7 +105,7 @@ describe('resolveCapabilities', () => {
C: false,
},
});
const result = await resolveCapabilities(caps, [switcher], request, []);
const result = await resolveCapabilities(caps, [switcher], request, [], true);
expect(result.catalogue).toEqual({
A: true,
B: true,
Expand All @@ -127,7 +127,7 @@ describe('resolveCapabilities', () => {
.filter(([key]) => key !== 'B')
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
});
const result = await resolveCapabilities(caps, [switcher], request, []);
const result = await resolveCapabilities(caps, [switcher], request, [], true);
expect(result.catalogue).toEqual({
A: true,
B: true,
Expand All @@ -153,7 +153,7 @@ describe('resolveCapabilities', () => {
record: false,
},
});
const result = await resolveCapabilities(caps, [switcher], request, []);
const result = await resolveCapabilities(caps, [switcher], request, [], true);
expect(result.section).toEqual({
boolean: true,
record: {
Expand Down
19 changes: 14 additions & 5 deletions src/core/server/capabilities/resolve_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,33 @@ import { KibanaRequest } from '../http';

export type CapabilitiesResolver = (
request: KibanaRequest,
applications: string[]
applications: string[],
useDefaultCapabilities: boolean
) => Promise<Capabilities>;

export const getCapabilitiesResolver = (
capabilities: () => Capabilities,
switchers: () => CapabilitiesSwitcher[]
): CapabilitiesResolver => async (
request: KibanaRequest,
applications: string[]
applications: string[],
useDefaultCapabilities: boolean
): Promise<Capabilities> => {
return resolveCapabilities(capabilities(), switchers(), request, applications);
return resolveCapabilities(
capabilities(),
switchers(),
request,
applications,
useDefaultCapabilities
);
};

export const resolveCapabilities = async (
capabilities: Capabilities,
switchers: CapabilitiesSwitcher[],
request: KibanaRequest,
applications: string[]
applications: string[],
useDefaultCapabilities: boolean
): Promise<Capabilities> => {
const mergedCaps = cloneDeep({
...capabilities,
Expand All @@ -54,7 +63,7 @@ export const resolveCapabilities = async (
});
return switchers.reduce(async (caps, switcher) => {
const resolvedCaps = await caps;
const changes = await switcher(request, resolvedCaps);
const changes = await switcher(request, resolvedCaps, useDefaultCapabilities);
return recursiveApplyChanges(resolvedCaps, changes);
}, Promise.resolve(mergedCaps));
};
Expand Down
6 changes: 5 additions & 1 deletion src/core/server/capabilities/routes/resolve_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ export function registerCapabilitiesRoutes(router: IRouter, resolver: Capabiliti
authRequired: 'optional',
},
validate: {
query: schema.object({
useDefaultCapabilities: schema.boolean({ defaultValue: false }),
}),
body: schema.object({
applications: schema.arrayOf(schema.string()),
}),
},
},
async (ctx, req, res) => {
const { useDefaultCapabilities } = req.query;
const { applications } = req.body;
const capabilities = await resolver(req, applications);
const capabilities = await resolver(req, applications, useDefaultCapabilities);
return res.ok({
body: capabilities,
});
Expand Down
3 changes: 2 additions & 1 deletion src/core/server/capabilities/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ export type CapabilitiesProvider = () => Partial<Capabilities>;
*/
export type CapabilitiesSwitcher = (
request: KibanaRequest,
uiCapabilities: Capabilities
uiCapabilities: Capabilities,
useDefaultCapabilities: boolean
) => Partial<Capabilities> | Promise<Partial<Capabilities>>;
2 changes: 1 addition & 1 deletion src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export interface CapabilitiesStart {
}

// @public
export type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities) => Partial<Capabilities> | Promise<Partial<Capabilities>>;
export type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities, useDefaultCapabilities: boolean) => Partial<Capabilities> | Promise<Partial<Capabilities>>;

// @alpha
export const config: {
Expand Down
Loading

0 comments on commit 965eb07

Please sign in to comment.