-
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
Changes from 18 commits
5d50b00
1079df2
f8023a1
5bafd74
0dd8f38
7cc7dc6
8300f8f
40cdabb
4d15c05
9a4e07d
2974118
ffdd91c
8b2c17c
f88753e
4b092df
dfb16e9
a0bd707
0fa0926
de81ff1
46e59e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* 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 { SecurityFeatureUsageService } from './feature_usage_service'; | ||
|
||
describe('#setup', () => { | ||
it('registers all known security features', () => { | ||
const featureUsage = { register: jest.fn() }; | ||
const securityFeatureUsage = new SecurityFeatureUsageService(); | ||
securityFeatureUsage.setup({ featureUsage }); | ||
expect(featureUsage.register).toHaveBeenCalledTimes(2); | ||
expect(featureUsage.register.mock.calls.map((c) => c[0])).toMatchInlineSnapshot(` | ||
Array [ | ||
"Subfeature privileges", | ||
"Pre-access agreement", | ||
] | ||
`); | ||
}); | ||
}); | ||
|
||
describe('start contract', () => { | ||
it('notifies when sub-feature privileges are in use', () => { | ||
const featureUsage = { notifyUsage: jest.fn(), getLastUsages: jest.fn() }; | ||
const securityFeatureUsage = new SecurityFeatureUsageService(); | ||
const startContract = securityFeatureUsage.start({ featureUsage }); | ||
startContract.recordSubFeaturePrivilegeUsage(); | ||
expect(featureUsage.notifyUsage).toHaveBeenCalledTimes(1); | ||
expect(featureUsage.notifyUsage).toHaveBeenCalledWith('Subfeature privileges'); | ||
}); | ||
|
||
it('notifies when pre-access agreement is used', () => { | ||
const featureUsage = { notifyUsage: jest.fn(), getLastUsages: jest.fn() }; | ||
const securityFeatureUsage = new SecurityFeatureUsageService(); | ||
const startContract = securityFeatureUsage.start({ featureUsage }); | ||
startContract.recordPreAccessAgreementUsage(); | ||
expect(featureUsage.notifyUsage).toHaveBeenCalledTimes(1); | ||
expect(featureUsage.notifyUsage).toHaveBeenCalledWith('Pre-access agreement'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/* | ||
* 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 { FeatureUsageServiceSetup, FeatureUsageServiceStart } from '../../../licensing/server'; | ||
|
||
interface SetupDeps { | ||
featureUsage: FeatureUsageServiceSetup; | ||
} | ||
|
||
interface StartDeps { | ||
featureUsage: FeatureUsageServiceStart; | ||
} | ||
|
||
export interface SecurityFeatureUsageServiceStart { | ||
recordPreAccessAgreementUsage: () => void; | ||
recordSubFeaturePrivilegeUsage: () => void; | ||
} | ||
|
||
export class SecurityFeatureUsageService { | ||
public setup({ featureUsage }: SetupDeps) { | ||
featureUsage.register('Subfeature privileges', 'gold'); | ||
featureUsage.register('Pre-access agreement', 'gold'); | ||
} | ||
|
||
public start({ featureUsage }: StartDeps): SecurityFeatureUsageServiceStart { | ||
return { | ||
recordPreAccessAgreementUsage() { | ||
featureUsage.notifyUsage('Pre-access agreement'); | ||
}, | ||
recordSubFeaturePrivilegeUsage() { | ||
featureUsage.notifyUsage('Subfeature privileges'); | ||
}, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { SecurityFeatureUsageServiceStart } from './feature_usage_service'; | ||
|
||
export const securityFeatureUsageServiceMock = { | ||
createStartContract() { | ||
return { | ||
recordPreAccessAgreementUsage: jest.fn(), | ||
recordSubFeaturePrivilegeUsage: jest.fn(), | ||
} as jest.Mocked<SecurityFeatureUsageServiceStart>; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
export { | ||
SecurityFeatureUsageService, | ||
SecurityFeatureUsageServiceStart, | ||
} from './feature_usage_service'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,7 +41,9 @@ describe('Security Plugin', () => { | |
mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); | ||
mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); | ||
|
||
mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; | ||
mockDependencies = ({ | ||
licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, | ||
} as unknown) as PluginSetupDependencies; | ||
Comment on lines
+44
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use the licensing plugin's mocks in |
||
}); | ||
|
||
describe('setup()', () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,11 +12,15 @@ import { | |
CoreSetup, | ||
Logger, | ||
PluginInitializerContext, | ||
CoreStart, | ||
} from '../../../../src/core/server'; | ||
import { deepFreeze } from '../../../../src/core/server'; | ||
import { SpacesPluginSetup } from '../../spaces/server'; | ||
import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; | ||
import { LicensingPluginSetup } from '../../licensing/server'; | ||
import { | ||
PluginSetupContract as FeaturesSetupContract, | ||
PluginStartContract as FeaturesStartContract, | ||
} from '../../features/server'; | ||
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; | ||
|
||
import { Authentication, setupAuthentication } from './authentication'; | ||
import { Authorization, setupAuthorization } from './authorization'; | ||
|
@@ -26,6 +30,7 @@ import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; | |
import { setupSavedObjects } from './saved_objects'; | ||
import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; | ||
import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; | ||
import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage'; | ||
|
||
export type SpacesService = Pick< | ||
SpacesPluginSetup['spacesService'], | ||
|
@@ -72,6 +77,11 @@ export interface PluginSetupDependencies { | |
licensing: LicensingPluginSetup; | ||
} | ||
|
||
export interface PluginStartDependencies { | ||
features: FeaturesStartContract; | ||
licensing: LicensingPluginStart; | ||
} | ||
|
||
/** | ||
* Represents Security Plugin instance that will be managed by the Kibana plugin system. | ||
*/ | ||
|
@@ -80,6 +90,16 @@ export class Plugin { | |
private clusterClient?: ICustomClusterClient; | ||
private spacesService?: SpacesService | symbol = Symbol('not accessed'); | ||
private securityLicenseService?: SecurityLicenseService; | ||
|
||
private readonly featureUsageService = new SecurityFeatureUsageService(); | ||
private featureUsageServiceStart?: SecurityFeatureUsageServiceStart; | ||
private readonly getFeatureUsageService = () => { | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. nit: do we need |
||
}; | ||
|
||
private readonly auditService = new AuditService(this.initializerContext.logger.get('audit')); | ||
|
||
private readonly getSpacesService = () => { | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. note: yeah, the time has come 🙂 We need a proper |
||
auditLogger, | ||
getFeatureUsageService: this.getFeatureUsageService, | ||
http: core.http, | ||
clusterClient: this.clusterClient, | ||
config, | ||
|
@@ -160,6 +183,11 @@ export class Plugin { | |
authc, | ||
authz, | ||
license, | ||
getFeatures: () => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I guess we can simply define P.S. we'll need to migrate to |
||
(core.getStartServices() as Promise< | ||
[CoreStart, PluginStartDependencies, unknown] | ||
>).then(([, { features: featuresStart }]) => featuresStart.getFeatures()), | ||
getFeatureUsageService: this.getFeatureUsageService, | ||
}); | ||
|
||
return deepFreeze<SecurityPluginSetup>({ | ||
|
@@ -199,8 +227,11 @@ export class Plugin { | |
}); | ||
} | ||
|
||
public start() { | ||
public start(core: CoreStart, { licensing }: PluginStartDependencies) { | ||
this.logger.debug('Starting plugin'); | ||
this.featureUsageServiceStart = this.featureUsageService.start({ | ||
featureUsage: licensing.featureUsage, | ||
}); | ||
} | ||
|
||
public stop() { | ||
|
@@ -216,6 +247,9 @@ export class Plugin { | |
this.securityLicenseService = undefined; | ||
} | ||
|
||
if (this.featureUsageServiceStart) { | ||
this.featureUsageServiceStart = undefined; | ||
} | ||
this.auditService.stop(); | ||
} | ||
|
||
|
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 thefeatureUsage
service exposed by the licensing plugin.I opted to do this anyway because:
SecurityLicense
service, so it felt appropriate to continue this initiative.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 👍