Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Record security feature usage #67526

Merged
merged 20 commits into from
Jun 4, 2020

Conversation

legrego
Copy link
Member

@legrego legrego commented May 27, 2020

Summary

Updates the security plugin to notify the featureUsage service when its paid features are used:

  • Sub-feature privileges
  • Pre-access agreement

Resolves #64847
Resolves #67527

@legrego
Copy link
Member Author

legrego commented May 28, 2020

@elasticmachine merge upstream

featureUsage.register('security_pre_access_agreement');
}

public start({ featureUsage }: StartDeps): SecurityFeatureUsageServiceStart {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This SecurityFeaureUsageService feels rather unnecessary given the amount of work it's doing. It's really just a very thin facade over the featureUsage service exposed by the licensing plugin.

I opted to do this anyway because:

  1. We've previously tried to isolate the security plugin from the licensing plugin by introducing the SecurityLicense service, so it felt appropriate to continue this initiative.
  2. We expect the featureUsage service to change in the near future, but we don't yet know what that will look like. Having this encapsulated here will hopefully prevent sprawling changes to the security plugin when this happens.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sounds reasonable to me 👍

@legrego
Copy link
Member Author

legrego commented May 28, 2020

@elasticmachine merge upstream

@legrego
Copy link
Member Author

legrego commented May 28, 2020

@elasticmachine merge upstream

@legrego
Copy link
Member Author

legrego commented May 28, 2020

Latest failure was due to OOM?

11:43:43 Cannot contact kibana-ci-immutable-ubuntu-16-1590677899491886197: java.lang.InterruptedException
11:48:19 OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x0000000789380000, 37224448, 0) failed; error='Cannot allocate memory' (errno=12)
11:48:19 #
11:48:19 # There is insufficient memory for the Java Runtime Environment to continue.
11:48:19 # Native memory allocation (mmap) failed to map 37224448 bytes for committing reserved memory.
11:48:19 # An error report file with more information is saved as:
11:48:19 # /var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/hs_err_pid4148.log

@legrego
Copy link
Member Author

legrego commented May 28, 2020

@elasticmachine merge upstream

@legrego legrego added release_note:skip Skip the PR/issue when compiling release notes Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v7.9.0 v8.0.0 labels May 28, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-security (Team:Security)

@legrego legrego marked this pull request as ready for review May 29, 2020 13:48
@legrego legrego requested review from a team as code owners May 29, 2020 13:48
@legrego
Copy link
Member Author

legrego commented Jun 1, 2020

@elasticmachine merge upstream

@kobelb
Copy link
Contributor

kobelb commented Jun 1, 2020

#67712 just merged 😬

@pgayvallet
Copy link
Contributor

Ack: will review tomorrow.

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM once the FTR test is modified. Other comments are NITs

@@ -8,6 +8,8 @@ import { IClusterClient } from 'src/core/server';
import { ILicense, LicenseStatus, LicenseType } from '../common/types';
import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services';

export { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: i feel like it would be slightly better to export them directly from the index, as there are no other re-export in types

Comment on lines 35 to 37
recordSubFeaturePrivilegeUsage(kibanaPrivileges: Array<Optional<RoleKibanaPrivilege>> = []) {
featureUsage.notifyUsage('Subfeature privileges');
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unused parameter is here to anticipate the changes on the featureUsage service?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good catch, this was a holdover from an earlier implementation I was experimenting with. I'm surprised the linter didn't flag this as unused

Comment on lines +44 to +46
mockDependencies = ({
licensing: { license$: of({}), featureUsage: { register: jest.fn() } },
} as unknown) as PluginSetupDependencies;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the licensing plugin's mocks in x-pack/plugins/licensing/server/mocks.ts instead. That would also avoid the unknown cast.

Comment on lines 251 to 257
if (this.featureUsageService) {
this.featureUsageService = undefined;
}

if (this.featureUsageServiceStart) {
this.featureUsageServiceStart = undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you are also doing it with your other services (but the other service also have a call to a destroy-ish method), but are these undefined attributions really useful in any way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I suppose they aren't. I can at least get rid of the logic for featureUsageService, but I'll probably keep the handling for featureUsageServiceStart for the time being

Comment on lines 47 to 51
{
last_used: null,
license_level: 'gold',
name: 'Subfeature privileges',
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about this one, but to avoid all other futur consumers of the service to have to do the same thing, could you adapt the test to only assert existence / date of the 3 Test feature X instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I absolutely can! I wasn't sure about the intent of this test, but your suggestion makes sense to me based on the test description.

@legrego
Copy link
Member Author

legrego commented Jun 3, 2020

@elastic/kibana-security this is ready for review

@azasypkin
Copy link
Member

ACK: will review today or early morning tomorrow.

@azasypkin azasypkin self-requested a review June 4, 2020 09:52
Copy link
Member

@azasypkin azasypkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for doing that! Tested locally and everything worked as expected.

featureUsage.register('security_pre_access_agreement');
}

public start({ featureUsage }: StartDeps): SecurityFeatureUsageServiceStart {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sounds reasonable to me 👍

@@ -118,11 +138,14 @@ export class Plugin {
license$: licensing.license$,
});

this.featureUsageService.setup({ featureUsage: licensing.featureUsage });

const audit = this.auditService.setup({ license, config: config.audit });
const auditLogger = new SecurityAuditLogger(audit.getLogger());

const authc = await setupAuthentication({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: yeah, the time has come 🙂 We need a proper AuthenticationService with setup and start methods. Added to my To Do list, doing similar things with AuthorizationService in #65472

if (!this.featureUsageServiceStart) {
throw new Error(`featureUsageServiceStart is not registered!`);
}
return this.featureUsageServiceStart!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need ! here? Theoretically the check above should tell TS that it's defined.

@@ -160,6 +183,11 @@ export class Plugin {
authc,
authz,
license,
getFeatures: () =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I guess we can simply define core argument as core: CoreSetup<PluginStartDependencies> and then we can get rid of as Promise<[CoreStart, PluginStartDependencies, unknown]> completely

P.S. we'll need to migrate to getFeatures from start contract in other places too, and the worst thing is that we need features synchronously in one of our custom route validation code 🙈 (tried once and stuck at that point).

@@ -14,7 +16,40 @@ import {
transformPutPayloadToElasticsearchRole,
} from './model';

export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) {
const roleGrantsSubFeaturePrivileges = (features: Feature[], role: Role) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I'm wondering if it'd be safer to use TypeOf<ReturnType<typeof getPutPayloadSchema>> to catch any discrepancies between Role and the type of request we send to create that role at the compile-time (and hence remove as Role cast below)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional nit: I think you could also simply this a bit (to get rid of reduce and hasOwnProperty) with a Map, but it's subjective and up to you:

const roleGrantsSubFeaturePrivileges = (
  features: Feature[],
  role: TypeOf<ReturnType<typeof getPutPayloadSchema>>
) => {
  if (!role.kibana) {
    return false;
  }

  const subFeaturePrivileges = new Map(
    features.map(
      (feature) =>
        [
          feature.id,
          feature.subFeatures?.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2),
        ] as [string, SubFeaturePrivilegeConfig[]]
    )
  );

  const hasAnySubFeaturePrivileges = role.kibana.some((kibanaPrivilege) =>
    Object.entries(kibanaPrivilege.feature ?? {}).some(([featureId, privileges]) => {
      return !!subFeaturePrivileges.get(featureId)?.some(({ id }) => privileges.includes(id));
    })
  );

  return hasAnySubFeaturePrivileges;
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both great ideas, thank you!

Comment on lines 151 to 159
if (asserts.recordSubFeaturePrivilegeUsage) {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(1);
} else {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(0);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional nit:

Suggested change
if (asserts.recordSubFeaturePrivilegeUsage) {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(1);
} else {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(0);
}
if (asserts.recordSubFeaturePrivilegeUsage) {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(1);
} else {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).not.toHaveBeenCalled();
}

OR

Suggested change
if (asserts.recordSubFeaturePrivilegeUsage) {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(1);
} else {
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(0);
}
expect(
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage
).toHaveBeenCalledTimes(asserts.recordSubFeaturePrivilegeUsage ? 1 : 0);

{}
);

const hasAnySubFeaturePrivileges = (role.kibana ?? []).some((kibanaPrivilege) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: am I understanding this correctly that we're recording not the fact that subfeature privileges are used per se, but rather the fact that user chose to customize sub features (e.g. when user chooses All, all subfeature privileges are "granted" and relied upon implicitly if I'm not missing anything)? If so, wondering if we should make that clear in the feature usage name/id (e.g. put custom in there or something like this). Not important at all, just curious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly right. We are only recording the fact that a role was created/updated with explicit sub-feature privileges granted, as opposed to the "primary" feature privileges, or the base privileges.

I think the usage name is ok for the time being. The intent is to figure out when sub-feature privileges are being used at all, and this customization check is a "close enough" approximation of this. There is potential that we'll change the way we check for this in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, works for me too 👍

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@legrego legrego merged commit a9b2d50 into elastic:master Jun 4, 2020
@legrego legrego deleted the security/record-subfeature-usage branch June 4, 2020 16:29
legrego added a commit to legrego/kibana that referenced this pull request Jun 4, 2020
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
legrego added a commit that referenced this pull request Jun 4, 2020
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release_note:skip Skip the PR/issue when compiling release notes Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v7.9.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Licensed feature usage - access message Licensed feature usage - roles with sub-feature privileges
6 participants