-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Record security feature usage #67526
Conversation
@elasticmachine merge upstream |
featureUsage.register('security_pre_access_agreement'); | ||
} | ||
|
||
public start({ featureUsage }: StartDeps): SecurityFeatureUsageServiceStart { |
There was a problem hiding this comment.
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:
- 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. - 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.
There was a problem hiding this comment.
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 👍
@elasticmachine merge upstream |
@elasticmachine merge upstream |
Latest failure was due to OOM?
|
@elasticmachine merge upstream |
Pinging @elastic/kibana-security (Team:Security) |
x-pack/plugins/security/server/feature_usage/feature_usage_service.ts
Outdated
Show resolved
Hide resolved
@elasticmachine merge upstream |
…/kibana into security/record-subfeature-usage
#67712 just merged 😬 |
Ack: will review tomorrow. |
There was a problem hiding this 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'; |
There was a problem hiding this comment.
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
recordSubFeaturePrivilegeUsage(kibanaPrivileges: Array<Optional<RoleKibanaPrivilege>> = []) { | ||
featureUsage.notifyUsage('Subfeature privileges'); | ||
}, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
mockDependencies = ({ | ||
licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, | ||
} as unknown) as PluginSetupDependencies; |
There was a problem hiding this comment.
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.
if (this.featureUsageService) { | ||
this.featureUsageService = undefined; | ||
} | ||
|
||
if (this.featureUsageServiceStart) { | ||
this.featureUsageServiceStart = undefined; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
{ | ||
last_used: null, | ||
license_level: 'gold', | ||
name: 'Subfeature privileges', | ||
}, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@elastic/kibana-security this is ready for review |
ACK: will review today or early morning tomorrow. |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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!; |
There was a problem hiding this comment.
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: () => |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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;
};
There was a problem hiding this comment.
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!
if (asserts.recordSubFeaturePrivilegeUsage) { | ||
expect( | ||
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage | ||
).toHaveBeenCalledTimes(1); | ||
} else { | ||
expect( | ||
mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage | ||
).toHaveBeenCalledTimes(0); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optional nit:
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
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) => |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👍
💚 Build SucceededHistory
To update your PR or re-run it, just comment with: |
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Summary
Updates the security plugin to notify the
featureUsage
service when its paid features are used:Resolves #64847
Resolves #67527