[6.7] [BeatsCM] fix API for tokens to support any number (#30335) (#31661)

* [BeatsCM] fix API for tokens to support any number (#30335)

* add basic script for standing up a fake env

* tweaks

* API needs to support any number of tokens not us

* [BeatsCM] Add testing script used to create test deployments

* Move to JWT for enrollment

* wrap dont scroll command

* Dont use token as token ID

* fix tests

* not sure why this file is enabled in this branch/PR…

# Conflicts:
#	x-pack/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts

* remove dev only k7Design
This commit is contained in:
Matt Apperson 2019-02-21 10:30:58 -05:00 committed by GitHub
parent 495b952235
commit 47d838e436
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 468 additions and 91 deletions

View file

@ -5,12 +5,16 @@
*/
import {
EuiBasicTable,
EuiButton,
EuiCodeBlock,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiModalBody,
// @ts-ignore
EuiSelect,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
@ -87,6 +91,11 @@ export class EnrollBeat extends React.Component<ComponentProps, ComponentState>
if (this.props.enrollmentToken && !this.state.enrolledBeat) {
this.waitForTokenToEnrollBeat();
}
const cmdText = `${this.state.command
.replace('{{beatType}}', this.state.beatType)
.replace('{{beatTypeInCaps}}', capitalize(this.state.beatType))} enroll ${
window.location.protocol
}//${window.location.host}${this.props.frameworkBasePath} ${this.props.enrollmentToken}`;
return (
<React.Fragment>
@ -166,7 +175,7 @@ export class EnrollBeat extends React.Component<ComponentProps, ComponentState>
{this.state.command && (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>
@ -180,23 +189,27 @@ export class EnrollBeat extends React.Component<ComponentProps, ComponentState>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem className="homTutorial__instruction" grow={false}>
<EuiCopy textToCopy={cmdText}>
{(copy: any) => (
<EuiButton size="s" onClick={copy}>
<FormattedMessage
id="xpack.beatsManagement.enrollBeat.copyButtonLabel"
defaultMessage="Copy command"
/>
</EuiButton>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
<div className="euiFormControlLayout euiFormControlLayout--fullWidth">
<div
className="euiFieldText euiFieldText--fullWidth"
style={{ textAlign: 'left' }}
>
{`$ ${this.state.command
.replace('{{beatType}}', this.state.beatType)
.replace('{{beatTypeInCaps}}', capitalize(this.state.beatType))} enroll ${
window.location.protocol
}//${window.location.host}${this.props.frameworkBasePath} ${
this.props.enrollmentToken
}`}
</div>
<div className="eui-textBreakAll">
<EuiSpacer size="m" />
<EuiCodeBlock language="sh">{`$ ${cmdText}`}</EuiCodeBlock>
</div>
<br />
<br />
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center">

View file

@ -32,12 +32,11 @@ export interface FrameworkAdapter {
order?: number;
}): void;
setUISettings(key: string, value: any): void;
getUISetting(key: 'k7design'): boolean;
getUISetting(key: string): boolean;
}
export const RuntimeFrameworkInfo = t.type({
basePath: t.string,
k7Design: t.boolean,
license: t.type({
type: t.union(LICENSES.map(s => t.literal(s))),
expired: t.boolean,

View file

@ -49,19 +49,11 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
public readonly version: string
) {
this.adapterService = new KibanaAdapterServiceProvider();
this.settingSubscription = uiSettings.getUpdate$().subscribe({
next: ({ key, newValue }: { key: string; newValue: boolean }) => {
if (key === 'k7design' && this.xpackInfo) {
this.xpackInfo.k7Design = newValue;
}
},
});
}
// We dont really want to have this, but it's needed to conditionaly render for k7 due to
// when that data is needed.
public getUISetting(key: 'k7design'): boolean {
public getUISetting(key: string): boolean {
return this.uiSettings.get(key);
}
@ -86,7 +78,6 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
try {
xpackInfoUnpacked = {
basePath: this.getBasePath(),
k7Design: this.uiSettings.get('k7design'),
license: {
type: xpackInfo ? xpackInfo.getLicense().type : 'oss',
expired: xpackInfo ? !xpackInfo.getLicense().isActive : false,

View file

@ -0,0 +1,69 @@
/*
* 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 * as React from 'react';
import { FrameworkAdapter, FrameworkInfo, FrameworkUser } from './adapter_types';
export class TestingFrameworkAdapter implements FrameworkAdapter {
public get info() {
if (this.xpackInfo) {
return this.xpackInfo;
} else {
throw new Error('framework adapter must have init called before anything else');
}
}
public get currentUser() {
return this.shieldUser!;
}
private settings: any;
constructor(
private readonly xpackInfo: FrameworkInfo | null,
private readonly shieldUser: FrameworkUser | null,
public readonly version: string
) {}
// We dont really want to have this, but it's needed to conditionaly render for k7 due to
// when that data is needed.
public getUISetting(key: string): boolean {
return this.settings[key];
}
public setUISettings = (key: string, value: any) => {
this.settings[key] = value;
};
public async waitUntilFrameworkReady(): Promise<void> {
return;
}
public renderUIAtPath(
path: string,
component: React.ReactElement<any>,
toController: 'management' | 'self' = 'self'
) {
throw new Error('not yet implamented');
}
public registerManagementSection(settings: {
id?: string;
name: string;
iconName: string;
order?: number;
}) {
throw new Error('not yet implamented');
}
public registerManagementUI(settings: {
sectionId?: string;
name: string;
basePath: string;
visable?: boolean;
order?: number;
}) {
throw new Error('not yet implamented');
}
}

View file

@ -0,0 +1,92 @@
/*
* 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 axios, { AxiosInstance } from 'axios';
import fs from 'fs';
import { join, resolve } from 'path';
import { FlatObject } from '../../../frontend_types';
import { RestAPIAdapter } from './adapter_types';
const pkg = JSON.parse(
fs.readFileSync(resolve(join(__dirname, '../../../../../../../package.json'))).toString()
);
let globalAPI: AxiosInstance;
export class NodeAxiosAPIAdapter implements RestAPIAdapter {
constructor(
private readonly username: string,
private readonly password: string,
private readonly basePath: string
) {}
public async get<ResponseData>(url: string, query?: FlatObject<object>): Promise<ResponseData> {
return await this.REST.get(url, query ? { params: query } : {}).then(resp => resp.data);
}
public async post<ResponseData>(
url: string,
body?: { [key: string]: any }
): Promise<ResponseData> {
return await this.REST.post(url, body).then(resp => resp.data);
}
public async delete<T>(url: string): Promise<T> {
return await this.REST.delete(url).then(resp => resp.data);
}
public async put<ResponseData>(url: string, body?: any): Promise<ResponseData> {
return await this.REST.put(url, body).then(resp => resp.data);
}
private get REST() {
if (globalAPI) {
return globalAPI;
}
globalAPI = axios.create({
baseURL: this.basePath,
withCredentials: true,
responseType: 'json',
timeout: 60 * 10 * 1000, // 10min
auth: {
username: this.username,
password: this.password,
},
headers: {
'Access-Control-Allow-Origin': '*',
Accept: 'application/json',
'Content-Type': 'application/json',
'kbn-version': (pkg as any).version,
'kbn-xsrf': 'xxx',
},
});
// Add a request interceptor
globalAPI.interceptors.request.use(
config => {
// Do something before request is sent
return config;
},
error => {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
globalAPI.interceptors.response.use(
response => {
// Do something with response data
return response;
},
error => {
// Do something with response error
return Promise.reject(JSON.stringify(error.response.data));
}
);
return globalAPI;
}
}

View file

@ -5,5 +5,5 @@
*/
export interface CMTokensAdapter {
createEnrollmentToken(): Promise<string>;
createEnrollmentTokens(numTokens?: number): Promise<string[]>;
}

View file

@ -7,7 +7,7 @@
import { CMTokensAdapter } from './adapter_types';
export class MemoryTokensAdapter implements CMTokensAdapter {
public async createEnrollmentToken(): Promise<string> {
return '2jnwkrhkwuehriauhweair';
public async createEnrollmentTokens(): Promise<string[]> {
return ['2jnwkrhkwuehriauhweair'];
}
}

View file

@ -10,9 +10,10 @@ import { CMTokensAdapter } from './adapter_types';
export class RestTokensAdapter implements CMTokensAdapter {
constructor(private readonly REST: RestAPIAdapter) {}
public async createEnrollmentToken(): Promise<string> {
const tokens = (await this.REST.post<{ tokens: string[] }>('/api/beats/enrollment_tokens'))
.tokens;
return tokens[0];
public async createEnrollmentTokens(numTokens: number = 1): Promise<string[]> {
const tokens = (await this.REST.post<{ tokens: string[] }>('/api/beats/enrollment_tokens', {
num_tokens: numTokens,
})).tokens;
return tokens;
}
}

View file

@ -0,0 +1,74 @@
/*
* 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 { configBlockSchemas } from '../../../common/config_schemas';
import { translateConfigSchema } from '../../../common/config_schemas_translations_map';
import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter';
import { RestConfigBlocksAdapter } from '../adapters/configuration_blocks/rest_config_blocks_adapter';
import { MemoryElasticsearchAdapter } from '../adapters/elasticsearch/memory';
import { TestingFrameworkAdapter } from '../adapters/framework/testing_framework_adapter';
import { NodeAxiosAPIAdapter } from '../adapters/rest_api/node_axios_api_adapter';
import { RestTagsAdapter } from '../adapters/tags/rest_tags_adapter';
import { RestTokensAdapter } from '../adapters/tokens/rest_tokens_adapter';
import { BeatsLib } from '../beats';
import { ConfigBlocksLib } from '../configuration_blocks';
import { ElasticsearchLib } from '../elasticsearch';
import { FrameworkLib } from '../framework';
import { TagsLib } from '../tags';
import { FrontendLibs } from '../types';
export function compose(basePath: string): FrontendLibs {
const api = new NodeAxiosAPIAdapter('elastic', 'changeme', basePath);
const esAdapter = new MemoryElasticsearchAdapter(() => true, () => '', []);
const elasticsearchLib = new ElasticsearchLib(esAdapter);
const configBlocks = new ConfigBlocksLib(
new RestConfigBlocksAdapter(api),
translateConfigSchema(configBlockSchemas)
);
const tags = new TagsLib(new RestTagsAdapter(api), elasticsearchLib);
const tokens = new RestTokensAdapter(api);
const beats = new BeatsLib(new RestBeatsAdapter(api), elasticsearchLib);
const framework = new FrameworkLib(
new TestingFrameworkAdapter(
{
basePath,
license: {
type: 'gold',
expired: false,
expiry_date_in_millis: 34353453452345,
},
security: {
enabled: true,
available: true,
},
settings: {
encryptionKey: 'xpack_beats_default_encryptionKey',
enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes
defaultUserRoles: ['superuser'],
},
},
{
username: 'joeuser',
roles: ['beats_admin'],
enabled: true,
full_name: null,
email: null,
},
'6.7.0'
)
);
const libs: FrontendLibs = {
framework,
elasticsearch: elasticsearchLib,
tags,
tokens,
beats,
configBlocks,
};
return libs;
}

View file

@ -127,9 +127,9 @@ class BeatsPageComponent extends React.PureComponent<PageProps, PageState> {
enrollmentToken={this.props.urlState.enrollmentToken}
getBeatWithToken={this.props.containers.beats.getBeatWithToken}
createEnrollmentToken={async () => {
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
const enrollmentTokens = await this.props.libs.tokens.createEnrollmentTokens();
this.props.setUrlState({
enrollmentToken,
enrollmentToken: enrollmentTokens[0],
});
}}
onBeatEnrolled={() => {

View file

@ -27,9 +27,9 @@ export class BeatsInitialEnrollmentPage extends Component<AppPageProps, Componen
};
public createEnrollmentToken = async () => {
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
const enrollmentToken = await this.props.libs.tokens.createEnrollmentTokens();
this.props.setUrlState({
enrollmentToken,
enrollmentToken: enrollmentToken[0],
});
};

View file

@ -12,7 +12,7 @@ node scripts/jest.js plugins/beats --watch
and for functional... (from x-pack root)
```
node scripts/functional_tests --config test/api_integration/config
node scripts/functional_tests --config test/api_integration/config
```
### Run command to fake an enrolling beat (from beats_management dir)
@ -20,3 +20,11 @@ and for functional... (from x-pack root)
```
node scripts/enroll.js <enrollment token>
```
### Run a command to setup a fake large-scale deployment
Note: ts-node is required to be installed gloably from NPM/Yarn for this action
```
ts-node scripts/fake_env.ts <KIBANA BASE PATH> <# of beats> <# of tags per beat> <# of congifs per tag>
```

View file

@ -0,0 +1,156 @@
/*
* 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 Chance from 'chance'; // eslint-disable-line
// @ts-ignore
import request from 'request';
import uuidv4 from 'uuid/v4';
import { configBlockSchemas } from 'x-pack/plugins/beats_management/common/config_schemas';
import { BeatTag } from '../common/domain_types';
import { compose } from '../public/lib/compose/scripts';
const args = process.argv.slice(2);
const chance = new Chance();
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
const enroll = async (kibanaURL: string, token: string) => {
const beatId = uuidv4();
await request(
{
url: `${kibanaURL}/api/beats/agent/${beatId}`,
method: 'POST',
headers: {
'kbn-xsrf': 'xxx',
'kbn-beats-enrollment-token': token,
},
body: JSON.stringify({
type: Math.random() >= 0.5 ? 'filebeat' : 'metricbeat',
host_name: `${chance.word()}.local`,
name: chance.word(),
version: '6.7.0',
}),
},
(error: any, response: any, body: any) => {
const res = JSON.parse(body);
if (res.message) {
// tslint:disable-next-line
console.log(res.message);
}
}
);
};
const start = async (
kibanaURL: string,
numberOfBeats = 10,
maxNumberOfTagsPerBeat = 2,
maxNumberOfConfigsPerTag = 4
) => {
try {
const libs = compose(kibanaURL);
// tslint:disable-next-line
console.error(`Enrolling ${numberOfBeats} fake beats...`);
const enrollmentTokens = await libs.tokens.createEnrollmentTokens(numberOfBeats);
process.stdout.write(`enrolling fake beats... 0 of ${numberOfBeats}`);
let count = 0;
for (const token of enrollmentTokens) {
count++;
// @ts-ignore
process.stdout.clearLine();
// @ts-ignore
process.stdout.cursorTo(0);
process.stdout.write(`enrolling fake beats... ${count} of ${numberOfBeats}`);
await enroll(kibanaURL, token);
await sleep(10);
}
process.stdout.write('\n');
await sleep(2000);
// tslint:disable-next-line
console.error(`${numberOfBeats} fake beats are enrolled`);
const beats = await libs.beats.getAll();
// tslint:disable-next-line
console.error(`Creating tags, configs, and assigning them...`);
process.stdout.write(`creating tags/configs for beat... 0 of ${numberOfBeats}`);
count = 0;
for (const beat of beats) {
count++;
// @ts-ignore
process.stdout.clearLine();
// @ts-ignore
process.stdout.cursorTo(0);
process.stdout.write(`creating tags w/configs for beat... ${count} of ${numberOfBeats}`);
const tags = await Promise.all(
[...Array(maxNumberOfTagsPerBeat)].map(() => {
return libs.tags.upsertTag({
name: chance.word(),
color: getRandomColor(),
hasConfigurationBlocksTypes: [] as string[],
} as BeatTag);
})
);
await libs.beats.assignTagsToBeats(
tags.map((tag: any) => ({
beatId: beat.id,
tag: tag.id,
}))
);
await Promise.all(
tags.map((tag: any) => {
return libs.configBlocks.upsert(
[...Array(maxNumberOfConfigsPerTag)].map(
() =>
({
type: configBlockSchemas[Math.floor(Math.random())].id,
description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sint ista Graecorum;
Nihil ad rem! Ne sit sane; Quod quidem nobis non saepe contingit.
Duo Reges: constructio interrete. Itaque his sapiens semper vacabit.`.substring(
0,
Math.floor(Math.random() * (0 - 115 + 1))
),
tag: tag.id,
last_updated: new Date(),
config: {},
} as any)
)
);
})
);
}
} catch (e) {
if (e.response && e.response.data && e.response.message) {
// tslint:disable-next-line
console.error(e.response.data.message);
} else if (e.response && e.response.data && e.response.reason) {
// tslint:disable-next-line
console.error(e.response.data.reason);
} else if (e.code) {
// tslint:disable-next-line
console.error(e.code);
} else {
// tslint:disable-next-line
console.error(e);
}
}
};
// @ts-ignore
start(...args);

View file

@ -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.
*/
// file.skip
import { camelCase } from 'lodash';
// @ts-ignore
import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server';
// @ts-ignore
import { xpackKbnServerConfig } from '../../../../../../../test_utils/kbn_server_config';
import { PLUGIN } from './../../../../../common/constants/plugin';
import { KibanaBackendFrameworkAdapter } from './../kibana_framework_adapter';
import { contractTests } from './test_contract';
let servers: any;
contractTests('Kibana Framework Adapter', {
async before() {
servers = await kbnTestServer.startTestServers({
adjustTimeout: (t: number) => jest.setTimeout(t),
settings: xpackKbnServerConfig,
});
},
async after() {
await servers.stop();
},
adapterSetup: () => {
return new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), servers.kbnServer.server);
},
});

View file

@ -13,7 +13,7 @@ interface ContractConfig {
export const contractTests = (testName: string, config: ContractConfig) => {
describe(testName, () => {
let frameworkAdapter: any;
let frameworkAdapter: BackendFrameworkAdapter;
beforeAll(config.before);
afterAll(config.after);
beforeEach(async () => {
@ -21,9 +21,9 @@ export const contractTests = (testName: string, config: ContractConfig) => {
});
it('Should have tests here', () => {
expect(frameworkAdapter.info).toHaveProperty('server');
expect(frameworkAdapter).toHaveProperty('server');
expect(frameworkAdapter.server).toHaveProperty('plugins');
expect(frameworkAdapter.server.plugins).toHaveProperty('security');
});
});
};

View file

@ -13,5 +13,5 @@ export interface TokenEnrollmentData {
export interface CMTokensAdapter {
deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise<void>;
getEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise<TokenEnrollmentData>;
upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]): Promise<TokenEnrollmentData[]>;
insertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]): Promise<TokenEnrollmentData[]>;
}

View file

@ -35,6 +35,7 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter {
};
const response = await this.database.get(user, params);
const tokenDetails = get<TokenEnrollmentData>(response, '_source.enrollment_token', {
expires_on: '0',
token: null,
@ -50,7 +51,7 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter {
);
}
public async upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]) {
public async insertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]) {
const body = flatten(
tokens.map(token => [
{ index: { _id: `enrollment_token:${token.token}` } },

View file

@ -31,7 +31,7 @@ export class MemoryTokensAdapter implements CMTokensAdapter {
});
}
public async upsertTokens(user: FrameworkAuthenticatedUser, tokens: TokenEnrollmentData[]) {
public async insertTokens(user: FrameworkAuthenticatedUser, tokens: TokenEnrollmentData[]) {
tokens.forEach(token => {
const existingIndex = this.tokenDB.findIndex(t => t.token === token.token);
if (existingIndex !== -1) {

View file

@ -96,7 +96,6 @@ export class CMBeatsDomain {
beat: Partial<CMBeat>
): Promise<{ status: string; accessToken?: string }> {
const { token, expires_on } = await this.tokens.getEnrollmentToken(enrollmentToken);
if (expires_on && moment(expires_on).isBefore(moment())) {
return { status: BeatEnrollmentStatus.ExpiredEnrollmentToken };
}

View file

@ -3,10 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { timingSafeEqual } from 'crypto';
import { randomBytes, timingSafeEqual } from 'crypto';
import { sign as signToken, verify as verifyToken } from 'jsonwebtoken';
import { chunk } from 'lodash';
import moment from 'moment';
import uuid from 'uuid';
import { FrameworkUser } from './adapters/framework/adapter_types';
import { CMTokensAdapter } from './adapters/tokens/adapter_types';
import { CMServerLibs } from './types';
@ -28,7 +28,6 @@ export class CMTokensDomain {
this.framework.internalUser,
enrollmentToken
);
if (!fullToken) {
return {
token: null,
@ -103,7 +102,7 @@ export class CMTokensDomain {
const tokenData = {
created: moment().toJSON(),
randomHash: this.createRandomHash(),
randomHash: randomBytes(26).toString(),
};
return signToken(tokenData, enrollmentTokenSecret);
@ -119,20 +118,25 @@ export class CMTokensDomain {
const enrollmentTokenExpiration = moment()
.add(enrollmentTokensTtlInSeconds, 'seconds')
.toJSON();
const enrollmentTokenSecret = this.framework.getSetting('encryptionKey');
while (tokens.length < numTokens) {
const tokenData = {
created: moment().toJSON(),
expires: enrollmentTokenExpiration,
randomHash: randomBytes(26).toString(),
};
tokens.push({
expires_on: enrollmentTokenExpiration,
token: this.createRandomHash(),
token: signToken(tokenData, enrollmentTokenSecret),
});
}
await this.adapter.upsertTokens(user, tokens);
await Promise.all(
chunk(tokens, 100).map(tokenChunk => this.adapter.insertTokens(user, tokenChunk))
);
return tokens.map(token => token.token);
}
private createRandomHash() {
return uuid.v4().replace(/-/g, '');
}
}

View file

@ -13,7 +13,6 @@ import {
export default function ({ getService }) {
const supertest = getService('supertest');
const chance = getService('chance');
const es = getService('es');
describe('create_enrollment_token', () => {
@ -42,7 +41,7 @@ export default function ({ getService }) {
});
it('should create the specified number of tokens', async () => {
const numTokens = chance.integer({ min: 1, max: 2000 });
const numTokens = 1000;
const { body: apiResponse } = await supertest
.post(
@ -66,8 +65,10 @@ export default function ({ getService }) {
const tokensInEs = esResponse.hits.hits
.map(hit => hit._source.enrollment_token.token);
expect(tokensFromApi).to.be.an('array');
expect(tokensFromApi.length).to.eql(numTokens);
expect(tokensFromApi).to.eql(tokensInEs);
expect(tokensInEs.length).to.eql(numTokens);
expect(tokensFromApi.sort()).to.eql(tokensInEs.sort());
});
it('should set token expiration to 10 minutes from now by default', async () => {