mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Add updated_by
to saved objects (#182687)
## Summary close https://github.com/elastic/kibana-team/issues/899 - Adds `updated_by` to saved object, similar to recently added `created_by` https://github.com/elastic/kibana/pull/179344 - Fixes `created_by` / `created_at` should be set during upsert - Improves functional tests coverage
This commit is contained in:
parent
f7b9777f40
commit
88757a30a6
37 changed files with 742 additions and 25 deletions
|
@ -368,6 +368,7 @@ enabled:
|
|||
- x-pack/test/saved_object_api_integration/security_and_spaces/config_basic.ts
|
||||
- x-pack/test/saved_object_api_integration/security_and_spaces/config_trial.ts
|
||||
- x-pack/test/saved_object_api_integration/spaces_only/config.ts
|
||||
- x-pack/test/saved_object_api_integration/user_profiles/config.ts
|
||||
- x-pack/test/saved_object_tagging/api_integration/security_and_spaces/config.ts
|
||||
- x-pack/test/saved_object_tagging/api_integration/tagging_api/config.ts
|
||||
- x-pack/test/saved_object_tagging/api_integration/tagging_usage_collection/config.ts
|
||||
|
|
|
@ -42,6 +42,8 @@ export interface SimpleSavedObject<T = unknown> {
|
|||
references: SavedObjectType<T>['references'];
|
||||
/** The date this object was last updated */
|
||||
updatedAt: SavedObjectType<T>['updated_at'];
|
||||
/** The user that last updated this object */
|
||||
updatedBy: SavedObjectType<T>['updated_by'];
|
||||
/** The date this object was created */
|
||||
createdAt: SavedObjectType<T>['created_at'];
|
||||
/** The user that created this object */
|
||||
|
|
|
@ -85,6 +85,7 @@ export const performBulkCreate = async <T>(
|
|||
} = options;
|
||||
const time = getCurrentTime();
|
||||
const createdBy = userHelper.getCurrentUserProfileUid();
|
||||
const updatedBy = createdBy;
|
||||
|
||||
let preflightCheckIndexCounter = 0;
|
||||
const expectedResults = objects.map<ExpectedResult>((object) => {
|
||||
|
@ -234,6 +235,7 @@ export const performBulkCreate = async <T>(
|
|||
updated_at: time,
|
||||
created_at: time,
|
||||
...(createdBy && { created_by: createdBy }),
|
||||
...(updatedBy && { updated_by: updatedBy }),
|
||||
references: object.references || [],
|
||||
originId,
|
||||
}) as SavedObjectSanitizedDoc<T>;
|
||||
|
|
|
@ -82,10 +82,12 @@ export const performBulkUpdate = async <T>(
|
|||
common: commonHelper,
|
||||
encryption: encryptionHelper,
|
||||
migration: migrationHelper,
|
||||
user: userHelper,
|
||||
} = helpers;
|
||||
const { securityExtension } = extensions;
|
||||
const { migrationVersionCompatibility } = options;
|
||||
const namespace = commonHelper.getCurrentNamespace(options.namespace);
|
||||
const updatedBy = userHelper.getCurrentUserProfileUid();
|
||||
const time = getCurrentTime();
|
||||
|
||||
let bulkGetRequestIndexCounter = 0;
|
||||
|
@ -120,6 +122,7 @@ export const performBulkUpdate = async <T>(
|
|||
const documentToSave = {
|
||||
[type]: attributes,
|
||||
updated_at: time,
|
||||
updated_by: updatedBy,
|
||||
...(Array.isArray(references) && { references }),
|
||||
};
|
||||
|
||||
|
@ -304,6 +307,7 @@ export const performBulkUpdate = async <T>(
|
|||
namespaces,
|
||||
attributes: updatedAttributes,
|
||||
updated_at: time,
|
||||
updated_by: updatedBy,
|
||||
...(Array.isArray(documentToSave.references) && { references: documentToSave.references }),
|
||||
});
|
||||
const updatedMigratedDocumentToSave = serializer.savedObjectToRaw(
|
||||
|
@ -364,7 +368,7 @@ export const performBulkUpdate = async <T>(
|
|||
const { _seq_no: seqNo, _primary_term: primaryTerm } = rawResponse;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { [type]: attributes, references, updated_at } = documentToSave;
|
||||
const { [type]: attributes, references, updated_at, updated_by } = documentToSave;
|
||||
|
||||
const { originId } = rawMigratedUpdatedDoc._source;
|
||||
return {
|
||||
|
@ -373,6 +377,7 @@ export const performBulkUpdate = async <T>(
|
|||
...(namespaces && { namespaces }),
|
||||
...(originId && { originId }),
|
||||
updated_at,
|
||||
updated_by,
|
||||
version: encodeVersion(seqNo, primaryTerm),
|
||||
attributes,
|
||||
references,
|
||||
|
|
|
@ -71,6 +71,7 @@ export const performCreate = async <T>(
|
|||
|
||||
const time = getCurrentTime();
|
||||
const createdBy = userHelper.getCurrentUserProfileUid();
|
||||
const updatedBy = createdBy;
|
||||
let savedObjectNamespace: string | undefined;
|
||||
let savedObjectNamespaces: string[] | undefined;
|
||||
let existingOriginId: string | undefined;
|
||||
|
@ -136,6 +137,7 @@ export const performCreate = async <T>(
|
|||
created_at: time,
|
||||
updated_at: time,
|
||||
...(createdBy && { created_by: createdBy }),
|
||||
...(updatedBy && { updated_by: updatedBy }),
|
||||
...(Array.isArray(references) && { references }),
|
||||
});
|
||||
|
||||
|
|
|
@ -149,6 +149,7 @@ describe('find', () => {
|
|||
'typeMigrationVersion',
|
||||
'managed',
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'originId',
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
createConflictErrorPayload,
|
||||
createGenericNotFoundErrorPayload,
|
||||
updateSuccess,
|
||||
mockTimestampFieldsWithCreated,
|
||||
} from '../../test_helpers/repository.test.common';
|
||||
|
||||
describe('#update', () => {
|
||||
|
@ -319,7 +320,7 @@ describe('#update', () => {
|
|||
const expected = {
|
||||
'index-pattern': { description: 'bar', title: 'foo' },
|
||||
type: 'index-pattern',
|
||||
...mockTimestampFields,
|
||||
...mockTimestampFieldsWithCreated,
|
||||
};
|
||||
expect(
|
||||
(client.create.mock.calls[0][0] as estypes.CreateRequest<SavedObjectsRawDocSource>).body!
|
||||
|
@ -352,7 +353,7 @@ describe('#update', () => {
|
|||
multiNamespaceIsolatedType: { description: 'bar', title: 'foo' },
|
||||
namespaces: ['default'],
|
||||
type: 'multiNamespaceIsolatedType',
|
||||
...mockTimestampFields,
|
||||
...mockTimestampFieldsWithCreated,
|
||||
};
|
||||
expect(
|
||||
(client.create.mock.calls[0][0] as estypes.CreateRequest<SavedObjectsRawDocSource>).body!
|
||||
|
|
|
@ -81,6 +81,7 @@ export const executeUpdate = async <T>(
|
|||
preflight: preflightHelper,
|
||||
migration: migrationHelper,
|
||||
validation: validationHelper,
|
||||
user: userHelper,
|
||||
} = helpers;
|
||||
const { securityExtension } = extensions;
|
||||
const typeDefinition = registry.getType(type)!;
|
||||
|
@ -151,6 +152,7 @@ export const executeUpdate = async <T>(
|
|||
// END ALL PRE_CLIENT CALL CHECKS && MIGRATE EXISTING DOC;
|
||||
|
||||
const time = getCurrentTime();
|
||||
const updatedBy = userHelper.getCurrentUserProfileUid();
|
||||
let updatedOrCreatedSavedObject: SavedObject<T>;
|
||||
// `upsert` option set and document was not found -> we need to perform an upsert operation
|
||||
const shouldPerformUpsert = upsert && docNotFound;
|
||||
|
@ -176,7 +178,9 @@ export const executeUpdate = async <T>(
|
|||
attributes: {
|
||||
...(await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, upsert)),
|
||||
},
|
||||
created_at: time,
|
||||
updated_at: time,
|
||||
...(updatedBy && { created_by: updatedBy, updated_by: updatedBy }),
|
||||
...(Array.isArray(references) && { references }),
|
||||
}) as SavedObjectSanitizedDoc<T>;
|
||||
validationHelper.validateObjectForCreate(type, migratedUpsert);
|
||||
|
@ -232,7 +236,9 @@ export const executeUpdate = async <T>(
|
|||
updatedOrCreatedSavedObject = {
|
||||
id,
|
||||
type,
|
||||
created_at: time,
|
||||
updated_at: time,
|
||||
...(updatedBy && { created_by: updatedBy, updated_by: updatedBy }),
|
||||
version: encodeHitVersion(createDocResponseBody),
|
||||
namespaces,
|
||||
...(originId && { originId }),
|
||||
|
@ -273,6 +279,7 @@ export const executeUpdate = async <T>(
|
|||
namespaces: savedObjectNamespaces,
|
||||
attributes: updatedAttributes,
|
||||
updated_at: time,
|
||||
updated_by: updatedBy,
|
||||
...(Array.isArray(references) && { references }),
|
||||
});
|
||||
|
||||
|
@ -336,6 +343,7 @@ export const executeUpdate = async <T>(
|
|||
id,
|
||||
type,
|
||||
updated_at: time,
|
||||
...(updatedBy && { updated_by: updatedBy }),
|
||||
version: encodeHitVersion(indexDocResponseBody),
|
||||
namespaces,
|
||||
...(originId && { originId }),
|
||||
|
|
|
@ -102,6 +102,8 @@ describe('#getSavedObjectFromSource', () => {
|
|||
const updated_at = 'updatedAt';
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const created_by = 'createdBy';
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const updated_by = 'updatedBy';
|
||||
const managed = false;
|
||||
|
||||
function createRawDoc(
|
||||
|
@ -123,6 +125,7 @@ describe('#getSavedObjectFromSource', () => {
|
|||
originId,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by,
|
||||
...namespaceAttrs,
|
||||
},
|
||||
};
|
||||
|
@ -145,6 +148,7 @@ describe('#getSavedObjectFromSource', () => {
|
|||
references,
|
||||
type,
|
||||
updated_at,
|
||||
updated_by,
|
||||
created_by,
|
||||
version: encodeHitVersion(doc),
|
||||
});
|
||||
|
|
|
@ -110,6 +110,7 @@ export function getSavedObjectFromSource<T>(
|
|||
updated_at: updatedAt,
|
||||
created_at: createdAt,
|
||||
created_by: createdBy,
|
||||
updated_by: updatedBy,
|
||||
coreMigrationVersion,
|
||||
typeMigrationVersion,
|
||||
managed,
|
||||
|
@ -136,6 +137,7 @@ export function getSavedObjectFromSource<T>(
|
|||
...(updatedAt && { updated_at: updatedAt }),
|
||||
...(createdAt && { created_at: createdAt }),
|
||||
...(createdBy && { created_by: createdBy }),
|
||||
...(updatedBy && { updated_by: updatedBy }),
|
||||
version: encodeHitVersion(doc),
|
||||
attributes: doc._source[type],
|
||||
references: doc._source.references || [],
|
||||
|
|
|
@ -302,6 +302,20 @@ describe('SavedObjectsRepository Security Extension', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
test(`adds updated_by to the saved object when the current user is available`, async () => {
|
||||
const profileUid = 'profileUid';
|
||||
mockSecurityExt.getCurrentUser.mockImplementationOnce(() =>
|
||||
mockAuthenticatedUser({ profile_uid: profileUid })
|
||||
);
|
||||
|
||||
const result = await updateSuccess(client, repository, registry, type, id, attributes, {
|
||||
namespace,
|
||||
});
|
||||
|
||||
expect(result).not.toHaveProperty('created_by');
|
||||
expect(result.updated_by).toBe(profileUid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#create', () => {
|
||||
|
@ -425,7 +439,7 @@ describe('SavedObjectsRepository Security Extension', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test(`adds created_by to the saved object when the current user is available`, async () => {
|
||||
test(`adds created_by, updated_by to the saved object when the current user is available`, async () => {
|
||||
const profileUid = 'profileUid';
|
||||
mockSecurityExt.getCurrentUser.mockImplementationOnce(() =>
|
||||
mockAuthenticatedUser({ profile_uid: profileUid })
|
||||
|
@ -434,14 +448,16 @@ describe('SavedObjectsRepository Security Extension', () => {
|
|||
namespace,
|
||||
});
|
||||
expect(response.created_by).toBe(profileUid);
|
||||
expect(response.updated_by).toBe(profileUid);
|
||||
});
|
||||
|
||||
test(`keeps created_by empty if the current user is not available`, async () => {
|
||||
test(`keeps created_by, updated_by empty if the current user is not available`, async () => {
|
||||
mockSecurityExt.getCurrentUser.mockImplementationOnce(() => null);
|
||||
const response = await repository.create(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, attributes, {
|
||||
namespace,
|
||||
});
|
||||
expect(response).not.toHaveProperty('created_by');
|
||||
expect(response).not.toHaveProperty('updated_by');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1345,7 +1361,7 @@ describe('SavedObjectsRepository Security Extension', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test(`adds created_by to the saved object when the current user is available`, async () => {
|
||||
test(`adds created_by, updated_by to the saved object when the current user is available`, async () => {
|
||||
const profileUid = 'profileUid';
|
||||
mockSecurityExt.getCurrentUser.mockImplementationOnce(() =>
|
||||
mockAuthenticatedUser({ profile_uid: profileUid })
|
||||
|
@ -1353,13 +1369,19 @@ describe('SavedObjectsRepository Security Extension', () => {
|
|||
const response = await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
|
||||
expect(response.saved_objects[0].created_by).toBe(profileUid);
|
||||
expect(response.saved_objects[1].created_by).toBe(profileUid);
|
||||
|
||||
expect(response.saved_objects[0].updated_by).toBe(profileUid);
|
||||
expect(response.saved_objects[1].updated_by).toBe(profileUid);
|
||||
});
|
||||
|
||||
test(`keeps created_by empty if the current user is not available`, async () => {
|
||||
test(`keeps created_by, updated_by empty if the current user is not available`, async () => {
|
||||
mockSecurityExt.getCurrentUser.mockImplementationOnce(() => null);
|
||||
const response = await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
|
||||
expect(response.saved_objects[0]).not.toHaveProperty('created_by');
|
||||
expect(response.saved_objects[1]).not.toHaveProperty('created_by');
|
||||
|
||||
expect(response.saved_objects[0]).not.toHaveProperty('updated_by');
|
||||
expect(response.saved_objects[1]).not.toHaveProperty('updated_by');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1512,6 +1534,19 @@ describe('SavedObjectsRepository Security Extension', () => {
|
|||
expect(typeMap).toBe(authMap);
|
||||
});
|
||||
});
|
||||
|
||||
test(`adds updated_by to the saved object when the current user is available`, async () => {
|
||||
const profileUid = 'profileUid';
|
||||
mockSecurityExt.getCurrentUser.mockImplementationOnce(() =>
|
||||
mockAuthenticatedUser({ profile_uid: profileUid })
|
||||
);
|
||||
|
||||
const objects = [obj1, obj2];
|
||||
const result = await bulkUpdateSuccess(client, repository, registry, objects, { namespace });
|
||||
|
||||
expect(result.saved_objects[0].updated_by).toBe(profileUid);
|
||||
expect(result.saved_objects[1].updated_by).toBe(profileUid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkDelete', () => {
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('getRootFields', () => {
|
|||
"typeMigrationVersion",
|
||||
"managed",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"originId",
|
||||
|
|
|
@ -16,6 +16,7 @@ const ROOT_FIELDS = [
|
|||
'typeMigrationVersion',
|
||||
'managed',
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'originId',
|
||||
|
|
|
@ -297,6 +297,18 @@ describe('#rawToSavedObject', () => {
|
|||
expect(actual).toHaveProperty('updated_at', now);
|
||||
});
|
||||
|
||||
test('if specified it copies the _source.updated_by property to updated_by', () => {
|
||||
const updatedBy = 'elastic';
|
||||
const actual = singleNamespaceSerializer.rawToSavedObject({
|
||||
_id: 'foo:bar',
|
||||
_source: {
|
||||
type: 'foo',
|
||||
updated_by: updatedBy,
|
||||
},
|
||||
});
|
||||
expect(actual).toHaveProperty('updated_by', updatedBy);
|
||||
});
|
||||
|
||||
test('if specified it copies the _source.created_at property to created_at', () => {
|
||||
const now = Date();
|
||||
const actual = singleNamespaceSerializer.rawToSavedObject({
|
||||
|
@ -341,6 +353,16 @@ describe('#rawToSavedObject', () => {
|
|||
expect(actual).not.toHaveProperty('created_at');
|
||||
});
|
||||
|
||||
test(`if _source.updated_by is unspecified it doesn't set updated_by`, () => {
|
||||
const actual = singleNamespaceSerializer.rawToSavedObject({
|
||||
_id: 'foo:bar',
|
||||
_source: {
|
||||
type: 'foo',
|
||||
},
|
||||
});
|
||||
expect(actual).not.toHaveProperty('updated_by');
|
||||
});
|
||||
|
||||
test('if specified it copies the _source.originId property to originId', () => {
|
||||
const originId = 'baz';
|
||||
const actual = singleNamespaceSerializer.rawToSavedObject({
|
||||
|
|
|
@ -122,6 +122,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
|
|||
...(coreMigrationVersion && { coreMigrationVersion }),
|
||||
...(typeMigrationVersion != null ? { typeMigrationVersion } : {}),
|
||||
...(_source.updated_at && { updated_at: _source.updated_at }),
|
||||
...(_source.updated_by && { updated_by: _source.updated_by }),
|
||||
...(_source.created_at && { created_at: _source.created_at }),
|
||||
...(_source.created_by && { created_by: _source.created_by }),
|
||||
...(version && { version }),
|
||||
|
@ -144,6 +145,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
|
|||
migrationVersion,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
updated_at,
|
||||
updated_by: updatedBy,
|
||||
created_at: createdAt,
|
||||
created_by: createdBy,
|
||||
version,
|
||||
|
@ -164,6 +166,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer {
|
|||
...(coreMigrationVersion && { coreMigrationVersion }),
|
||||
...(typeMigrationVersion != null ? { typeMigrationVersion } : {}),
|
||||
...(updated_at && { updated_at }),
|
||||
...(updatedBy && { updated_by: updatedBy }),
|
||||
...(createdAt && { created_at: createdAt }),
|
||||
...(createdBy && { created_by: createdBy }),
|
||||
};
|
||||
|
|
|
@ -36,6 +36,7 @@ const baseSchema = schema.object<SavedObjectSanitizedDocSchema>({
|
|||
coreMigrationVersion: schema.maybe(schema.string()),
|
||||
typeMigrationVersion: schema.maybe(schema.string()),
|
||||
updated_at: schema.maybe(schema.string()),
|
||||
updated_by: schema.maybe(schema.string()),
|
||||
created_at: schema.maybe(schema.string()),
|
||||
created_by: schema.maybe(schema.string()),
|
||||
version: schema.maybe(schema.string()),
|
||||
|
|
|
@ -33,6 +33,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
|
|||
public error: SavedObjectType<T>['error'];
|
||||
public references: SavedObjectType<T>['references'];
|
||||
public updatedAt: SavedObjectType<T>['updated_at'];
|
||||
public updatedBy: SavedObjectType<T>['updated_by'];
|
||||
public createdAt: SavedObjectType<T>['created_at'];
|
||||
public createdBy: SavedObjectType<T>['created_by'];
|
||||
public namespaces: SavedObjectType<T>['namespaces'];
|
||||
|
@ -52,6 +53,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
|
|||
managed,
|
||||
namespaces,
|
||||
updated_at: updatedAt,
|
||||
updated_by: updatedBy,
|
||||
created_at: createdAt,
|
||||
created_by: createdBy,
|
||||
}: SavedObjectType<T>
|
||||
|
@ -69,6 +71,7 @@ export class SimpleSavedObjectImpl<T = unknown> implements SimpleSavedObject<T>
|
|||
this.updatedAt = updatedAt;
|
||||
this.createdAt = createdAt;
|
||||
this.createdBy = createdBy;
|
||||
this.updatedBy = updatedBy;
|
||||
if (error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ const createSimpleSavedObjectMock = (
|
|||
error: savedObject.error,
|
||||
references: savedObject.references,
|
||||
updatedAt: savedObject.updated_at,
|
||||
updatedBy: savedObject.updated_by,
|
||||
createdAt: savedObject.created_at,
|
||||
createdBy: savedObject.created_by,
|
||||
namespaces: savedObject.namespaces,
|
||||
|
|
|
@ -76,6 +76,8 @@ export interface SavedObject<T = unknown> {
|
|||
created_by?: string;
|
||||
/** Timestamp of the last time this document had been updated. */
|
||||
updated_at?: string;
|
||||
/** The ID of the user who last updated this object. */
|
||||
updated_by?: string;
|
||||
/** Error associated with this object, populated if an operation failed for this object. */
|
||||
error?: SavedObjectError;
|
||||
/** The data for a Saved Object is stored as an object in the `attributes` property. **/
|
||||
|
|
|
@ -62,6 +62,9 @@ Object {
|
|||
"updated_at": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"updated_by": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -54,6 +54,9 @@ Object {
|
|||
"updated_at": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"updated_by": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
@ -129,6 +132,9 @@ Object {
|
|||
"updated_at": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"updated_by": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -99,6 +99,9 @@ describe('getBaseMappings', () => {
|
|||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
updated_by: {
|
||||
type: 'keyword',
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
},
|
||||
|
|
|
@ -62,6 +62,9 @@ export function getBaseMappings(): IndexMapping {
|
|||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
updated_by: {
|
||||
type: 'keyword',
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
},
|
||||
|
|
|
@ -111,6 +111,7 @@ export interface SavedObjectDoc<T = unknown> {
|
|||
typeMigrationVersion?: string;
|
||||
version?: string;
|
||||
updated_at?: string;
|
||||
updated_by?: string;
|
||||
created_at?: string;
|
||||
created_by?: string;
|
||||
originId?: string;
|
||||
|
|
|
@ -64,6 +64,7 @@ function savedObjectToItem<Attributes extends object>(
|
|||
id,
|
||||
type,
|
||||
updated_at: updatedAt,
|
||||
updated_by: updatedBy,
|
||||
created_at: createdAt,
|
||||
created_by: createdBy,
|
||||
attributes,
|
||||
|
@ -78,6 +79,7 @@ function savedObjectToItem<Attributes extends object>(
|
|||
id,
|
||||
type,
|
||||
managed,
|
||||
updatedBy,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
createdBy,
|
||||
|
|
|
@ -202,6 +202,7 @@ export interface SOWithMetadata<Attributes extends object = object> {
|
|||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
createdBy?: string;
|
||||
updatedBy?: string;
|
||||
error?: {
|
||||
error: string;
|
||||
message: string;
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
setupInteractiveUser,
|
||||
sampleDashboard,
|
||||
cleanupInteractiveUser,
|
||||
LoginAsInteractiveUserResponse,
|
||||
} from './helpers';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
|
@ -32,11 +33,11 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
describe('for interactive user', function () {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
let sessionHeaders: { [key: string]: string } = {};
|
||||
let interactiveUser: LoginAsInteractiveUserResponse;
|
||||
|
||||
before(async () => {
|
||||
await setupInteractiveUser({ getService });
|
||||
sessionHeaders = await loginAsInteractiveUser({ getService });
|
||||
interactiveUser = await loginAsInteractiveUser({ getService });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -46,13 +47,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
it('created_by is with profile_id', async () => {
|
||||
const createResponse = await supertest
|
||||
.post('/api/content_management/rpc/create')
|
||||
.set(sessionHeaders)
|
||||
.set(interactiveUser.headers)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(sampleDashboard);
|
||||
|
||||
expect(createResponse.status).to.be(200);
|
||||
expect(createResponse.body.result.result.item).to.be.ok();
|
||||
expect(createResponse.body.result.result.item).to.have.key('createdBy');
|
||||
expect(createResponse.body.result.result.item.createdBy).to.be(interactiveUser.uid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,10 +21,11 @@ export const sampleDashboard = {
|
|||
version: 2,
|
||||
};
|
||||
|
||||
const usernameOrRole = 'content_manager_dashboard';
|
||||
const role = 'content_manager_dashboard';
|
||||
const users = ['content_manager_dashboard_1', 'content_manager_dashboard_2'] as const;
|
||||
export async function setupInteractiveUser({ getService }: Pick<FtrProviderContext, 'getService'>) {
|
||||
const security = getService('security');
|
||||
await security.role.create(usernameOrRole, {
|
||||
await security.role.create(role, {
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana: [
|
||||
{
|
||||
|
@ -35,27 +36,38 @@ export async function setupInteractiveUser({ getService }: Pick<FtrProviderConte
|
|||
],
|
||||
});
|
||||
|
||||
await security.user.create(usernameOrRole, {
|
||||
password: usernameOrRole,
|
||||
roles: [usernameOrRole],
|
||||
full_name: usernameOrRole.toUpperCase(),
|
||||
email: `${usernameOrRole}@elastic.co`,
|
||||
});
|
||||
for (const user of users) {
|
||||
await security.user.create(user, {
|
||||
password: user,
|
||||
roles: [role],
|
||||
full_name: user.toUpperCase(),
|
||||
email: `${user}@elastic.co`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupInteractiveUser({
|
||||
getService,
|
||||
}: Pick<FtrProviderContext, 'getService'>) {
|
||||
const security = getService('security');
|
||||
await security.user.delete(usernameOrRole);
|
||||
await security.role.delete(usernameOrRole);
|
||||
for (const user of users) {
|
||||
await security.user.delete(user);
|
||||
}
|
||||
await security.role.delete(role);
|
||||
}
|
||||
|
||||
export interface LoginAsInteractiveUserResponse {
|
||||
headers: {
|
||||
Cookie: string;
|
||||
};
|
||||
uid: string;
|
||||
}
|
||||
export async function loginAsInteractiveUser({
|
||||
getService,
|
||||
}: Pick<FtrProviderContext, 'getService'>): Promise<{
|
||||
Cookie: string;
|
||||
}> {
|
||||
username = users[0],
|
||||
}: Pick<FtrProviderContext, 'getService'> & {
|
||||
username?: typeof users[number];
|
||||
}): Promise<LoginAsInteractiveUserResponse> {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
|
||||
const response = await supertest
|
||||
|
@ -65,10 +77,15 @@ export async function loginAsInteractiveUser({
|
|||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: { username: usernameOrRole, password: usernameOrRole },
|
||||
params: { username, password: username },
|
||||
})
|
||||
.expect(200);
|
||||
const cookie = parseCookie(response.header['set-cookie'][0])!.cookieString();
|
||||
|
||||
return { Cookie: cookie };
|
||||
const { body: userWithProfileId } = await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
return { headers: { Cookie: cookie }, uid: userWithProfileId.profile_uid };
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
||||
describe('content management', function () {
|
||||
loadTestFile(require.resolve('./created_by'));
|
||||
loadTestFile(require.resolve('./updated_by'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import {
|
||||
loginAsInteractiveUser,
|
||||
setupInteractiveUser,
|
||||
sampleDashboard,
|
||||
cleanupInteractiveUser,
|
||||
LoginAsInteractiveUserResponse,
|
||||
} from './helpers';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
describe('updated_by', function () {
|
||||
describe('for not interactive user', function () {
|
||||
const supertest = getService('supertest');
|
||||
it('updated_by is empty', async () => {
|
||||
const createResponse = await supertest
|
||||
.post('/api/content_management/rpc/create')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(sampleDashboard);
|
||||
|
||||
expect(createResponse.status).to.be(200);
|
||||
expect(createResponse.body.result.result.item).to.be.ok();
|
||||
expect(createResponse.body.result.result.item).to.not.have.key('updatedBy');
|
||||
|
||||
const updateResponse = await supertest
|
||||
.post('/api/content_management/rpc/update')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
contentTypeId: sampleDashboard.contentTypeId,
|
||||
version: sampleDashboard.version,
|
||||
options: {
|
||||
references: [],
|
||||
mergeAttributes: false,
|
||||
},
|
||||
id: createResponse.body.result.result.item.id,
|
||||
data: {
|
||||
title: 'updated title',
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateResponse.status).to.be(200);
|
||||
expect(updateResponse.body.result.result.item).to.be.ok();
|
||||
|
||||
const getResponse = await supertest
|
||||
.post('/api/content_management/rpc/get')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
id: createResponse.body.result.result.item.id,
|
||||
contentTypeId: sampleDashboard.contentTypeId,
|
||||
version: sampleDashboard.version,
|
||||
});
|
||||
|
||||
expect(getResponse.status).to.be(200);
|
||||
expect(getResponse.body.result.result.item).to.be.ok();
|
||||
expect(getResponse.body.result.result.item).to.not.have.key('updatedBy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('for interactive user', function () {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const supertestWithAuth = getService('supertest');
|
||||
let interactiveUser: LoginAsInteractiveUserResponse;
|
||||
let createResponse: any;
|
||||
|
||||
before(async () => {
|
||||
await setupInteractiveUser({ getService });
|
||||
interactiveUser = await loginAsInteractiveUser({ getService });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
createResponse = await supertestWithoutAuth
|
||||
.post('/api/content_management/rpc/create')
|
||||
.set(interactiveUser.headers)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(sampleDashboard);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanupInteractiveUser({ getService });
|
||||
});
|
||||
|
||||
it('updated_by is with profile_id', async () => {
|
||||
expect(createResponse.status).to.be(200);
|
||||
expect(createResponse.body.result.result.item).to.be.ok();
|
||||
expect(createResponse.body.result.result.item).to.have.key('updatedBy');
|
||||
expect(createResponse.body.result.result.item.updatedBy).to.be(interactiveUser.uid);
|
||||
});
|
||||
|
||||
it('updated_by is empty after update with non interactive user', async () => {
|
||||
const updateResponse = await supertestWithAuth
|
||||
.post('/api/content_management/rpc/update')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
contentTypeId: sampleDashboard.contentTypeId,
|
||||
version: sampleDashboard.version,
|
||||
options: {
|
||||
references: [],
|
||||
mergeAttributes: false,
|
||||
},
|
||||
id: createResponse.body.result.result.item.id,
|
||||
data: {
|
||||
title: 'updated title',
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateResponse.status).to.be(200);
|
||||
|
||||
const getResponse = await supertestWithAuth
|
||||
.post('/api/content_management/rpc/get')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
id: createResponse.body.result.result.item.id,
|
||||
contentTypeId: sampleDashboard.contentTypeId,
|
||||
version: sampleDashboard.version,
|
||||
});
|
||||
|
||||
expect(getResponse.status).to.be(200);
|
||||
expect(getResponse.body.result.result.item).to.be.ok();
|
||||
|
||||
const createdObject = createResponse.body.result.result.item;
|
||||
const updatedObject = getResponse.body.result.result.item;
|
||||
|
||||
expect(updatedObject).to.not.have.key('updatedBy');
|
||||
expect(updatedObject.createdBy).to.eql(createdObject.createdBy);
|
||||
expect(updatedObject.createdAt).to.eql(createdObject.createdAt);
|
||||
expect(updatedObject.updatedAt).to.be.greaterThan(createdObject.updatedAt);
|
||||
});
|
||||
|
||||
it('updated_by is with profile_id of another user after update', async () => {
|
||||
const interactiveUser2 = await loginAsInteractiveUser({
|
||||
getService,
|
||||
username: 'content_manager_dashboard_2',
|
||||
});
|
||||
|
||||
const updateResponse = await supertestWithoutAuth
|
||||
.post('/api/content_management/rpc/update')
|
||||
.set(interactiveUser2.headers)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
contentTypeId: sampleDashboard.contentTypeId,
|
||||
version: sampleDashboard.version,
|
||||
options: {
|
||||
references: [],
|
||||
mergeAttributes: false,
|
||||
},
|
||||
id: createResponse.body.result.result.item.id,
|
||||
data: {
|
||||
title: 'updated title',
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateResponse.status).to.be(200);
|
||||
|
||||
const getResponse = await supertestWithAuth
|
||||
.post('/api/content_management/rpc/get')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
id: createResponse.body.result.result.item.id,
|
||||
contentTypeId: sampleDashboard.contentTypeId,
|
||||
version: sampleDashboard.version,
|
||||
});
|
||||
|
||||
expect(getResponse.status).to.be(200);
|
||||
expect(getResponse.body.result.result.item).to.be.ok();
|
||||
|
||||
const createdObject = createResponse.body.result.result.item;
|
||||
const updatedObject = getResponse.body.result.result.item;
|
||||
|
||||
expect(updatedObject).to.have.key('updatedBy');
|
||||
expect(updatedObject.updatedBy).to.not.eql(createdObject.updatedBy);
|
||||
expect(updatedObject.createdBy).to.eql(interactiveUser.uid);
|
||||
expect(updatedObject.updatedBy).to.eql(interactiveUser2.uid);
|
||||
expect(updatedObject.createdAt).to.eql(createdObject.createdAt);
|
||||
expect(updatedObject.updatedAt).to.be.greaterThan(createdObject.updatedAt);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { loginAsInteractiveUser, LoginAsInteractiveUserResponse } from '../helpers';
|
||||
import { TEST_CASES } from '../../common/suites/create';
|
||||
import { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
describe('bulk_create', function () {
|
||||
let interactiveUser: LoginAsInteractiveUserResponse;
|
||||
|
||||
before(async () => {
|
||||
interactiveUser = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
});
|
||||
});
|
||||
|
||||
it('created_by/updated_by is with profile_id', async () => {
|
||||
const soType = TEST_CASES.NEW_SINGLE_NAMESPACE_OBJ.type;
|
||||
const createResponse = await supertest
|
||||
.post(`/api/saved_objects/_bulk_create`)
|
||||
.set(interactiveUser.headers)
|
||||
.send([
|
||||
{ type: soType, attributes: { title: 'test' } },
|
||||
{ type: soType, attributes: { title: 'test' } },
|
||||
]);
|
||||
|
||||
expect(createResponse.status).to.be(200);
|
||||
const [so1, so2] = createResponse.body.saved_objects;
|
||||
expect(so1.created_by).to.be(interactiveUser.uid);
|
||||
expect(so1.updated_by).to.be(interactiveUser.uid);
|
||||
expect(so2.created_by).to.be(interactiveUser.uid);
|
||||
expect(so2.updated_by).to.be(interactiveUser.uid);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { loginAsInteractiveUser } from '../helpers';
|
||||
import { TEST_CASES } from '../../common/suites/create';
|
||||
import { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('bulk_update', function () {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
|
||||
);
|
||||
});
|
||||
|
||||
it('updates updated_by with profile_id, created_by is untouched', async () => {
|
||||
const { type, id } = TEST_CASES.SINGLE_NAMESPACE_DEFAULT_SPACE;
|
||||
|
||||
// update with interactive user 1
|
||||
const interactiveUser1 = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
});
|
||||
const updateResponse1 = await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.set(interactiveUser1.headers)
|
||||
.send([{ id, type, attributes: { title: 'test' } }]);
|
||||
|
||||
expect(updateResponse1.status).to.be(200);
|
||||
expect(updateResponse1.body.saved_objects[0].updated_by).to.be(interactiveUser1.uid);
|
||||
expect(updateResponse1.body.saved_objects[0].created_by).not.to.be.ok();
|
||||
|
||||
const getResponse1 = await supertest
|
||||
.get(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser1.headers);
|
||||
|
||||
expect(getResponse1.body.updated_by).to.be(interactiveUser1.uid);
|
||||
expect(getResponse1.body.created_by).not.to.be.ok();
|
||||
|
||||
// update with interactive user 2
|
||||
const interactiveUser2 = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
});
|
||||
|
||||
const updateResponse2 = await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.set(interactiveUser2.headers)
|
||||
.send([{ type, id, attributes: { title: 'test 2' } }]);
|
||||
|
||||
expect(updateResponse2.status).to.be(200);
|
||||
expect(updateResponse2.body.saved_objects[0].updated_by).to.be(interactiveUser2.uid);
|
||||
expect(updateResponse2.body.saved_objects[0].created_by).not.to.be.ok();
|
||||
|
||||
const getResponse2 = await supertest
|
||||
.get(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser2.headers);
|
||||
|
||||
expect(getResponse2.body.updated_by).to.be(interactiveUser2.uid);
|
||||
expect(getResponse2.body.created_by).not.to.be.ok();
|
||||
|
||||
// update with "non-interactive" user, updated_by should become empty
|
||||
const updateResponse3 = await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.auth(AUTHENTICATION.KIBANA_RBAC_USER.username, AUTHENTICATION.KIBANA_RBAC_USER.password)
|
||||
.send([{ type, id, attributes: { title: 'test 3' } }]);
|
||||
|
||||
expect(updateResponse3.status).to.be(200);
|
||||
expect(updateResponse3.body.saved_objects[0].updated_by).not.to.be.ok();
|
||||
expect(updateResponse3.body.saved_objects[0].created_by).not.to.be.ok();
|
||||
|
||||
const getResponse3 = await supertest
|
||||
.get(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser2.headers);
|
||||
|
||||
expect(getResponse3.body.updated_by).not.to.be.ok();
|
||||
expect(getResponse3.body.created_by).not.to.be.ok();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { loginAsInteractiveUser, LoginAsInteractiveUserResponse } from '../helpers';
|
||||
import { TEST_CASES } from '../../common/suites/create';
|
||||
import { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
describe('create', function () {
|
||||
let interactiveUser: LoginAsInteractiveUserResponse;
|
||||
|
||||
before(async () => {
|
||||
interactiveUser = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
});
|
||||
});
|
||||
|
||||
it('created_by/updated_by is with profile_id', async () => {
|
||||
const soType = TEST_CASES.NEW_SINGLE_NAMESPACE_OBJ.type;
|
||||
const createResponse = await supertest
|
||||
.post(`/api/saved_objects/${soType}`)
|
||||
.set(interactiveUser.headers)
|
||||
.send({ attributes: { title: 'test' } });
|
||||
|
||||
expect(createResponse.status).to.be(200);
|
||||
const so = createResponse.body;
|
||||
expect(so.created_by).to.be(interactiveUser.uid);
|
||||
expect(so.updated_by).to.be(interactiveUser.uid);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createUsersAndRoles } from '../../common/lib/create_users_and_roles';
|
||||
|
||||
export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('saved objects user profiles integration', function () {
|
||||
before(async () => {
|
||||
await createUsersAndRoles(es, supertest);
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./bulk_create'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { loginAsInteractiveUser } from '../helpers';
|
||||
import { TEST_CASES } from '../../common/suites/create';
|
||||
import { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('update', function () {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
|
||||
);
|
||||
});
|
||||
|
||||
it('updates updated_by with profile_id, created_by is untouched', async () => {
|
||||
const { type, id } = TEST_CASES.SINGLE_NAMESPACE_DEFAULT_SPACE;
|
||||
|
||||
// update with interactive user 1
|
||||
const interactiveUser1 = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
});
|
||||
const updateResponse1 = await supertest
|
||||
.put(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser1.headers)
|
||||
.send({ attributes: { title: 'test' } });
|
||||
|
||||
expect(updateResponse1.status).to.be(200);
|
||||
expect(updateResponse1.body.updated_by).to.be(interactiveUser1.uid);
|
||||
expect(updateResponse1.body.created_by).not.to.be.ok();
|
||||
|
||||
const getResponse1 = await supertest
|
||||
.get(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser1.headers);
|
||||
|
||||
expect(getResponse1.body.updated_by).to.be(interactiveUser1.uid);
|
||||
expect(getResponse1.body.created_by).not.to.be.ok();
|
||||
|
||||
// update with interactive user 2
|
||||
const interactiveUser2 = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
});
|
||||
|
||||
const updateResponse2 = await supertest
|
||||
.put(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser2.headers)
|
||||
.send({ attributes: { title: 'test 2' } });
|
||||
|
||||
expect(updateResponse2.status).to.be(200);
|
||||
expect(updateResponse2.body.updated_by).to.be(interactiveUser2.uid);
|
||||
expect(updateResponse2.body.created_by).not.to.be.ok();
|
||||
|
||||
const getResponse2 = await supertest
|
||||
.get(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser2.headers);
|
||||
|
||||
expect(getResponse2.body.updated_by).to.be(interactiveUser2.uid);
|
||||
expect(getResponse2.body.created_by).not.to.be.ok();
|
||||
|
||||
// update with "non-interactive" user, updated_by should become empty
|
||||
const updateResponse3 = await supertest
|
||||
.put(`/api/saved_objects/${type}/${id}`)
|
||||
.auth(AUTHENTICATION.KIBANA_RBAC_USER.username, AUTHENTICATION.KIBANA_RBAC_USER.password)
|
||||
.send({ attributes: { title: 'test 3' } });
|
||||
|
||||
expect(updateResponse3.status).to.be(200);
|
||||
expect(updateResponse3.body.updated_by).not.to.be.ok();
|
||||
expect(updateResponse3.body.created_by).not.to.be.ok();
|
||||
|
||||
const getResponse3 = await supertest
|
||||
.get(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser2.headers);
|
||||
|
||||
expect(getResponse3.body.updated_by).not.to.be.ok();
|
||||
expect(getResponse3.body.created_by).not.to.be.ok();
|
||||
});
|
||||
|
||||
it('upsert sets created_by and updated_by', async () => {
|
||||
const { type } = TEST_CASES.SINGLE_NAMESPACE_DEFAULT_SPACE;
|
||||
const id = `some-new-id-${Date.now()}`;
|
||||
|
||||
// upsert with interactive user 1
|
||||
const interactiveUser1 = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
});
|
||||
const upsertResponse = await supertest
|
||||
.put(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser1.headers)
|
||||
.send({ attributes: { title: 'updated' }, upsert: { title: 'upserted' } });
|
||||
|
||||
expect(upsertResponse.status).to.be(200);
|
||||
expect(upsertResponse.body.attributes.title).to.be('upserted');
|
||||
expect(upsertResponse.body.updated_by).to.be(interactiveUser1.uid);
|
||||
expect(upsertResponse.body.created_by).to.be(interactiveUser1.uid);
|
||||
|
||||
// update with interactive user 2
|
||||
const interactiveUser2 = await loginAsInteractiveUser({
|
||||
getService,
|
||||
...AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
});
|
||||
|
||||
const updateResponse = await supertest
|
||||
.put(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser2.headers)
|
||||
.send({ attributes: { title: 'updated' }, upsert: { title: 'upserted' } });
|
||||
|
||||
expect(updateResponse.status).to.be(200);
|
||||
expect(updateResponse.body.attributes.title).to.be('updated');
|
||||
expect(updateResponse.body.updated_by).to.be(interactiveUser2.uid);
|
||||
expect(updateResponse.body.created_by).not.to.be.ok();
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(`/api/saved_objects/${type}/${id}`)
|
||||
.set(interactiveUser2.headers);
|
||||
expect(getResponse.body.updated_by).to.be(interactiveUser2.uid);
|
||||
expect(getResponse.body.created_by).to.be(interactiveUser1.uid);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createTestConfig } from '../common/config';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default createTestConfig('user_profiles', { license: 'basic' });
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { parse as parseCookie } from 'tough-cookie';
|
||||
import { FtrProviderContext } from '../common/ftr_provider_context';
|
||||
|
||||
export interface LoginAsInteractiveUserResponse {
|
||||
headers: {
|
||||
Cookie: string;
|
||||
};
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export async function loginAsInteractiveUser({
|
||||
getService,
|
||||
username,
|
||||
password,
|
||||
}: Pick<FtrProviderContext, 'getService'> & {
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<LoginAsInteractiveUserResponse> {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
|
||||
const response = await supertest
|
||||
.post('/internal/security/login')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
const cookie = parseCookie(response.header['set-cookie'][0])!.cookieString();
|
||||
|
||||
const { body: userWithProfileId } = await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
return { headers: { Cookie: cookie }, uid: userWithProfileId.profile_uid };
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue