Unable to get the Username and Roles in Kibana 7.9 through the Legacy and New Platforms

We have to upgrade to ELK 7.9 and I've been unable to retrieve user details from a Kibana plugin on both the LP and NP. Ideally there's a way to get this working on the Legacy Platform which would provide time to perform the migration.

Legacy Platform

Up until Kibana 7.8 the username and roles could be fetched within HAPI middleware (or route handler) as follows. In version 7.9 the handler started failing because there's no longer a getUser method attached to the security plugin. It seems the only property remaining is server.plugins.security.status which doesn't help.

export default server => {
  server.ext('onPreHandler', async (req, h) => {
    try{
      if (server.plugins.security) {
        console.log('LEGACY: console.dir(server.plugins.security)')
        console.dir(server.plugins.security)
        //const userObj = await server.plugins.security.getUser(req);
        //req.app.user = { username: userObj.username, roles: userObj.roles };
      } else{
        throw new Error('The security plugin is unavailable.')
      }
    }
    catch(e) {
      req.app.user = { username: null, roles: [] };
    }
    return h.continue;
  });
};

Legacy request handler output:

LEGACY: console.dir(server.plugins.security)
{
status: Status {
_events: [Object: null prototype] {
change: [Function]
},
_eventsCount: 1,
_maxListeners: undefined,
id: 'plugin:security@7.9.0',
since: 2020 - 09 - 21 T03: 43: 21.237 Z,
state: 'green',
message: 'Ready',
plugin: Plugin {
kbnServer: [KbnServer],
spec: [ScopedPluginSpec],
pkg: [Object],
path: '/usr/share/kibana/x-pack',
id: 'security',
version: '7.9.0',
requiredIds: [Array],
externalPreInit: undefined,
externalInit: [Function: init],
externalPostInit: undefined,
enabled: true,
configPrefix: 'xpack.security',
publicDir: '/usr/share/kibana/x-pack/legacy/plugins/security/public',
preInit: [Function],
init: [Function],
postInit: [Function],
_server: [Object],
_options: [Object],
status: [Circular]
},
error: null
}
}

New Platform

Here are some things that I've explored within plugin.ts:

public setup(core: CoreSetup, plugins: PluginDeps) {
  const router = core.http.createRouter();
  console.log('SETUP: core.http.auth', Object.keys(core.http.auth))
  console.log('SETUP: core.http.auth.get()', core.http.auth.get())
  console.log('SETUP: plugins.security.authc.isAuthenticated()', plugins.security.authc.isAuthenticated())
  console.log('SETUP: console.dir(plugins.security.authc)')
  console.dir(plugins.security.authc)
  console.log('SETUP: plugins.security.authc.isAuthenticated()', plugins.security.authc.isAuthenticated())
  console.log('SETUP: plugins.security.authc.getCurrentUser()', plugins.security.authc.getCurrentUser())
  defineRoutes(router);
  this.logger.debug('Plugin Initialized');
  return {};
}

public start(core: CoreStart, plugins: PluginDeps) {
  console.log('START: core.http.auth', Object.keys(core.http.auth))
  console.log('START: core.http.auth.get()', core.http.auth.get())
  console.log('START: console.dir(plugins.security)')
  console.dir(plugins.security)
  this.logger.debug('Plugin Started');
  return {};
}

plugin.ts console output:

SETUP: core.http.auth [ 'get', 'isAuthenticated' ]
SETUP: core.http.auth.get() { status: 'unauthenticated', state: undefined }
SETUP: plugins.security.authc.isAuthenticated() false
SETUP: console.dir(plugins.security.authc)
{ isAuthenticated: [Function: isAuthenticated],
getCurrentUser: [Function: getCurrentUser],
areAPIKeysEnabled: [Function: areAPIKeysEnabled],
createAPIKey: [Function: createAPIKey],
invalidateAPIKey: [Function: invalidateAPIKey],
grantAPIKeyAsInternalUser: [Function: grantAPIKeyAsInternalUser],
invalidateAPIKeyAsInternalUser: [Function: invalidateAPIKeyAsInternalUser] }
SETUP: plugins.security.authc.isAuthenticated() false
SETUP: plugins.security.authc.getCurrentUser() null
...
START: core.http.auth [ 'get', 'isAuthenticated' ]
START: core.http.auth.get() { status: 'unauthenticated', state: undefined }
START: console.dir(plugins.security)

There's no .get() method exposed to the route handler, just .isAuthenticated. That flag is working properly, but it doesn't help me get the username or the roles.

export function defineRoutes(router: IRouter) {
  router.get(
    {
      path: '/api/my_plugin_name/example',
      options: { authRequired: 'required' },
      validate: false,
    },
    async (context, request, response) => {
      console.log('ROUTE HANDLER: Object.keys(request.auth)', Object.keys(request.auth))
      console.log('ROUTE HANDLER: request.auth.isAuthenticated', request.auth.isAuthenticated)
      return response.ok({
        body: {
          time:  new Date().toISOString(),
        },
      });
    }
  );
}

route handler console output:

ROUTE HANDLER: Object.keys(request.auth) [ 'isAuthenticated' ]
ROUTE HANDLER: request.auth.isAuthenticated true

Hey @BitBite,

You're very close! If you pass the security plugin's setup contract through to your route handler, then you can access the current user. Something like the following should work:

interface RouteDeps {
  security: SecurityPluginSetup
}

