Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ import {
createGenericNotFoundErrorPayload,
updateSuccess,
mockTimestampFieldsWithCreated,
ACCESS_CONTROL_TYPE,
MULTI_NAMESPACE_TYPE,
} from '../../test_helpers/repository.test.common';
import { mockAuthenticatedUser } from '@kbn/core-security-common/mocks';

describe('#update', () => {
let client: ReturnType<typeof elasticsearchClientMock.createElasticsearchClient>;
Expand Down Expand Up @@ -834,6 +837,127 @@ describe('#update', () => {
})
);
});

describe('access control', () => {
it('should define access control metadata when upserting a supporting type', async () => {
securityExtension.getCurrentUser.mockReturnValue(
mockAuthenticatedUser({ profile_uid: 'u_test_user_version' })
);

const options = { upsert: { title: 'foo', description: 'bar' } };
migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc }));
await updateSuccess(
client,
repository,
registry,
ACCESS_CONTROL_TYPE,
id,
attributes,
{
upsert: {
title: 'foo',
description: 'bar',
},
},
{
mockGetResponseAsNotFound: { found: false } as estypes.GetResponse,
}
);
await repository.update(ACCESS_CONTROL_TYPE, id, attributes, options);
expect(client.get).toHaveBeenCalledTimes(2);
const expectedType = {
accessControlType: { description: 'bar', title: 'foo' },
namespaces: ['default'],
type: 'accessControlType',
accessControl: {
accessMode: 'default',
owner: 'u_test_user_version',
},
created_by: 'u_test_user_version',
updated_by: 'u_test_user_version',
...mockTimestampFieldsWithCreated,
};
expect(
(client.create.mock.calls[0][0] as estypes.CreateRequest<SavedObjectsRawDocSource>)
.document!
).toEqual(expectedType);
});

it('should not define access control metadata when upserting a supporting type but there is no active user profile', async () => {
securityExtension.getCurrentUser.mockReturnValue(null);
const options = { upsert: { title: 'foo', description: 'bar' } };
migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc }));
await updateSuccess(
client,
repository,
registry,
ACCESS_CONTROL_TYPE,
id,
attributes,
{
upsert: {
title: 'foo',
description: 'bar',
},
},
{
mockGetResponseAsNotFound: { found: false } as estypes.GetResponse,
}
);
await repository.update(ACCESS_CONTROL_TYPE, id, attributes, options);
expect(client.get).toHaveBeenCalledTimes(2);
const expectedType = {
accessControlType: { description: 'bar', title: 'foo' },
namespaces: ['default'],
type: 'accessControlType',
...mockTimestampFieldsWithCreated,
};
expect(
(client.create.mock.calls[0][0] as estypes.CreateRequest<SavedObjectsRawDocSource>)
.document!
).toEqual(expectedType);
});

it('should not define access control metadata when upserting a non-supporting type', async () => {
securityExtension.getCurrentUser.mockReturnValue(
mockAuthenticatedUser({ profile_uid: 'u_test_user_version' })
);

const options = { upsert: { title: 'foo', description: 'bar' } };
migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc }));
await updateSuccess(
client,
repository,
registry,
MULTI_NAMESPACE_TYPE,
id,
attributes,
{
upsert: {
title: 'foo',
description: 'bar',
},
},
{
mockGetResponseAsNotFound: { found: false } as estypes.GetResponse,
}
);
await repository.update(MULTI_NAMESPACE_TYPE, id, attributes, options);
expect(client.get).toHaveBeenCalledTimes(2);
const expectedType = {
multiNamespaceType: { description: 'bar', title: 'foo' },
namespaces: ['default'],
type: 'multiNamespaceType',
created_by: 'u_test_user_version',
updated_by: 'u_test_user_version',
...mockTimestampFieldsWithCreated,
};
expect(
(client.create.mock.calls[0][0] as estypes.CreateRequest<SavedObjectsRawDocSource>)
.document!
).toEqual(expectedType);
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
encodeHitVersion,
} from '@kbn/core-saved-objects-base-server-internal';
import type {
SavedObjectAccessControl,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
} from '@kbn/core-saved-objects-api-server';
Expand All @@ -27,6 +28,7 @@ import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from '../constants';
import { isValidRequest } from '../utils';
import { getCurrentTime, getSavedObjectFromSource, mergeForUpdate } from './utils';
import type { ApiExecutionContext } from './types';
import { setAccessControl } from './utils/internal_utils';

export interface PerformUpdateParams<T = unknown> {
type: string;
Expand Down Expand Up @@ -182,13 +184,18 @@ export const executeUpdate = async <T>(

// UPSERT CASE START
if (shouldPerformUpsert) {
// Note: Upsert does not support adding accessControl properties. To do so, the `create` API should be used.

if (registry.supportsAccessControl(type)) {
throw SavedObjectsErrorHelpers.createBadRequestError(
`"update" does not support "upsert" of objects that support access control. Use "create" or "bulk create" instead.`
);
// Note: Update does not support accessControl parameters. If applicable and possible (type supports access control
// and there is an active user profile), the default access control metadata will be set.
// To explicitly set access control for a new object, the `create` API should be used.
let accessControlToWrite: SavedObjectAccessControl | undefined;
if (securityExtension) {
accessControlToWrite = setAccessControl({
typeSupportsAccessControl: registry.supportsAccessControl(type),
createdBy: updatedBy,
accessMode: 'default',
});
}

// ignore attributes if creating a new doc: only use the upsert attributes
// don't include upsert if the object already exists; ES doesn't allow upsert in combination with version properties
const migratedUpsert = migrationHelper.migrateInputDocument({
Expand All @@ -203,6 +210,7 @@ export const executeUpdate = async <T>(
updated_at: time,
...(updatedBy && { created_by: updatedBy, updated_by: updatedBy }),
...(Array.isArray(references) && { references }),
...(accessControlToWrite && { accessControl: accessControlToWrite }),
}) as SavedObjectSanitizedDoc<T>;
validationHelper.validateObjectForCreate(type, migratedUpsert);
const rawUpsert = serializer.savedObjectToRaw(migratedUpsert);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,94 @@ export default function ({ getService }: FtrProviderContext) {
expect(updateResponse.body).to.have.property('message');
expect(updateResponse.body.message).to.contain(`Unable to update ${ACCESS_CONTROL_TYPE}`);
});

it('should apply defaults when upserting a supported type', async () => {
const { cookie: objectOwnerCookie, profileUid: ownerProfileUid } = await loginAsObjectOwner(
'test_user',
'changeme'
);

const objectId = 'upserted-object-1';
const updateResponse = await supertestWithoutAuth
.put('/access_control_objects/update')
.set('kbn-xsrf', 'true')
.set('cookie', objectOwnerCookie.cookieString())
.send({ objectId, type: ACCESS_CONTROL_TYPE, upsert: true })
.expect(200);

expect(updateResponse.body.id).to.eql(objectId);
expect(updateResponse.body.attributes).to.have.property(
'description',
'updated description'
);
// get the object to verify access control metadata
const getResponse = await supertestWithoutAuth
.get(`/access_control_objects/${objectId}`)
.set('kbn-xsrf', 'true')
.set('cookie', objectOwnerCookie.cookieString())
.expect(200);
expect(getResponse.body).to.have.property('accessControl');
expect(getResponse.body.accessControl).to.have.property('owner', ownerProfileUid);
expect(getResponse.body.accessControl).to.have.property('accessMode', 'default');
});

it('should not write access control metadata when upserting unsupported types', async () => {
const { cookie: objectOwnerCookie } = await loginAsObjectOwner('test_user', 'changeme');

const objectId = 'upserted-object-2';
const updateResponse = await supertestWithoutAuth
.put('/access_control_objects/update')
.set('kbn-xsrf', 'true')
.set('cookie', objectOwnerCookie.cookieString())
.send({ objectId, type: NON_ACCESS_CONTROL_TYPE, upsert: true })
.expect(200);

expect(updateResponse.body.id).to.eql(objectId);
expect(updateResponse.body.attributes).to.have.property(
'description',
'updated description'
);
// get the object to verify access control metadata
const getResponse = await supertestWithoutAuth
.get(`/non_access_control_objects/${objectId}`)
.set('kbn-xsrf', 'true')
.set('cookie', objectOwnerCookie.cookieString())
.expect(200);
expect(getResponse.body).not.to.have.property('accessControl');
});

it('should not write access control metadata when upserting a supported type if there is no active user profile ID', async () => {
const objectId = 'upserted-object-3';
const updateResponse = await supertestWithoutAuth
.put('/access_control_objects/update')
.set('kbn-xsrf', 'true')
.set(
'Authorization',
`Basic ${Buffer.from(`${adminTestUser.username}:${adminTestUser.password}`).toString(
'base64'
)}`
)
.send({ objectId, type: ACCESS_CONTROL_TYPE, upsert: true })
.expect(200);

expect(updateResponse.body.id).to.eql(objectId);
expect(updateResponse.body.attributes).to.have.property(
'description',
'updated description'
);
// get the object to verify access control metadata
const getResponse = await supertestWithoutAuth
.get(`/access_control_objects/${objectId}`)
.set('kbn-xsrf', 'true')
.set(
'Authorization',
`Basic ${Buffer.from(`${adminTestUser.username}:${adminTestUser.password}`).toString(
'base64'
)}`
)
.expect(200);
expect(getResponse.body).not.to.have.property('accessControl');
});
});

describe('#bulk_update', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,16 +254,21 @@ export class AccessControlTestPlugin implements Plugin {
schema.literal(NON_ACCESS_CONTROL_TYPE),
]),
objectId: schema.string(),
upsert: schema.boolean({ defaultValue: false }),
}),
},
},
async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
try {
const objectType = request.body.type || ACCESS_CONTROL_TYPE;
const result = await soClient.update(objectType, request.body.objectId, {
description: 'updated description',
});
const upsert = request.body.upsert ?? false;
const result = await soClient.update(
objectType,
request.body.objectId,
{ description: 'updated description' },
{ upsert: upsert ? { description: 'updated description' } : undefined }
);

return response.ok({
body: result,
Expand Down