diff --git a/x-pack/plugins/code/public/actions/index.ts b/x-pack/plugins/code/public/actions/index.ts index 5237a7887df9d..6f12693e43bdd 100644 --- a/x-pack/plugins/code/public/actions/index.ts +++ b/x-pack/plugins/code/public/actions/index.ts @@ -11,7 +11,6 @@ export * from './search'; export * from './file'; export * from './structure'; export * from './editor'; -export * from './user'; export * from './commit'; export * from './status'; export * from './project_config'; diff --git a/x-pack/plugins/code/public/actions/user.ts b/x-pack/plugins/code/public/actions/user.ts deleted file mode 100644 index 45e0a2a158fba..0000000000000 --- a/x-pack/plugins/code/public/actions/user.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createAction } from 'redux-actions'; - -export const loadUserProfile = createAction('LOAD USER PROFILE'); -export const loadUserProfileSuccess = createAction('LOAD USER PROFILE SUCCESS'); -export const loadUserProfileFailed = createAction('LOAD USER PROFILE FAILED'); diff --git a/x-pack/plugins/code/public/components/admin_page/admin.tsx b/x-pack/plugins/code/public/components/admin_page/admin.tsx index 8bc89c3a80239..ea9bec6ad5335 100644 --- a/x-pack/plugins/code/public/components/admin_page/admin.tsx +++ b/x-pack/plugins/code/public/components/admin_page/admin.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTab, EuiTabs } from '@elastic/eui'; +import { parse as parseQuery } from 'querystring'; import React from 'react'; import { connect } from 'react-redux'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; import styled from 'styled-components'; import url from 'url'; +import { EuiTab, EuiTabs } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { parse as parseQuery } from 'querystring'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; + import { Repository } from '../../../model'; import { RootState } from '../../reducers'; import { EmptyProject } from './empty_project'; @@ -39,7 +40,6 @@ enum AdminTabs { interface Props extends RouteComponentProps { repositories: Repository[]; repositoryLoading: boolean; - isAdmin: boolean; } interface State { @@ -120,7 +120,7 @@ class AdminPage extends React.PureComponent { const repositoriesCount = this.props.repositories.length; const showEmpty = repositoriesCount === 0 && !this.props.repositoryLoading; if (showEmpty) { - return ; + return ; } return ; } @@ -142,7 +142,6 @@ class AdminPage extends React.PureComponent { const mapStateToProps = (state: RootState) => ({ repositories: state.repository.repositories, repositoryLoading: state.repository.loading, - isAdmin: state.userProfile.isCodeAdmin, }); export const Admin = withRouter(connect(mapStateToProps)(AdminPage)); diff --git a/x-pack/plugins/code/public/components/admin_page/empty_project.tsx b/x-pack/plugins/code/public/components/admin_page/empty_project.tsx index a03bed6cfb243..4a68fa91634d3 100644 --- a/x-pack/plugins/code/public/components/admin_page/empty_project.tsx +++ b/x-pack/plugins/code/public/components/admin_page/empty_project.tsx @@ -4,26 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; import { Link } from 'react-router-dom'; + +import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; +import { uiCapabilities } from 'ui/capabilities'; + import { ImportProject } from './import_project'; -export const EmptyProject = ({ isAdmin }: { isAdmin: boolean }) => ( -
- -
- -

You don't have any projects yet

-
- {isAdmin &&

Let's import your first one

}
+export const EmptyProject = () => { + const isAdmin = uiCapabilities.code.admin as boolean; + return ( +
+ +
+ +

You don't have any projects yet

+
+ {isAdmin &&

Let's import your first one

}
+
+ {isAdmin && } + + + + View the Setup Guide + +
- {isAdmin && } - - - - View the Setup Guide - - -
-); + ); +}; diff --git a/x-pack/plugins/code/public/components/admin_page/project_tab.tsx b/x-pack/plugins/code/public/components/admin_page/project_tab.tsx index 72ef6a12bcbf1..425c62a451636 100644 --- a/x-pack/plugins/code/public/components/admin_page/project_tab.tsx +++ b/x-pack/plugins/code/public/components/admin_page/project_tab.tsx @@ -29,6 +29,8 @@ import moment from 'moment'; import React, { ChangeEvent } from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; +import { uiCapabilities } from 'ui/capabilities'; + import { Repository } from '../../../model'; import { closeToast, importRepo } from '../../actions'; import { RepoStatus, RootState } from '../../reducers'; @@ -77,7 +79,6 @@ const sortOptions = [ interface Props { projects: Repository[]; status: { [key: string]: RepoStatus }; - isAdmin: boolean; importRepo: (repoUrl: string) => void; importLoading: boolean; toastMessage?: string; @@ -192,7 +193,7 @@ class CodeProjectTab extends React.PureComponent { }; public render() { - const { projects, isAdmin, status, toastMessage, showToast, toastType } = this.props; + const { projects, status, toastMessage, showToast, toastType } = this.props; const projectsCount = projects.length; const modal = this.state.showImportProjectModal && this.renderImportModal(); @@ -205,7 +206,7 @@ class CodeProjectTab extends React.PureComponent { project={repo} showStatus={true} status={status[repo.uri]} - enableManagement={isAdmin} + enableManagement={uiCapabilities.code.admin as boolean} /> )); @@ -243,7 +244,7 @@ class CodeProjectTab extends React.PureComponent { - {isAdmin && ( + {(uiCapabilities.code.admin as boolean) && ( // @ts-ignore Add New Project @@ -270,7 +271,6 @@ class CodeProjectTab extends React.PureComponent { const mapStateToProps = (state: RootState) => ({ projects: state.repository.repositories, status: state.status.status, - isAdmin: state.userProfile.isCodeAdmin, importLoading: state.repository.importLoading, toastMessage: state.repository.toastMessage, toastType: state.repository.toastType, diff --git a/x-pack/plugins/code/public/reducers/index.ts b/x-pack/plugins/code/public/reducers/index.ts index adceaacc72a6d..8c8ebbee447ae 100644 --- a/x-pack/plugins/code/public/reducers/index.ts +++ b/x-pack/plugins/code/public/reducers/index.ts @@ -18,7 +18,6 @@ import { setup, SetupState } from './setup'; import { shortcuts, ShortcutsState } from './shortcuts'; import { RepoState, RepoStatus, status, StatusState } from './status'; import { symbol, SymbolState } from './symbol'; -import { userProfile, UserProfileState } from './user'; export { RepoState, RepoStatus }; @@ -30,7 +29,6 @@ export interface RootState { editor: EditorState; route: RouteState; status: StatusState; - userProfile: UserProfileState; commit: CommitState; blame: BlameState; languageServer: LanguageServerState; @@ -46,7 +44,6 @@ const reducers = { search, route, status, - userProfile, commit, blame, languageServer, diff --git a/x-pack/plugins/code/public/reducers/user.ts b/x-pack/plugins/code/public/reducers/user.ts deleted file mode 100644 index 95f99f356979a..0000000000000 --- a/x-pack/plugins/code/public/reducers/user.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import produce from 'immer'; -import { Action, handleActions } from 'redux-actions'; - -import { loadUserProfile, loadUserProfileFailed, loadUserProfileSuccess } from '../actions'; - -export interface UserProfileState { - isCodeAdmin: boolean; - isCodeUser: boolean; - error?: Error; -} - -const initialState: UserProfileState = { - isCodeAdmin: false, - isCodeUser: false, -}; - -export const userProfile = handleActions( - { - [String(loadUserProfile)]: (state: UserProfileState, action: Action) => - produce(state, draft => { - draft.error = undefined; - }), - [String(loadUserProfileSuccess)]: (state: UserProfileState, action: Action) => - produce(state, draft => { - if (action.payload!.roles) { - // If security is enabled and the roles field is set. Then we should check the - // 'code_admin' and 'code_user' roles. - draft.isCodeAdmin = - action.payload!.roles.includes('code_admin') || - // 'superuser' should be deemed as code admin user as well. - action.payload!.roles.includes('superuser'); - draft.isCodeUser = action.payload!.roles.includes('code_user'); - } else { - // If security is not enabled, then every user is code admin. - draft.isCodeAdmin = true; - } - }), - [String(loadUserProfileFailed)]: (state: UserProfileState, action: Action) => { - if (action.payload) { - return produce(state, draft => { - draft.error = action.payload; - }); - } else { - return state; - } - }, - }, - initialState -); diff --git a/x-pack/plugins/code/public/sagas/index.ts b/x-pack/plugins/code/public/sagas/index.ts index e6b43cc3d4b7b..41219b79ac7be 100644 --- a/x-pack/plugins/code/public/sagas/index.ts +++ b/x-pack/plugins/code/public/sagas/index.ts @@ -43,7 +43,6 @@ import { import { watchRootRoute } from './setup'; import { watchRepoCloneSuccess, watchRepoDeleteFinished } from './status'; import { watchLoadStructure } from './structure'; -import { watchLoadUserProfile } from './user'; export function* rootSaga() { yield fork(watchRootRoute); @@ -63,7 +62,6 @@ export function* rootSaga() { yield fork(watchInitRepoCmd); yield fork(watchGotoRepo); yield fork(watchLoadRepo); - yield fork(watchLoadUserProfile); yield fork(watchSearchRouteChange); yield fork(watchAdminRouteChange); yield fork(watchMainRouteChange); diff --git a/x-pack/plugins/code/public/sagas/repository.ts b/x-pack/plugins/code/public/sagas/repository.ts index 1f809b16cbd68..7f679097b7d94 100644 --- a/x-pack/plugins/code/public/sagas/repository.ts +++ b/x-pack/plugins/code/public/sagas/repository.ts @@ -25,7 +25,6 @@ import { indexRepoFailed, indexRepoSuccess, initRepoCommand, - loadUserProfile, updateCloneProgress, updateDeleteProgress, updateIndexProgress, @@ -171,7 +170,6 @@ export function* watchGotoRepo() { } function* handleAdminRouteChange() { - yield put(loadUserProfile()); yield put(fetchRepos()); yield put(fetchRepoConfigs()); yield put(loadLanguageServers()); diff --git a/x-pack/plugins/code/public/sagas/user.ts b/x-pack/plugins/code/public/sagas/user.ts deleted file mode 100644 index d7e95915a34c2..0000000000000 --- a/x-pack/plugins/code/public/sagas/user.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Action } from 'redux-actions'; -import { kfetch } from 'ui/kfetch'; - -import { call, put, takeEvery } from 'redux-saga/effects'; -import { loadUserProfile, loadUserProfileFailed, loadUserProfileSuccess } from '../actions'; - -function requestUserProfile() { - return kfetch({ - pathname: `/api/security/v1/me`, - method: 'get', - }); -} - -function* handleLoadUserProfile(_: Action) { - try { - const data = yield call(requestUserProfile); - yield put(loadUserProfileSuccess(data)); - } catch (err) { - yield put(loadUserProfileFailed(err)); - } -} - -export function* watchLoadUserProfile() { - yield takeEvery(String(loadUserProfile), handleLoadUserProfile); -} diff --git a/x-pack/plugins/code/server/init.ts b/x-pack/plugins/code/server/init.ts index 309683d7930c9..da70087c9a0e6 100644 --- a/x-pack/plugins/code/server/init.ts +++ b/x-pack/plugins/code/server/init.ts @@ -6,6 +6,8 @@ import { Server } from 'hapi'; import fetch from 'node-fetch'; +import { i18n } from '@kbn/i18n'; +import { XPackMainPlugin } from '../../xpack_main/xpack_main'; import { checkRepos } from './check_repos'; import { LspIndexerFactory, RepositoryIndexInitializerFactory, tryMigrateIndices } from './indexer'; import { EsClient, Esqueue } from './lib/esqueue'; @@ -25,7 +27,7 @@ import { documentSearchRoute, repositorySearchRoute, symbolSearchRoute } from '. import { setupRoute } from './routes/setup'; import { workspaceRoute } from './routes/workspace'; import { IndexScheduler, UpdateScheduler } from './scheduler'; -import { enableSecurity } from './security'; +import { CodeServerRouter } from './security'; import { ServerOptions } from './server_options'; import { ServerLoggerFactory } from './utils/server_logger_factory'; @@ -77,12 +79,41 @@ async function getCodeNodeUuid(url: string, log: Logger) { export function init(server: Server, options: any) { const log = new Logger(server); const serverOptions = new ServerOptions(options, server.config()); + const xpackMainPlugin: XPackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: 'code', + name: i18n.translate('xpack.code.featureRegistry.codeFeatureName', { + defaultMessage: 'Code', + }), + icon: 'codeApp', + navLinkId: 'code', + app: ['code', 'kibana'], + catalogue: [], // TODO add catalogue here + privileges: { + all: { + api: ['code_user', 'code_admin'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show', 'user', 'admin'], + }, + read: { + api: ['code_user'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show', 'user'], + }, + }, + }); + // @ts-ignore const kbnServer = this.kbnServer; kbnServer.ready().then(async () => { const serverUuid = await retryUntilAvailable(() => getServerUuid(server), 50); // enable security check in routes - enableSecurity(server); const codeNodeUrl = serverOptions.codeNodeUrl; if (codeNodeUrl) { const codeNodeUuid = (await retryUntilAvailable( @@ -200,9 +231,11 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L } // check code node repos on disk await checkRepos(cloneWorker, esClient, serverOptions, log); + + const codeServerRouter = new CodeServerRouter(server); // Add server routes and initialize the plugin here repositoryRoute( - server, + codeServerRouter, cloneWorker, deleteWorker, indexWorker, @@ -210,13 +243,13 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L repoConfigController, serverOptions ); - repositorySearchRoute(server, log); - documentSearchRoute(server, log); - symbolSearchRoute(server, log); - fileRoute(server, serverOptions); - workspaceRoute(server, serverOptions); - symbolByQnameRoute(server, log); - installRoute(server, lspService, installManager); - lspRoute(server, lspService, serverOptions); - setupRoute(server); + repositorySearchRoute(codeServerRouter, log); + documentSearchRoute(codeServerRouter, log); + symbolSearchRoute(codeServerRouter, log); + fileRoute(codeServerRouter, serverOptions); + workspaceRoute(codeServerRouter, serverOptions); + symbolByQnameRoute(codeServerRouter, log); + installRoute(codeServerRouter, lspService, installManager); + lspRoute(codeServerRouter, lspService, serverOptions); + setupRoute(codeServerRouter); } diff --git a/x-pack/plugins/code/server/routes/file.ts b/x-pack/plugins/code/server/routes/file.ts index c5209cb296d86..da915ec577ac0 100644 --- a/x-pack/plugins/code/server/routes/file.ts +++ b/x-pack/plugins/code/server/routes/file.ts @@ -19,11 +19,12 @@ import { import { ServerOptions } from '../server_options'; import { extractLines } from '../utils/buffer'; import { detectLanguage } from '../utils/detect_language'; +import { CodeServerRouter } from '../security'; const TEXT_FILE_LIMIT = 1024 * 1024; // 1mb -export function fileRoute(server: hapi.Server, options: ServerOptions) { - server.securedRoute({ +export function fileRoute(server: CodeServerRouter, options: ServerOptions) { + server.route({ path: '/api/code/repo/{uri*3}/tree/{ref}/{path*}', method: 'GET', async handler(req: hapi.Request) { @@ -58,7 +59,7 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) { }, }); - server.securedRoute({ + server.route({ path: '/api/code/repo/{uri*3}/blob/{ref}/{path*}', method: 'GET', async handler(req: hapi.Request, h: hapi.ResponseToolkit) { @@ -108,7 +109,7 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) { }, }); - server.securedRoute({ + server.route({ path: '/app/code/repo/{uri*3}/raw/{ref}/{path*}', method: 'GET', async handler(req, h: hapi.ResponseToolkit) { @@ -131,16 +132,18 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) { }, }); - server.securedRoute({ + server.route({ path: '/api/code/repo/{uri*3}/history/{ref}', method: 'GET', handler: historyHandler, }); + server.route({ path: '/api/code/repo/{uri*3}/history/{ref}/{path*}', method: 'GET', handler: historyHandler, }); + async function historyHandler(req: hapi.Request) { const gitOperations = new GitOperations(options.repoPath); const { uri, ref, path } = req.params; @@ -176,7 +179,7 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) { } } } - server.securedRoute({ + server.route({ path: '/api/code/repo/{uri*3}/references', method: 'GET', async handler(req, reply) { @@ -197,7 +200,7 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) { }, }); - server.securedRoute({ + server.route({ path: '/api/code/repo/{uri*3}/diff/{revision}', method: 'GET', async handler(req) { @@ -216,7 +219,7 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) { }, }); - server.securedRoute({ + server.route({ path: '/api/code/repo/{uri*3}/blame/{revision}/{path*}', method: 'GET', async handler(req) { diff --git a/x-pack/plugins/code/server/routes/install.ts b/x-pack/plugins/code/server/routes/install.ts index 21a7761fb9780..4f7ec68d21a71 100644 --- a/x-pack/plugins/code/server/routes/install.ts +++ b/x-pack/plugins/code/server/routes/install.ts @@ -5,18 +5,19 @@ */ import * as Boom from 'boom'; -import { Request, Server } from 'hapi'; +import { Request } from 'hapi'; import { InstallationType } from '../../common/installation'; import { InstallManager } from '../lsp/install_manager'; import { LanguageServerDefinition, LanguageServers } from '../lsp/language_servers'; import { LspService } from '../lsp/lsp_service'; +import { CodeServerRouter } from '../security'; export function installRoute( - server: Server, + server: CodeServerRouter, lspService: LspService, installManager: InstallManager ) { - const kibanaVersion = server.config().get('pkg.version') as string; + const kibanaVersion = server.server.config().get('pkg.version') as string; const status = (def: LanguageServerDefinition) => ({ name: def.name, status: lspService.languageServerStatus(def.name), @@ -29,7 +30,7 @@ export function installRoute( pluginName: def.pluginName, }); - server.securedRoute({ + server.route({ path: '/api/code/install', handler() { return LanguageServers.map(status); @@ -51,7 +52,7 @@ export function installRoute( method: 'GET', }); - server.securedRoute({ + server.route({ path: '/api/code/install/{name}', requireAdmin: true, async handler(req: Request) { diff --git a/x-pack/plugins/code/server/routes/lsp.ts b/x-pack/plugins/code/server/routes/lsp.ts index e46e450d45658..ae272d8bdf649 100644 --- a/x-pack/plugins/code/server/routes/lsp.ts +++ b/x-pack/plugins/code/server/routes/lsp.ts @@ -26,16 +26,17 @@ import { import { detectLanguage } from '../utils/detect_language'; import { EsClientWithRequest } from '../utils/esclient_with_request'; import { promiseTimeout } from '../utils/timeout'; +import { CodeServerRouter } from '../security'; const LANG_SERVER_ERROR = 'language server error'; export function lspRoute( - server: hapi.Server, + server: CodeServerRouter, lspService: LspService, serverOptions: ServerOptions ) { - const log = new Logger(server); - server.securedRoute({ + const log = new Logger(server.server); + server.route({ path: '/api/code/lsp/textDocument/{method}', async handler(req, h: hapi.ResponseToolkit) { if (typeof req.payload === 'object' && req.payload != null) { @@ -77,7 +78,7 @@ export function lspRoute( method: 'POST', }); - server.securedRoute({ + server.route({ path: '/api/code/lsp/findReferences', method: 'POST', async handler(req, h: hapi.ResponseToolkit) { @@ -169,8 +170,8 @@ export function lspRoute( }); } -export function symbolByQnameRoute(server: hapi.Server, log: Logger) { - server.securedRoute({ +export function symbolByQnameRoute(server: CodeServerRouter, log: Logger) { + server.route({ path: '/api/code/lsp/symbol/{qname}', method: 'GET', async handler(req) { diff --git a/x-pack/plugins/code/server/routes/redirect.ts b/x-pack/plugins/code/server/routes/redirect.ts index 4743a2bd4eeac..17084a98f738e 100644 --- a/x-pack/plugins/code/server/routes/redirect.ts +++ b/x-pack/plugins/code/server/routes/redirect.ts @@ -24,11 +24,13 @@ export function redirectRoute(server: hapi.Server, redirectUrl: string, log: Log }, }, }; + server.route({ path: '/api/code/{p*}', method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], handler: proxyHandler, }); + server.route({ path: '/api/code/lsp/{p*}', method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], diff --git a/x-pack/plugins/code/server/routes/repository.ts b/x-pack/plugins/code/server/routes/repository.ts index 92aec426bc799..7061775b269e7 100644 --- a/x-pack/plugins/code/server/routes/repository.ts +++ b/x-pack/plugins/code/server/routes/repository.ts @@ -6,7 +6,6 @@ import Boom from 'boom'; -import { Server } from 'hapi'; import { validateGitUrl } from '../../common/git_url_utils'; import { RepositoryUtils } from '../../common/repository_utils'; import { RepositoryConfig, RepositoryUri } from '../../model'; @@ -17,9 +16,10 @@ import { RepositoryConfigController } from '../repository_config_controller'; import { RepositoryObjectClient } from '../search'; import { ServerOptions } from '../server_options'; import { EsClientWithRequest } from '../utils/esclient_with_request'; +import { CodeServerRouter } from '../security'; export function repositoryRoute( - server: Server, + server: CodeServerRouter, cloneWorker: CloneWorker, deleteWorker: DeleteWorker, indexWorker: IndexWorker, @@ -28,7 +28,7 @@ export function repositoryRoute( options: ServerOptions ) { // Clone a git repository - server.securedRoute({ + server.route({ path: '/api/code/repo', requireAdmin: true, method: 'POST', @@ -91,7 +91,7 @@ export function repositoryRoute( }); // Remove a git repository - server.securedRoute({ + server.route({ path: '/api/code/repo/{uri*3}', requireAdmin: true, method: 'DELETE', @@ -131,7 +131,7 @@ export function repositoryRoute( }); // Get a git repository - server.securedRoute({ + server.route({ path: '/api/code/repo/{uri*3}', method: 'GET', async handler(req) { @@ -149,7 +149,7 @@ export function repositoryRoute( }, }); - server.securedRoute({ + server.route({ path: '/api/code/repo/status/{uri*3}', method: 'GET', async handler(req) { @@ -192,7 +192,7 @@ export function repositoryRoute( }); // Get all git repositories - server.securedRoute({ + server.route({ path: '/api/code/repos', method: 'GET', async handler(req) { @@ -212,7 +212,7 @@ export function repositoryRoute( // Issue a repository index task. // TODO(mengwei): This is just temporary API stub to trigger the index job. Eventually in the near // future, this route will be removed. The scheduling strategy is still in discussion. - server.securedRoute({ + server.route({ path: '/api/code/repo/index/{uri*3}', method: 'POST', requireAdmin: true, @@ -239,7 +239,7 @@ export function repositoryRoute( }); // Update a repo config - server.securedRoute({ + server.route({ path: '/api/code/repo/config/{uri*3}', method: 'PUT', requireAdmin: true, @@ -271,7 +271,7 @@ export function repositoryRoute( }); // Get repository config - server.securedRoute({ + server.route({ path: '/api/code/repo/config/{uri*3}', method: 'GET', async handler(req) { diff --git a/x-pack/plugins/code/server/routes/search.ts b/x-pack/plugins/code/server/routes/search.ts index 8518e2de11dc3..8b759f502ddf0 100644 --- a/x-pack/plugins/code/server/routes/search.ts +++ b/x-pack/plugins/code/server/routes/search.ts @@ -11,9 +11,10 @@ import { DocumentSearchRequest, RepositorySearchRequest, SymbolSearchRequest } f import { Logger } from '../log'; import { DocumentSearchClient, RepositorySearchClient, SymbolSearchClient } from '../search'; import { EsClientWithRequest } from '../utils/esclient_with_request'; +import { CodeServerRouter } from '../security'; -export function repositorySearchRoute(server: hapi.Server, log: Logger) { - server.securedRoute({ +export function repositorySearchRoute(server: CodeServerRouter, log: Logger) { + server.route({ path: '/api/code/search/repo', method: 'GET', async handler(req) { @@ -43,7 +44,7 @@ export function repositorySearchRoute(server: hapi.Server, log: Logger) { }, }); - server.securedRoute({ + server.route({ path: '/api/code/suggestions/repo', method: 'GET', async handler(req) { @@ -74,8 +75,8 @@ export function repositorySearchRoute(server: hapi.Server, log: Logger) { }); } -export function documentSearchRoute(server: hapi.Server, log: Logger) { - server.securedRoute({ +export function documentSearchRoute(server: CodeServerRouter, log: Logger) { + server.route({ path: '/api/code/search/doc', method: 'GET', async handler(req) { @@ -107,7 +108,7 @@ export function documentSearchRoute(server: hapi.Server, log: Logger) { }, }); - server.securedRoute({ + server.route({ path: '/api/code/suggestions/doc', method: 'GET', async handler(req) { @@ -138,7 +139,7 @@ export function documentSearchRoute(server: hapi.Server, log: Logger) { }); } -export function symbolSearchRoute(server: hapi.Server, log: Logger) { +export function symbolSearchRoute(server: CodeServerRouter, log: Logger) { const symbolSearchHandler = async (req: hapi.Request) => { let page = 1; const { p, q, repoScope } = req.query as hapi.RequestQuery; @@ -166,12 +167,12 @@ export function symbolSearchRoute(server: hapi.Server, log: Logger) { }; // Currently these 2 are the same. - server.securedRoute({ + server.route({ path: '/api/code/suggestions/symbol', method: 'GET', handler: symbolSearchHandler, }); - server.securedRoute({ + server.route({ path: '/api/code/search/symbol', method: 'GET', handler: symbolSearchHandler, diff --git a/x-pack/plugins/code/server/routes/setup.ts b/x-pack/plugins/code/server/routes/setup.ts index b9a67f0b7850f..0c75b8ec1d46a 100644 --- a/x-pack/plugins/code/server/routes/setup.ts +++ b/x-pack/plugins/code/server/routes/setup.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseToolkit, Server } from 'hapi'; +import { ResponseToolkit } from 'hapi'; +import { CodeServerRouter } from '../security'; -export function setupRoute(server: Server) { +export function setupRoute(server: CodeServerRouter) { server.route({ method: 'get', path: '/api/code/setup', diff --git a/x-pack/plugins/code/server/routes/workspace.ts b/x-pack/plugins/code/server/routes/workspace.ts index 6826519920cf7..3988d1fc8947e 100644 --- a/x-pack/plugins/code/server/routes/workspace.ts +++ b/x-pack/plugins/code/server/routes/workspace.ts @@ -13,9 +13,10 @@ import { WorkspaceHandler } from '../lsp/workspace_handler'; import { ServerOptions } from '../server_options'; import { EsClientWithRequest } from '../utils/esclient_with_request'; import { ServerLoggerFactory } from '../utils/server_logger_factory'; +import { CodeServerRouter } from '../security'; -export function workspaceRoute(server: hapi.Server, serverOptions: ServerOptions) { - server.securedRoute({ +export function workspaceRoute(server: CodeServerRouter, serverOptions: ServerOptions) { + server.route({ path: '/api/code/workspace', method: 'GET', async handler() { @@ -23,7 +24,7 @@ export function workspaceRoute(server: hapi.Server, serverOptions: ServerOptions }, }); - server.securedRoute({ + server.route({ path: '/api/code/workspace/{uri*3}/{revision}', requireAdmin: true, method: 'POST', @@ -33,12 +34,12 @@ export function workspaceRoute(server: hapi.Server, serverOptions: ServerOptions const repoConfig = serverOptions.repoConfigs[repoUri]; const force = !!(req.query as RequestQuery).force; if (repoConfig) { - const log = new Logger(server, ['workspace', repoUri]); + const log = new Logger(server.server, ['workspace', repoUri]); const workspaceHandler = new WorkspaceHandler( serverOptions.repoPath, serverOptions.workspacePath, new EsClientWithRequest(req), - new ServerLoggerFactory(server) + new ServerLoggerFactory(server.server) ); try { const { workspaceRepo, workspaceRevision } = await workspaceHandler.openWorkspace( diff --git a/x-pack/plugins/code/server/security.test.ts b/x-pack/plugins/code/server/security.test.ts deleted file mode 100644 index d099177bffce6..0000000000000 --- a/x-pack/plugins/code/server/security.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Server } from 'hapi'; -// @ts-ignore -import HapiAuthCookie from 'hapi-auth-cookie'; -import sinon from 'sinon'; -import { SecureRoute } from './security'; - -const createMockServer = async () => { - const server = new Server(); - server.register(HapiAuthCookie); - server.auth.strategy('session', 'cookie', { - password: 'secret', - validateFunc: async (request: any, username: any, password: any) => { - const isValid = username === 'foo' && password === 'bar'; - const credentials = { - roles: ['superuser'], - }; - return { isValid, credentials }; - }, - }); - - const secureRoute = new SecureRoute(server); - secureRoute.install(); - // @ts-ignore - const stub = sinon.stub(secureRoute, 'isSecurityEnabledInEs'); - stub.returns(true); - server.securedRoute({ - method: 'GET', - path: '/test', - options: { - auth: 'session', - }, - requireAdmin: true, - handler() { - return 'ok'; - }, - }); - return server; -}; - -it('should response 401 when not logged in', async () => { - const server = await createMockServer(); - - const response = await server.inject({ - method: 'GET', - url: '/test', - }); - expect(response.statusCode).toBe(401); -}); - -async function checkWithRoles(server: any, roles: any) { - const response = await server.inject({ - method: 'GET', - url: '/test', - credentials: { - username: 'foo', - password: 'bar', - roles, - }, - }); - return response; -} - -it('should response 403 when roles check failed', async () => { - const server = await createMockServer(); - const response = await checkWithRoles(server, []); - expect(response.statusCode).toBe(403); -}); - -it('should response 200 when user is superuser', async () => { - const server = await createMockServer(); - const response = await checkWithRoles(server, ['superuser']); - expect(response.statusCode).toBe(200); -}); - -it('should response 200 when user is code admin', async () => { - const server = await createMockServer(); - const response = await checkWithRoles(server, ['code_admin']); - expect(response.statusCode).toBe(200); -}); diff --git a/x-pack/plugins/code/server/security.ts b/x-pack/plugins/code/server/security.ts index 2640e42d32b19..50368117904ab 100644 --- a/x-pack/plugins/code/server/security.ts +++ b/x-pack/plugins/code/server/security.ts @@ -4,81 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { Lifecycle, Request, ResponseToolkit, Server, ServerRoute } from 'hapi'; +import { Server, ServerRoute, RouteOptions } from 'hapi'; -export interface SecuredRoute extends ServerRoute { - requireRoles?: string[]; - requireAdmin?: boolean; -} - -export const ADMIN_ROLE = 'code_admin'; - -declare module 'hapi' { - interface Server { - securedRoute(route: SecuredRoute): void; - } -} - -export class SecureRoute { +export class CodeServerRouter { constructor(readonly server: Server) {} - public install() { - const self = this; - function securedRoute(route: SecuredRoute) { - if (route.handler) { - const originHandler = route.handler as Lifecycle.Method; - route.handler = async (req: Request, h: ResponseToolkit, err?: Error) => { - if (self.isSecurityEnabledInEs()) { - let requiredRoles = route.requireRoles || []; - if (route.requireAdmin) { - requiredRoles = requiredRoles.concat([ADMIN_ROLE]); - } - if (requiredRoles.length > 0) { - if (!req.auth.isAuthenticated) { - throw Boom.unauthorized('not login.'); - } else { - // @ts-ignore - const userRoles = new Set(req.auth.credentials.roles || []); - const authorized = - userRoles.has('superuser') || - requiredRoles.every((value: string) => userRoles.has(value)); - if (!authorized) { - throw Boom.forbidden('not authorized user.'); - } - } - } - } - return await originHandler(req, h, err); - }; - } - self.server.route({ - handler: route.handler, - method: route.method, - options: route.options, - path: route.path, - }); - } - - this.server.securedRoute = securedRoute; - } - - private isSecurityEnabledInEs() { - const xpackInfo = this.server.plugins.xpack_main.info; - if (!xpackInfo.isAvailable()) { - throw Boom.serverUnavailable('x-pack info is not available yet.'); - } - if ( - !xpackInfo.feature('security').isEnabled() || - !xpackInfo.feature('security').isAvailable() - ) { - return false; - } - return true; + route(route: CodeRoute) { + const routeOptions: RouteOptions = (route.options || {}) as RouteOptions; + routeOptions.tags = [ + ...(routeOptions.tags || []), + `access:code_${route.requireAdmin ? 'admin' : 'user'}`, + ]; + + this.server.route({ + handler: route.handler, + method: route.method, + options: routeOptions, + path: route.path, + }); } } -export function enableSecurity(server: Server) { - const secureRoute = new SecureRoute(server); - secureRoute.install(); +export interface CodeRoute extends ServerRoute { + requireAdmin?: boolean; } diff --git a/x-pack/plugins/code/server/utils/es_index_client.ts b/x-pack/plugins/code/server/utils/es_index_client.ts index cf38d5a006189..6a4c76d0056cc 100644 --- a/x-pack/plugins/code/server/utils/es_index_client.ts +++ b/x-pack/plugins/code/server/utils/es_index_client.ts @@ -11,42 +11,42 @@ export class EsIndexClient { constructor(readonly self: WithRequest) {} public exists(params: AnyObject): Promise { - return this.self.callWithRequest('indices.exists', params); + return this.self.callCluster('indices.exists', params); } public create(params: AnyObject): Promise { - return this.self.callWithRequest('indices.create', params); + return this.self.callCluster('indices.create', params); } public refresh(params: AnyObject): Promise { - return this.self.callWithRequest('indices.refresh', params); + return this.self.callCluster('indices.refresh', params); } public delete(params: AnyObject): Promise { - return this.self.callWithRequest('indices.delete', params); + return this.self.callCluster('indices.delete', params); } public existsAlias(params: AnyObject): Promise { - return this.self.callWithRequest('indices.existsAlias', params); + return this.self.callCluster('indices.existsAlias', params); } public getAlias(params: AnyObject): Promise { - return this.self.callWithRequest('indices.getAlias', params); + return this.self.callCluster('indices.getAlias', params); } public putAlias(params: AnyObject): Promise { - return this.self.callWithRequest('indices.putAlias', params); + return this.self.callCluster('indices.putAlias', params); } public deleteAlias(params: AnyObject): Promise { - return this.self.callWithRequest('indices.deleteAlias', params); + return this.self.callCluster('indices.deleteAlias', params); } public updateAliases(params: AnyObject): Promise { - return this.self.callWithRequest('indices.updateAliases', params); + return this.self.callCluster('indices.updateAliases', params); } public getMapping(params: AnyObject): Promise { - return this.self.callWithRequest('indices.getMapping', params); + return this.self.callCluster('indices.getMapping', params); } } diff --git a/x-pack/plugins/code/server/utils/esclient_with_request.ts b/x-pack/plugins/code/server/utils/esclient_with_request.ts index d3ef3dda2b376..cb825a6543fa7 100644 --- a/x-pack/plugins/code/server/utils/esclient_with_request.ts +++ b/x-pack/plugins/code/server/utils/esclient_with_request.ts @@ -17,38 +17,38 @@ export class EsClientWithRequest extends WithRequest implements EsClient { } public bulk(params: AnyObject): Promise { - return this.callWithRequest('bulk', params); + return this.callCluster('bulk', params); } public delete(params: AnyObject): Promise { - return this.callWithRequest('delete', params); + return this.callCluster('delete', params); } public deleteByQuery(params: AnyObject): Promise { - return this.callWithRequest('deleteByQuery', params); + return this.callCluster('deleteByQuery', params); } public get(params: AnyObject): Promise { - return this.callWithRequest('get', params); + return this.callCluster('get', params); } public index(params: AnyObject): Promise { - return this.callWithRequest('index', params); + return this.callCluster('index', params); } public ping(): Promise { - return this.callWithRequest('ping'); + return this.callCluster('ping'); } public reindex(params: AnyObject): Promise { - return this.callWithRequest('reindex', params); + return this.callCluster('reindex', params); } public search(params: AnyObject): Promise { - return this.callWithRequest('search', params); + return this.callCluster('search', params); } public update(params: AnyObject): Promise { - return this.callWithRequest('update', params); + return this.callCluster('update', params); } } diff --git a/x-pack/plugins/code/server/utils/with_request.ts b/x-pack/plugins/code/server/utils/with_request.ts index 6ef266d710b2c..fe049d044d4df 100644 --- a/x-pack/plugins/code/server/utils/with_request.ts +++ b/x-pack/plugins/code/server/utils/with_request.ts @@ -8,11 +8,20 @@ import { Request } from 'hapi'; import { AnyObject } from '../lib/esqueue'; export class WithRequest { - public readonly callWithRequest: (endpoint: string, clientOptions?: AnyObject) => Promise; + public readonly callCluster: (endpoint: string, clientOptions?: AnyObject) => Promise; constructor(readonly req: Request) { - this.callWithRequest = req.server.plugins.elasticsearch - .getCluster('data') - .callWithRequest.bind(null, req); + const cluster = req.server.plugins.elasticsearch.getCluster('data'); + + // @ts-ignore + const securityPlugin = req.server.plugins.security; + if (securityPlugin) { + const useRbac = securityPlugin.authorization.mode.useRbacForRequest(req); + if (useRbac) { + this.callCluster = cluster.callWithInternalUser; + return; + } + } + this.callCluster = cluster.callWithRequest.bind(null, req); } } diff --git a/x-pack/test/api_integration/apis/code/feature_controls.ts b/x-pack/test/api_integration/apis/code/feature_controls.ts new file mode 100644 index 0000000000000..ea5579a40a73c --- /dev/null +++ b/x-pack/test/api_integration/apis/code/feature_controls.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// import expect from '@kbn/expect'; +// import { SecurityService, SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) { + // const supertest = getService('supertestWithoutAuth'); + // const security: SecurityService = getService('security'); + // const log = getService('log'); + + // const expect404 = (result: any) => { + // expect(result.error).to.be(undefined); + // expect(result.response).not.to.be(undefined); + // expect(result.response).to.have.property('statusCode', 404); + // }; + + // const expect200 = (result: any) => { + // expect(result.error).to.be(undefined); + // expect(result.response).not.to.be(undefined); + // expect(result.response).to.have.property('statusCode', 200); + // }; + + // const endpoints = [ + // { + // url: `/api/code/---`, + // expectForbidden: expect404, + // expectResponse: expect200, + // }, + // ]; + + // async function executeRequest( + // endpoint: string, + // username: string, + // password: string, + // ) { + + // return await supertest + // .get(endpoint) + // .auth(username, password) + // .set('kbn-xsrf', 'foo') + // .then((response: any) => ({ error: undefined, response })) + // .catch((error: any) => ({ error, response: undefined })); + // } + + // async function executeRequests( + // username: string, + // password: string, + // expectation: 'forbidden' | 'response' + // ) { + // for (const endpoint of endpoints) { + // log.debug(`hitting ${endpoint}`); + // const result = await executeRequest(endpoint.url, username, password); + // if (expectation === 'forbidden') { + // endpoint.expectForbidden(result); + // } else { + // endpoint.expectResponse(result); + // } + // } + // } + + describe('feature controls', () => { + // TODO implement tests similar to APM + }); +} diff --git a/x-pack/test/api_integration/apis/code/index.ts b/x-pack/test/api_integration/apis/code/index.ts new file mode 100644 index 0000000000000..b42b5965b0f46 --- /dev/null +++ b/x-pack/test/api_integration/apis/code/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function apmApiIntegrationTests({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('Code', () => { + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 5c33215392a3f..210402904c084 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -21,5 +21,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./uptime')); loadTestFile(require.resolve('./maps')); loadTestFile(require.resolve('./apm')); + loadTestFile(require.resolve('./code')); }); } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 6da0748e607d5..65197b1916203 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -498,6 +498,39 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:apm/show`, ], }, + code: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:code_user`, + `api:${version}:code_admin`, + `app:${version}:code`, + `app:${version}:kibana`, + `ui:${version}:navLinks/code`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:code/show`, + `ui:${version}:code/user`, + `ui:${version}:code/admin`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:code_user`, + `app:${version}:code`, + `app:${version}:kibana`, + `ui:${version}:navLinks/code`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:code/show`, + `ui:${version}:code/user`, + ], + }, maps: { all: [ 'login:', @@ -853,6 +886,13 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:catalogue/apm`, `ui:${version}:navLinks/apm`, `ui:${version}:apm/show`, + `api:${version}:code_user`, + `api:${version}:code_admin`, + `app:${version}:code`, + `ui:${version}:navLinks/code`, + `ui:${version}:code/show`, + `ui:${version}:code/user`, + `ui:${version}:code/admin`, `app:${version}:maps`, `ui:${version}:catalogue/maps`, `ui:${version}:navLinks/maps`, @@ -973,6 +1013,11 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:catalogue/apm`, `ui:${version}:navLinks/apm`, `ui:${version}:apm/show`, + `api:${version}:code_user`, + `app:${version}:code`, + `ui:${version}:navLinks/code`, + `ui:${version}:code/show`, + `ui:${version}:code/user`, `app:${version}:maps`, `ui:${version}:catalogue/maps`, `ui:${version}:navLinks/maps`, @@ -1132,6 +1177,13 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:catalogue/apm`, `ui:${version}:navLinks/apm`, `ui:${version}:apm/show`, + `api:${version}:code_user`, + `api:${version}:code_admin`, + `app:${version}:code`, + `ui:${version}:navLinks/code`, + `ui:${version}:code/show`, + `ui:${version}:code/user`, + `ui:${version}:code/admin`, `app:${version}:maps`, `ui:${version}:catalogue/maps`, `ui:${version}:navLinks/maps`, @@ -1252,6 +1304,11 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:catalogue/apm`, `ui:${version}:navLinks/apm`, `ui:${version}:apm/show`, + `api:${version}:code_user`, + `app:${version}:code`, + `ui:${version}:navLinks/code`, + `ui:${version}:code/show`, + `ui:${version}:code/user`, `app:${version}:maps`, `ui:${version}:catalogue/maps`, `ui:${version}:navLinks/maps`, @@ -1329,6 +1386,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { logs: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], + code: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/xpack_main/features/features.ts b/x-pack/test/api_integration/apis/xpack_main/features/features.ts index 265ce5f475839..ddeff89513ad6 100644 --- a/x-pack/test/api_integration/apis/xpack_main/features/features.ts +++ b/x-pack/test/api_integration/apis/xpack_main/features/features.ts @@ -37,6 +37,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { 'ml', 'apm', 'canvas', + 'code', 'infrastructure', 'logs', 'maps', diff --git a/x-pack/test/functional/apps/code/with_security.ts b/x-pack/test/functional/apps/code/with_security.ts index 87651e51019e7..57d127fe01762 100644 --- a/x-pack/test/functional/apps/code/with_security.ts +++ b/x-pack/test/functional/apps/code/with_security.ts @@ -19,31 +19,50 @@ export default function testWithSecurity({ getService, getPageObjects }: TestInv const repositoryListSelector = 'codeRepositoryList codeRepositoryItem'; const manageButtonSelectors = ['indexRepositoryButton', 'deleteRepositoryButton']; const log = getService('log'); + const security = getService('security'); describe('with security enabled:', () => { before(async () => { await esArchiver.load('empty_kibana'); - await PageObjects.settings.navigateTo(); - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ - username: codeAdmin, + await security.role.create('global_code_all_role', { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + code: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(codeAdmin, { password: dummyPassword, - confirmPassword: dummyPassword, - fullname: 'Code Admin', - email: 'codeAdmin@elastic.co', - save: true, - roles: ['kibana_user', 'code_admin'], + roles: ['global_code_all_role'], + full_name: 'code admin', + }); + + await security.role.create('global_code_read_role', { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + code: ['read'], + }, + spaces: ['*'], + }, + ], }); - await PageObjects.security.addUser({ - username: codeUser, - password: '123321', - confirmPassword: dummyPassword, - fullname: 'Code User', - email: 'codeUser@elastic.co', - save: true, - roles: ['kibana_user', 'code_user'], + + await security.user.create(codeUser, { + password: dummyPassword, + roles: ['global_code_read_role'], + full_name: 'code user', }); - // Navigate to the search page of the code app. }); async function login(user: string) {