-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce capabilities provider and switcher to file upload plugin (#…
…96593) Co-authored-by: Kibana Machine <[email protected]>
- Loading branch information
1 parent
108252b
commit c572ddd
Showing
6 changed files
with
382 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { setupCapabilities } from './capabilities'; | ||
import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; | ||
import { Capabilities, CoreStart } from 'kibana/server'; | ||
import { securityMock } from '../../security/server/mocks'; | ||
|
||
describe('setupCapabilities', () => { | ||
it('registers a capabilities provider for the file upload feature', () => { | ||
const coreSetup = coreMock.createSetup(); | ||
setupCapabilities(coreSetup); | ||
|
||
expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); | ||
const [provider] = coreSetup.capabilities.registerProvider.mock.calls[0]; | ||
expect(provider()).toMatchInlineSnapshot(` | ||
Object { | ||
"fileUpload": Object { | ||
"show": true, | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
it('registers a capabilities switcher that returns unaltered capabilities when security is disabled', async () => { | ||
const coreSetup = coreMock.createSetup(); | ||
setupCapabilities(coreSetup); | ||
|
||
expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); | ||
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; | ||
|
||
const capabilities = { | ||
navLinks: {}, | ||
management: {}, | ||
catalogue: {}, | ||
fileUpload: { | ||
show: true, | ||
}, | ||
} as Capabilities; | ||
|
||
const request = httpServerMock.createKibanaRequest(); | ||
|
||
await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` | ||
Object { | ||
"catalogue": Object {}, | ||
"fileUpload": Object { | ||
"show": true, | ||
}, | ||
"management": Object {}, | ||
"navLinks": Object {}, | ||
} | ||
`); | ||
}); | ||
|
||
it('registers a capabilities switcher that returns unaltered capabilities when default capabilities are requested', async () => { | ||
const coreSetup = coreMock.createSetup(); | ||
const security = securityMock.createStart(); | ||
security.authz.mode.useRbacForRequest.mockReturnValue(true); | ||
coreSetup.getStartServices.mockResolvedValue([ | ||
(undefined as unknown) as CoreStart, | ||
{ security }, | ||
undefined, | ||
]); | ||
setupCapabilities(coreSetup); | ||
|
||
expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); | ||
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; | ||
|
||
const capabilities = { | ||
navLinks: {}, | ||
management: {}, | ||
catalogue: {}, | ||
fileUpload: { | ||
show: true, | ||
}, | ||
} as Capabilities; | ||
|
||
const request = httpServerMock.createKibanaRequest(); | ||
|
||
await expect(switcher(request, capabilities, true)).resolves.toMatchInlineSnapshot(` | ||
Object { | ||
"catalogue": Object {}, | ||
"fileUpload": Object { | ||
"show": true, | ||
}, | ||
"management": Object {}, | ||
"navLinks": Object {}, | ||
} | ||
`); | ||
|
||
expect(security.authz.mode.useRbacForRequest).not.toHaveBeenCalled(); | ||
expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('registers a capabilities switcher that disables capabilities for underprivileged users', async () => { | ||
const coreSetup = coreMock.createSetup(); | ||
const security = securityMock.createStart(); | ||
security.authz.mode.useRbacForRequest.mockReturnValue(true); | ||
|
||
const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: false }); | ||
security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); | ||
coreSetup.getStartServices.mockResolvedValue([ | ||
(undefined as unknown) as CoreStart, | ||
{ security }, | ||
undefined, | ||
]); | ||
setupCapabilities(coreSetup); | ||
|
||
expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); | ||
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; | ||
|
||
const capabilities = { | ||
navLinks: {}, | ||
management: {}, | ||
catalogue: {}, | ||
fileUpload: { | ||
show: true, | ||
}, | ||
} as Capabilities; | ||
|
||
const request = httpServerMock.createKibanaRequest(); | ||
|
||
await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` | ||
Object { | ||
"fileUpload": Object { | ||
"show": false, | ||
}, | ||
} | ||
`); | ||
|
||
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); | ||
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); | ||
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); | ||
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); | ||
}); | ||
|
||
it('registers a capabilities switcher that enables capabilities for privileged users', async () => { | ||
const coreSetup = coreMock.createSetup(); | ||
const security = securityMock.createStart(); | ||
security.authz.mode.useRbacForRequest.mockReturnValue(true); | ||
|
||
const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: true }); | ||
security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); | ||
coreSetup.getStartServices.mockResolvedValue([ | ||
(undefined as unknown) as CoreStart, | ||
{ security }, | ||
undefined, | ||
]); | ||
setupCapabilities(coreSetup); | ||
|
||
expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); | ||
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; | ||
|
||
const capabilities = { | ||
navLinks: {}, | ||
management: {}, | ||
catalogue: {}, | ||
fileUpload: { | ||
show: true, | ||
}, | ||
} as Capabilities; | ||
|
||
const request = httpServerMock.createKibanaRequest(); | ||
|
||
await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` | ||
Object { | ||
"catalogue": Object {}, | ||
"fileUpload": Object { | ||
"show": true, | ||
}, | ||
"management": Object {}, | ||
"navLinks": Object {}, | ||
} | ||
`); | ||
|
||
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); | ||
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); | ||
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); | ||
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); | ||
}); | ||
|
||
it('registers a capabilities switcher that disables capabilities for unauthenticated requests', async () => { | ||
const coreSetup = coreMock.createSetup(); | ||
const security = securityMock.createStart(); | ||
security.authz.mode.useRbacForRequest.mockReturnValue(true); | ||
const mockCheckPrivileges = jest | ||
.fn() | ||
.mockRejectedValue(new Error('this should not have been called')); | ||
security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); | ||
coreSetup.getStartServices.mockResolvedValue([ | ||
(undefined as unknown) as CoreStart, | ||
{ security }, | ||
undefined, | ||
]); | ||
setupCapabilities(coreSetup); | ||
|
||
expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); | ||
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; | ||
|
||
const capabilities = { | ||
navLinks: {}, | ||
management: {}, | ||
catalogue: {}, | ||
fileUpload: { | ||
show: true, | ||
}, | ||
} as Capabilities; | ||
|
||
const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } }); | ||
|
||
await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` | ||
Object { | ||
"fileUpload": Object { | ||
"show": false, | ||
}, | ||
} | ||
`); | ||
|
||
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); | ||
expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('registers a capabilities switcher that skips privilege check for requests not using rbac', async () => { | ||
const coreSetup = coreMock.createSetup(); | ||
const security = securityMock.createStart(); | ||
security.authz.mode.useRbacForRequest.mockReturnValue(false); | ||
coreSetup.getStartServices.mockResolvedValue([ | ||
(undefined as unknown) as CoreStart, | ||
{ security }, | ||
undefined, | ||
]); | ||
setupCapabilities(coreSetup); | ||
|
||
expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); | ||
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; | ||
|
||
const capabilities = { | ||
navLinks: {}, | ||
management: {}, | ||
catalogue: {}, | ||
fileUpload: { | ||
show: true, | ||
}, | ||
} as Capabilities; | ||
|
||
const request = httpServerMock.createKibanaRequest(); | ||
|
||
await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` | ||
Object { | ||
"catalogue": Object {}, | ||
"fileUpload": Object { | ||
"show": true, | ||
}, | ||
"management": Object {}, | ||
"navLinks": Object {}, | ||
} | ||
`); | ||
|
||
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); | ||
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); | ||
expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { CoreSetup } from 'kibana/server'; | ||
import { checkFileUploadPrivileges } from './check_privileges'; | ||
import { StartDeps } from './types'; | ||
|
||
export const setupCapabilities = ( | ||
core: Pick<CoreSetup<StartDeps>, 'capabilities' | 'getStartServices'> | ||
) => { | ||
core.capabilities.registerProvider(() => { | ||
return { | ||
fileUpload: { | ||
show: true, | ||
}, | ||
}; | ||
}); | ||
|
||
core.capabilities.registerSwitcher(async (request, capabilities, useDefaultCapabilities) => { | ||
if (useDefaultCapabilities) { | ||
return capabilities; | ||
} | ||
const [, { security }] = await core.getStartServices(); | ||
|
||
// Check the bare minimum set of privileges required to get some utility out of this feature | ||
const { hasImportPermission } = await checkFileUploadPrivileges({ | ||
authorization: security?.authz, | ||
request, | ||
checkCreateIndexPattern: true, | ||
checkHasManagePipeline: false, | ||
}); | ||
|
||
if (!hasImportPermission) { | ||
return { | ||
fileUpload: { | ||
show: false, | ||
}, | ||
}; | ||
} | ||
|
||
return capabilities; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { KibanaRequest } from 'kibana/server'; | ||
import { AuthorizationServiceSetup, CheckPrivilegesPayload } from '../../security/server'; | ||
|
||
interface Deps { | ||
request: KibanaRequest; | ||
authorization?: Pick< | ||
AuthorizationServiceSetup, | ||
'mode' | 'actions' | 'checkPrivilegesDynamicallyWithRequest' | ||
>; | ||
checkHasManagePipeline: boolean; | ||
checkCreateIndexPattern: boolean; | ||
indexName?: string; | ||
} | ||
|
||
export const checkFileUploadPrivileges = async ({ | ||
request, | ||
authorization, | ||
checkHasManagePipeline, | ||
checkCreateIndexPattern, | ||
indexName, | ||
}: Deps) => { | ||
const requiresAuthz = authorization?.mode.useRbacForRequest(request) ?? false; | ||
|
||
if (!authorization || !requiresAuthz) { | ||
return { hasImportPermission: true }; | ||
} | ||
|
||
if (!request.auth.isAuthenticated) { | ||
return { hasImportPermission: false }; | ||
} | ||
|
||
const checkPrivilegesPayload: CheckPrivilegesPayload = { | ||
elasticsearch: { | ||
cluster: checkHasManagePipeline ? ['manage_pipeline'] : [], | ||
index: indexName ? { [indexName]: ['create', 'create_index'] } : {}, | ||
}, | ||
}; | ||
if (checkCreateIndexPattern) { | ||
checkPrivilegesPayload.kibana = [ | ||
authorization.actions.savedObject.get('index-pattern', 'create'), | ||
]; | ||
} | ||
|
||
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(request); | ||
const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload); | ||
|
||
return { hasImportPermission: checkPrivilegesResp.hasAllRequested }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.