export function defineRoutes(router: IRouter, { security }: RouteDeps) {
  router.get(
    {
      path: '/api/my_plugin_name/example',
      options: { authRequired: 'required' },
      validate: false,
    },
    async (context, request, response) => {
      const currentUser = security.authc.getCurrentUser(request);

      return response.ok({
        body: {
          time:  new Date().toISOString(),
        },
      });
    }
  );
}
1 Like

Wow, that did the trick, thanks!!! I couldn't have done this without your help. I'm not sure how useful the following feedback is, but adding an example in the plugin boilerplate could go a long way to help others from getting stuck.

  • I only discovered that plugin.ts -> public setup(core: CoreSetup, plugins: PluginsSetup) had a second argument available from inspecting Kibana source code. There wasn't any documentation that I could find.

  • TypeScript wasn't giving any hints. I still can't find an appropriate Type from ../src/core/server so I'll have to define my own interface (now that I know what to expect).

  • I was able to call plugins.security.authc.getCurrentUser() without giving a request handler. Instead of receiving an error message I got a response that seemed like it was working-ish { status: 'unauthenticated', state: undefined }

  • It doesn't make sense imho that I should be able to get the request.auth.isAuthenticated flag directly from the route handler object, but not the user details. I think it's because you wanted to expose a minimal "auth footprint" to the request handler (and call the xpack plugin for anything extra).

Any chance of getting a fix for the Legacy Platform? It's supposed to be supported until 7.10 according to the docs.

I'm glad that worked for you, and I really appreciate the feedback.

This is a valid criticism. We have an open issue to track this already, with hopes of providing something usable for third-party developers alongside the 7.10 release: https://github.com/elastic/kibana/issues/75786

This one is tricky, and hopefully it'll be addressed with the issue I linked above. I believe that all plugin development is expected to happen within the /plugins directory of your Kibana repository. From there, your plugin can use a relative-path style import to access the appropriate interfaces.

So for your examples above, the imports may look something like the following:

// Example plugin file located at: /path/to/kibana/plugins/my-plugin/server/plugin.ts
import { CoreSetup } from '../../../src/core/server';
import { SecurityPluginSetup } from '../../../x-pack/plugins/security/server';

If the imports I provided above work for you, then TypeScript should have all the information it needs to give you auto-completion, hints, etc.

We could do a better job of validating the input here. If those TypeScript imports work correctly, then you should get a compile-time warning about a missing parameter here.

Yeah the separation here is a bit arbitrary, but you're right: we wanted the security plugin to be responsible for providing any and all details about the current user.

We won't be returning the legacy API back into the legacy platform at this point, as the security plugin has been completely removed from the legacy world. The legacy platform itself is supported until 7.10, so third-party developers are free to build their own plugins there until this time. However, the plugins that ship with Kibana haven't ever offered a "stable" API that other developers can rely on. This is something that will eventually change now that we've migrated to the new platform, but I can't say when that will happen.

As a workaround, you should be able to access the new platform security plugin in the legacy world. Something like the following should get you started:

// inside your legacy plugin's main file:

new kibana.Plugin({
   id: 'my-plugin',
   configPrefix: 'myPlugin',
   uiExports: { /* ... */ },
   // etc,
  init(server) {
     const securityPlugin = server.newPlatform.setup.plugins.security as SecurityPluginSetup;
     if (!securityPlugin) {
       throw new Error('Kibana Platform Security plugin is not available.');
     }
     
     server.route({
        method: 'GET',
        path: `/api/my-plugin`,
        handler: async (request) => {
           const currentUser = securityPlugin.authc.getCurrentUser(request);
           // do something with currentUser
        }
    });
  }
})

Let me know if this workaround helps any.

Thanks for sticking with us while we stabilize the new platform during this transition period. We really do appreciate your feedback -- our goal is to make developing on top of Kibana much easier than it has been in the past, but it's a long process to migrate something as large as Kibana.

@Larry_Gregory , I can't thank you enough. The detail you put into the last reply was fantastic. I'm able to get user details on ELK 7.9.1 by tapping into the NP from the LP. That buys me more time for the migration!

1 Like

Fantastic, I'm happy that'll work for you in the near term.

It's not often that I see folks building plugins which take advantage of our security plugin. If you're able to share, I'd be interested in learning more about what your plugin does with security, and if you have any suggestions for enhancements to what the security plugin provides.

I usually use this internal API to get current user attributes

 const getCurrentUser = async () =>
      (await http.get('/internal/security/me', { asSystemRequest: true })) as AuthenticatedUser;

@ylasri that will absolutely work for client-side plugins, but I think Brian was interested in getting user information server-side.

For what it's worth, the client-side security plugin exposes a function to retrieve the current user, so that you don't have to rely on internal API calls which are more likely to change:

// from client side plugin, located at /path/to/kibana/plugins/myplugin/public/plugin.tsx

import { CoreSetup } from '../../../src/core/public';
import { SecurityPluginSetup } from '../../../x-pack/plugins/security/public';

interface PluginSetupDeps {
   security: SecurityPluginSetup;
}

export class MyPlugin {

   public setup(core: CoreSetup, { security }: PluginSetupDeps) {
      const { getCurrentUser } = security.authc;

       // not using `await` because the `setup` plugin lifecycle method should not be async.
      getCurrentUser().then(user => { /* ... */ });
   }
}


1 Like

Sorry I missed this in my earlier response. I agree, and opened https://github.com/elastic/kibana/issues/78276 to track this.

1 Like

This example is great for the sake of completeness on the thread. For our case, retrieving user details with front-end code isn't something that can be trusted because we require coordination with external systems.