[Write restricted SOs] Allow creation, update, delete and transfer ownership#224411
Conversation
…dashboards-crud-api
x-pack/platform/packages/private/security/authorization_core/src/privileges/privileges.ts
Outdated
Show resolved
Hide resolved
|
@elasticmachine merge upstream |
…dashboards-crud-api
jeramysoucy
left a comment
There was a problem hiding this comment.
Thanks for your patience with this, Sid. I commented with a suggestion to add a test case, but this can be added in one of the follow-up PRs.
src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.ts
Show resolved
Hide resolved
| ); | ||
| expect(client.create).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Add 'allows creating an object supporting access control with no access control metadata when there is no active user profile and no access mode is provided'
| const nameAttribute = registry.getNameAttribute(type); | ||
|
|
||
| const savedObjectResponse = await client.get<SavedObjectsRawDocSource>( | ||
| { | ||
| index: commonHelper.getIndexForType(type), | ||
| id: serializer.generateRawId(namespace, type, id), | ||
| _source_includes: [ | ||
| ...SavedObjectsUtils.getIncludedNameFields(type, nameAttribute), | ||
| 'accessControl', | ||
| ], | ||
| }, | ||
| { ignore: [404], meta: true } | ||
| ); | ||
|
|
||
| const saveObject = { attributes: savedObjectResponse.body._source?.[type] }; | ||
| const name = securityExtension.includeSavedObjectNames() | ||
| ? SavedObjectsUtils.getName(nameAttribute, saveObject) | ||
| : undefined; |
There was a problem hiding this comment.
++ I think this is simpler than what was there before, and changes the existing code less.
There was a problem hiding this comment.
It might make sense to just make sure the change access control function is being called correctly. If you agree, can we take care of this in the follow-up PR to restructure the tests?
src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.ts
Show resolved
Hide resolved
src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete.ts
Outdated
Show resolved
Hide resolved
...kages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts
Show resolved
Hide resolved
...s/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_access_control.ts
Outdated
Show resolved
Hide resolved
TinaHeiligers
left a comment
There was a problem hiding this comment.
Approving to unblock further work, as long as the follow up tasks are handled soon.
Anything saved object related's far more complex than one might think and it takes stamina to get a PR over the finish line. Great work @SiddharthMantri !
...s/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_access_control.ts
Outdated
Show resolved
Hide resolved
...s/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_access_control.ts
Outdated
Show resolved
Hide resolved
...kages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts
Outdated
Show resolved
Hide resolved
Thanks, Tina. We've merged a config feature flag already to gate this. We have the immediate follow-up PRs that are all staged and nearly ready: |
jloleysens
left a comment
There was a problem hiding this comment.
This is looking good @SiddharthMantri !
I'm leaving a "request changes" because I'd like to get your thoughts on the set of questions (mostly nits!) about the Core code before proceeding. I intend to also test this behaviour and read through the security code so part 2 is incoming!
Cheers!
|
|
||
| it('returns error if no read-only objects are specified', async () => { | ||
| const params = setup({ | ||
| objects: [{ type: NON_READ_ONLY_TYPE, id: 'id-1' }], |
There was a problem hiding this comment.
nit: perhaps worth specifying a set objects in some test (not just one) and checking the output
|
|
||
| // Regular expression to validate user profile IDs as generated by Elasticsearch | ||
| // User profile IDs are expected to be in the format "u_<principal>_<version>" | ||
| const USER_PROFILE_REGEX = /^u_.+_.+$/; |
There was a problem hiding this comment.
This regexp will match:
/^u_.+_.+$/.test('u_asd_asd_asd');
// => trueIs asd_asd a valid principle name?
...ges/saved-objects/api-server-internal/src/lib/apis/internals/change_object_access_control.ts
Outdated
Show resolved
Hide resolved
...ges/saved-objects/api-server-internal/src/lib/apis/internals/change_object_access_control.ts
Show resolved
Hide resolved
...ges/saved-objects/api-server-internal/src/lib/apis/internals/change_object_access_control.ts
Show resolved
Hide resolved
src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.ts
Outdated
Show resolved
Hide resolved
| @@ -111,7 +111,7 @@ describe('#delete', () => { | |||
|
|
|||
| it(`should use ES get action then delete action when using a multi-namespace type`, async () => { | |||
There was a problem hiding this comment.
Would you mind changing this test name given the .not?
|
|
||
| // UPSERT CASE START | ||
| if (shouldPerformUpsert) { | ||
| // Note: Upsert does not support adding accessControl properties. To do so, the `create` API should be used. |
There was a problem hiding this comment.
Would you mind explaining bit more on the internals of why in this comment?
There was a problem hiding this comment.
@SiddharthMantri Do you think we should just handle this case? If there is an active user profile, then we can set as the owner and use the default mode, otherwise just create the object "legacy" style with no access control?
| if (shouldPerformUpsert) { | ||
| // Note: Upsert does not support adding accessControl properties. To do so, the `create` API should be used. | ||
|
|
||
| if (registry.supportsAccessControl(type)) { |
There was a problem hiding this comment.
If this type !supportsAccessControl, should we be throwing a different error? I'd also just like to understand the rationale for this blocker a bit better, thanks!
There was a problem hiding this comment.
Only types supporting accessControl reject upserts. All other types, given that they pass other checks, should allow upsert.
| }); | ||
|
|
||
| const createSecurityExtension = (): jest.Mocked<ISavedObjectsSecurityExtension> => | ||
| lazyObject({ |
There was a problem hiding this comment.
Why did you remove lazyObject? Was it causing issues?
💔 Build Failed
Failed CI StepsTest Failures
Metrics [docs]Public APIs missing comments
Public APIs missing exports
Unknown metric groupsAPI count
History
|
jloleysens
left a comment
There was a problem hiding this comment.
I ran the code locally, thanks for addressing my feedback. Happy with the incremental approach to address issues regarding upsert. Nice work @SiddharthMantri, approving to unblock progress. We may just need @rudolf to also take re-review!
| const loginAsObjectOwner = (username: string, password: string) => login(username, password); | ||
|
|
||
| const loginAsNotObjectOwner = (username: string, password: string) => login(username, password); |
There was a problem hiding this comment.
nit: these loginAs* functions seem to be identical. Is that intentional?
There was a problem hiding this comment.
Yeah, they are redundant as such but help with readability. The idea is that when used in the test cases, it's obvious which user is performing the action.
27c9862
into
elastic:security/read-only-dashboards
Closes #221755 ## Summary Adds audit events on successful completion of changing object owner or access mode. Detailed list of audit events on issue attached above. ### Audit events added in this PR: | Audit Action | When Audited | Outcome | Bypass Conditions | |--------------|--------------|---------|-------------------| | **UPDATE_OBJECTS_OWNER** | • Failure: When access control enforcement fails • Success : When access control enforcement is granted | `'failure'` | No bypass for failure case | | **UPDATE_OBJECTS_ACCESS_MODE** | • Failure: When access control enforcement fails • Success : When access control enforcement is granted | `'failure'` | No bypass for failure case | ## Logging scenarios ### 1. AccessControl is modified by owner - Expected outcome: Auhorized - Log example: ``` {"event":{"action":"saved_object_update_objects_access_mode","category":["database"],"type":["change"],"outcome":"success"},"kibana":{"space_id":"default","session_id":"ujNfHZISIOS99ANjQxxSAjxtY+D0D0/VnGBDDIJG5IE=","saved_object":{"type":"access_control_type","id":"bdceca5c-cd1e-442e-8272-55c632398cc7"}},"user":{"id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0","name":"elastic","roles":["superuser"]},"trace":{"id":"3df30168-353c-48ee-b0ee-bb671f40d9f4"},"client":{"ip":"127.0.0.1"},"service":{"node":{"roles":["background_tasks","ui"]}},"ecs":{"version":"9.0.0"},"@timestamp":"2025-10-29T17:26:38.099+01:00","message":"User has updated access mode of access_control_type [id=bdceca5c-cd1e-442e-8272-55c632398cc7]","log":{"level":"INFO","logger":"plugins.security.audit.ecs"},"process":{"pid":42523,"uptime":107.077655333},"span":{"id":"2bf159f3eed6e557"}} ``` ### 2. AccessControl is modified by non-owner - Expected outcome: Unauthorized - Log example: ``` {"event":{"action":"saved_object_update_objects_access_mode","category":["database"],"type":["change"],"outcome":"failure"},"kibana":{"space_id":"default","session_id":"Mn8eOxtCD7MOGlCPR4tRdfDpsKNSTXzVfXIAmzFNNsc=","unauthorized_spaces":["default"],"unauthorized_types":["access_control_type"]},"error":{"code":"Error","message":"Unable to manage_access_control for types access_control_type"},"user":{"id":"u_EWATCHX9oIEsmcXj8aA1FkcaY3DE-XEpsiGTjrR2PmM_0","name":"test_user","roles":["editor"]},"trace":{"id":"c3497d4e-3a93-4d3b-8800-6062009f1b77"},"client":{"ip":"127.0.0.1"},"service":{"node":{"roles":["background_tasks","ui"]}},"ecs":{"version":"9.0.0"},"@timestamp":"2025-10-29T17:44:28.893+01:00","message":"Failed attempt to update access mode of saved objects","log":{"level":"INFO","logger":"plugins.security.audit.ecs"},"process":{"pid":42523,"uptime":1177.897796208},"span":{"id":"912b67ee3b28dd14"}} ``` ### 3. AccessControl is modified by admin on non-admin owned object - Expected outcome: Authorized - Logs: ``` {"event":{"action":"saved_object_update_objects_access_mode","category":["database"],"type":["change"],"outcome":"success"},"kibana":{"space_id":"default","session_id":"ujNfHZISIOS99ANjQxxSAjxtY+D0D0/VnGBDDIJG5IE=","saved_object":{"type":"access_control_type","id":"03eb9fcd-584b-4766-bfba-327a4f0f696c"}},"user":{"id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0","name":"elastic","roles":["superuser"]},"trace":{"id":"c6005951-24e4-4142-b13d-8bf101ca5468"},"client":{"ip":"127.0.0.1"},"service":{"node":{"roles":["background_tasks","ui"]}},"ecs":{"version":"9.0.0"},"@timestamp":"2025-10-29T18:09:28.253+01:00","message":"User has updated access mode of access_control_type [id=03eb9fcd-584b-4766-bfba-327a4f0f696c]","log":{"level":"INFO","logger":"plugins.security.audit.ecs"},"process":{"pid":61700,"uptime":596.898178917},"span":{"id":"b2b8a487b44915c8"}} ``` ### Steps to test using integration tests You can use the existing integration tests for access control by adding the following to the config file at: `x-pack/platform/test/spaces_api_integration/access_control_objects/config.ts` Config to add at `kbnTestServer.serverArgs` ``` ... '--xpack.security.audit.enabled=true', '--xpack.security.audit.appender.type=file', '--xpack.security.audit.appender.fileName=./kibana_audit.log', '--xpack.security.audit.appender.layout.type=json' ``` Once you run the tests as ``` no de scripts/functional_tests_server --config x-pack/platform/test/spaces_api_integration/access_control_objects/config.ts ``` You can perform the tests as described here #224411 and verify the logs in `kibana_audit.log` file generated at the root level of your kibana repository. --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
…n bulk operations (#238468) ## Summary This PR is a follow-up to #224411 (CRUD operations). It implements granular results for bulk operations performed on object types supporting access control. ### What does this mean? Traditionally, bulk operations throw if any type is not authorized for the action in the current space. With access control, we're introducing a new per-object authorization paradigm. A user may be authorized to perform an action for a type in the current space, but one of the objects requested may be in "write-restricted" mode (formerly "read-only"). Objects in "write-restricted" mode can only be overwritten, updated, or deleted if they are owned by the user performing the operation or the user is an "Admin"™️ (`*` privilege in the current space, granting the `manage_access_control` privilege). The CRUD PR implemented all operations as authorized/unauthorized based on whether the user performing the action was able to affect all requested objects or not. This means that if just one object of 100 that were being updated was in "write-restricted" mode, the operation would throw an error an no objects would be updated, even if the user has authorization to affect the other 99 objects. For import, this means rejecting creation or overwrite of all objects in a given file by throwing an error, due to the same reason. ### What this PR does, specifically This PR changes authorization enforcement for access control by granularly authorizing each individual object (of relevant types). Non-bulk operations will behave the same way - if a user is not authorized to perform the action on the object requested, the operation will throw. Bulk operations behave differently: - They will only **throw** if: - ALL of the requested access control objects are unauthorized, **OR** - If any requested types are unauthorized by RBAC (existing behavior) - They will respond with **success and status** for each object if: - Some of the requested access control objects are authorized **AND** - All of the requested types are authorized by RBAC This affects only bulk operations that authorize access control objects (with the exception of changing access control): - Bulk Create - Bulk Update - Bulk Delete ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [ ] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ### Risks There is risk of introducing regressions to authorization for SOR operations. - Severity: high - any regression in the SOR is not tolerable - Mitigation: we will ensure there are existing tests that adequately check RBAC for SO's independently of access control, and that these tests all pass - Stakeholders: Core, all consumers of SOs (response ops, fleet, etc.) --------- Co-authored-by: Sid <siddharthmantri1@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Christiane (Tina) Heiligers <christiane.heiligers@elastic.co>
…228416) Closes #221754 ## Summary This PR implements changes to support import and export of saved objects that support access control features - namely, "write-restricted" dashboards (formerly "read-only" dashboards). This effort is broken down into 2 phases: ### Phase 1 Import will not apply the access control metadata from the imported file. This metadata contains the owner and access mode for the object. Instead, any new objects (that support access control) created as a result of import will be owned by the importing user and will be in default mode. If there is no active user profile (import called via API), the object will be created with no access control metadata (no owner or mode), but this can be assigned later by an admin. Any objects being overwritten as a result of import will maintain existing ownership and mode, but this can be changed later by the owner or an admin if needed. ### Phase 2 Import will support applying the access control metadata from the imported file, but only for "Admin" users (users with the `manage_access_control` privilege). Admins will be able to specify an option to enable this behavior. If there is no access control metadata in the file for an existing object that will be overwritten, it will maintain existing ownership and mode. **This PR handles Phase 1, and Phase 2 will be handled in a follow-up effort.** ### Feature Flag Note The changes here do not check the `savedObjects.enableAccessControl` config feature flag. The reasoning is that we still want to intercept unexpected access control metadata that may be present in export/import files and reject the import for any applicable objects. Alternatively, we could check the feature flag, and if disabled, always strip any incoming access control metadata. ## Details ### Import Import relies on existing saved object bulk create functionality, which is handled by #224411 and #238468. Prior to bulk create, the import operation filters invalid objects and responds with this information to surface in the UI. Bulk create already handles object ID clashes and authorization for overwriting access control objects. When importing objects with access control, the access control metadata from the import file will be ignored in order to preserve the owner and access mode for any existing objects being overwritten. Access control requires some new filtering capability - - In Phase 1 (this phase) objects that should not include access control metadata - In Phase 2, objects missing required parts of access control metadata (only needed when we implement support for admins to import access control metadata - phase 2) Since Phase 1 does not support importing access control metadata, this metadata will be stripped from incoming objects via another transform. Because of that, the filter transform is not critical in Phase 1, however, it implements the baseline for filtering needed in Phase 2, so it was worth staging here. To achieve the filtering and stripping, I added the ability to set "access control transforms" with the saved object service. I created an import transform factory that will create a filter stream and a map streams (the transforms). This factory is set in the saved objects service and passed to the of `SavedObjectsImporter` constructor. This loosely follows the paradigm used to set the saved object repository extensions. The filter stream handles all of the filtering of invalid objects, specific to access control, while the map stream handles removal of access control metadata (in Phase 1 for all users, in Phase 2 for non-admin users or when an admin has not specified the option to import access control metadata). The end result - if the factory is defined, the import transform streams are generated during import, and executed inline with the other filter streams during the import process. After the invalid access control objects are filtered out, we rely on the bulk create operation to handle the rest. ### Export Originally during export we had planned to strip the access control metadata, but the design was revised. Keeping access control metadata in the export files means that they will contain everything necessary for import after Phase 2. Because of this, not export transforms are currently required. We _could_ implement a transform to strip the access control metadata when the access control feature flag is disabled, but this is not necessary - the additional metadata is not harmful or affective in Phase 1. We do not expect objects to contain metadata until the feature flag is enabled, and we do not expect the flag to be toggled back and forth. ### Testing This PR include both unit and integration tests for import, export, and resolve import errors APIs. ### Risk This functionality will sit behind a feature flag (to be implemented). If the feature is disabled, the transforms will not get set with the saved object service, leaving import and export to function as they did prior to this PR - verified by the existing suites of unit and integration tests. #### Known issue If an export file contains access control metadata and is imported when the feature flag is disabled, import will surface an error for each object that contains the metadata, stating that the object contains unexpected metadata. While we do not expect this to happen, if it ever does, removing the metadata from an export file is fairly simple and mitigates the issue. #### Approach As an alternative, the transforms can be moved inline with the rest of the importer and exporter codebase without significant effort. --------- Co-authored-by: Sid <siddharthmantri1@gmail.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Christiane (Tina) Heiligers <christiane.heiligers@elastic.co>
Closes elastic/kibana-team#808 The respective teams have been raising PRs against this feature branch. Approved PRs merged so far: - #221916 - #224411 - #239973 - #241101 - #238468 - #233552 - #228416 - #241168 - #244746 - #244830 ## Summary This pull request overhauls the saved object management workflow by introducing the concept of ownership for SOs - specifically enabled for dashboards only at the moment. Owners and administrators can now control a new write-restricted flag on their objects, allowing them to keep work draft/uneditable state before publishing. This change enables users to define who can modify shared objects, providing a crucial capability to manage and share dashboards. ## Release note Kibana Dashboards now support ownership and "write_restricted" mode. Users can now keep dashboards publicly editable or in a write-restricted state until they are ready to publish, giving them more control over who can edit their dashboards, regardless of broader space permissions. ## How to test ### Serverless Please reach out to me via slack or in the project channel (#read-only-dashboards) to be invited to the serverless environment where this feature has been enabled. ### Local - Clone this PR - Enable the feature by editing kibana.yml to include ``` savedObjects.enableAccessControl: true ``` - Start ES and Kibana as you would - Once started, seed Kibana with sample data. This should create a few dashboards. - Navigate to dashboards and create a new one. - In the share modal, change the view mode `Everybody in the space Can View`, <img width="500" height="410" alt="image" src="https://github.com/user-attachments/assets/b895442f-cce3-41a6-8b47-d206a9afbf43" /> - Now create a new role which grants access to indices and dashboards all. Create a new user and then assign that role to the newly created user. <img width="500" height="410" alt="image" src="https://github.com/user-attachments/assets/dd5251e1-a3b5-41a8-abc1-7e67399d65d2" /> - Login as the new user and navigate to the dashboard you had initially set as `Can view`. You'll see that you're not able to edit the dashboard and a warning like <img width="500" height="410" alt="Screenshot 2025-11-28 at 12 30 50" src="https://github.com/user-attachments/assets/1f71ccc7-9dc6-4a68-9a2c-540aa74e4f03" /> ### Local (2nd option) You can also follow the instructions in #224411 that detail how to use the funtional test runner to test this using the test plugin created for this feature. ### Risk matrix - What happens when your feature is used in a non-default space or a custom space? Works as expected - What happens when there are multiple Kibana nodes using the same Elasticsearch cluster? Does not depend on functionality of kibana nodes - What happens when a plugin you depend on is disabled? Changes are in core and security - both are always available - What happens when a feature you depend on is disabled? No dependency - What happens when a third party integration you depend on is not responding? No third party inregration - Does the feature work in Elastic Cloud? Yes - Does the feature create a setting that needs to be exposed, or configured differently than the default, on the Elastic Cloud? No - Is there a significant performance impact that may affect Cloud Kibana instances? No - Does your feature need to be aware of running in a container? No - Does the feature Work with security disabled, or fails gracefully? If disabled, fails gracefully. - Are there performance risks associated with your feature? No - Could this cause memory to leak in either the browser or server? No - Will your feature still work if Kibana is run behind a reverse proxy? Yes - Does your feature affect other plugins? No, other plugins could choose to use it if registering a SO with ownable types - Are migrations handled gracefully? Does the feature affect old indices or saved objects? Yes, migrations taken care of. - Are you using any technologies, protocols, techniques, conventions, libraries, NPM modules, etc. that may be new or unprecedented in Kibana? No --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jeramy Soucy <jeramy.soucy@elastic.co> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Christiane (Tina) Heiligers <christiane.heiligers@elastic.co> Co-authored-by: Krzysztof Kowalczyk <krzysztof.kowalczyk@elastic.co> Co-authored-by: Gerard Soldevila <gerard.soldevila@elastic.co>
Closes elastic/kibana-team#808 The respective teams have been raising PRs against this feature branch. Approved PRs merged so far: - elastic#221916 - elastic#224411 - elastic#239973 - elastic#241101 - elastic#238468 - elastic#233552 - elastic#228416 - elastic#241168 - elastic#244746 - elastic#244830 ## Summary This pull request overhauls the saved object management workflow by introducing the concept of ownership for SOs - specifically enabled for dashboards only at the moment. Owners and administrators can now control a new write-restricted flag on their objects, allowing them to keep work draft/uneditable state before publishing. This change enables users to define who can modify shared objects, providing a crucial capability to manage and share dashboards. ## Release note Kibana Dashboards now support ownership and "write_restricted" mode. Users can now keep dashboards publicly editable or in a write-restricted state until they are ready to publish, giving them more control over who can edit their dashboards, regardless of broader space permissions. ## How to test ### Serverless Please reach out to me via slack or in the project channel (#read-only-dashboards) to be invited to the serverless environment where this feature has been enabled. ### Local - Clone this PR - Enable the feature by editing kibana.yml to include ``` savedObjects.enableAccessControl: true ``` - Start ES and Kibana as you would - Once started, seed Kibana with sample data. This should create a few dashboards. - Navigate to dashboards and create a new one. - In the share modal, change the view mode `Everybody in the space Can View`, <img width="500" height="410" alt="image" src="https://github.com/user-attachments/assets/b895442f-cce3-41a6-8b47-d206a9afbf43" /> - Now create a new role which grants access to indices and dashboards all. Create a new user and then assign that role to the newly created user. <img width="500" height="410" alt="image" src="https://github.com/user-attachments/assets/dd5251e1-a3b5-41a8-abc1-7e67399d65d2" /> - Login as the new user and navigate to the dashboard you had initially set as `Can view`. You'll see that you're not able to edit the dashboard and a warning like <img width="500" height="410" alt="Screenshot 2025-11-28 at 12 30 50" src="https://github.com/user-attachments/assets/1f71ccc7-9dc6-4a68-9a2c-540aa74e4f03" /> ### Local (2nd option) You can also follow the instructions in elastic#224411 that detail how to use the funtional test runner to test this using the test plugin created for this feature. ### Risk matrix - What happens when your feature is used in a non-default space or a custom space? Works as expected - What happens when there are multiple Kibana nodes using the same Elasticsearch cluster? Does not depend on functionality of kibana nodes - What happens when a plugin you depend on is disabled? Changes are in core and security - both are always available - What happens when a feature you depend on is disabled? No dependency - What happens when a third party integration you depend on is not responding? No third party inregration - Does the feature work in Elastic Cloud? Yes - Does the feature create a setting that needs to be exposed, or configured differently than the default, on the Elastic Cloud? No - Is there a significant performance impact that may affect Cloud Kibana instances? No - Does your feature need to be aware of running in a container? No - Does the feature Work with security disabled, or fails gracefully? If disabled, fails gracefully. - Are there performance risks associated with your feature? No - Could this cause memory to leak in either the browser or server? No - Will your feature still work if Kibana is run behind a reverse proxy? Yes - Does your feature affect other plugins? No, other plugins could choose to use it if registering a SO with ownable types - Are migrations handled gracefully? Does the feature affect old indices or saved objects? Yes, migrations taken care of. - Are you using any technologies, protocols, techniques, conventions, libraries, NPM modules, etc. that may be new or unprecedented in Kibana? No --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jeramy Soucy <jeramy.soucy@elastic.co> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Christiane (Tina) Heiligers <christiane.heiligers@elastic.co> Co-authored-by: Krzysztof Kowalczyk <krzysztof.kowalczyk@elastic.co> Co-authored-by: Gerard Soldevila <gerard.soldevila@elastic.co>
Closes #221753
Important
To support requirements, this feature is restricted to SO types that are
multipleandmultiple-isolatednamespace types only.Summary
Summarize your PR. If it involves visual changes include a screenshot or gif.
Change Ownership Endpoints: Adds server-side logic, client methods, and repository support to transfer object ownership.
Extended Create Options : Allow specifying
accessMode: 'read_only'when creating an object.Security & Authorization: Integrates with the security extension to enforce owner-based restrictions.
Mappings & Migrations: Elasticsearch mappings and migrations updated to persist
accessControl.Tests: New FTR integration tests and a dedicated test plugin for read-only object scenarios.
Ownership-Based Authorization Logic
All ownership checks are now centralized in
SavedObjectsAccessControlService.enforceAccessControl(operation, args…).The general flow is:
accessControlmetadata.createreadmodify(covers both update & delete)changeOwnershipobject.accessControl.accessMode === 'read_only', also enforceuser === object.accessControl.owner.createaccessControl.owneraccessControl.accessModepassed in create optionsreadmodifychangeOwnershipAssumptions
createinstead.forceoption works the same way.Expected behavior of SOR operations
All operations in the SOR on accessControl types follow the same rules. After the existing RBAC rules for these operations, for a user to be granted permission to perform an action on an SO Type supporting access control, they must be superuser or owner of the object.
Objects supporting access control will be created with the accessControl metadata as follows:
owner: User profile ID of creating useraccessMode:read_only|defaultbased on the incoming optionIf objects supporting access control are created with no access control metadata, then they are created just as they are today - no change to how that object type will function.
Read
No changes to read. Must have RBAC granted access for SOs.
Update
Update operations will not support changing accessControl metadata. Updates (and in turn upserts) will also not allow objects to be created as write restricted types. We throw an error and ask the consumer to use
createinstead. Updates are restricted by the access control rules stated aboveDelete
Delete operations will be allowed/disallowed based on the rules above.
Bulk update
Objects supporting access control but not owned by the current user (unless superuser) will not be updated in this operation.
Bulk delete
Bulk delete (with and without the force option) work the same way. If an object in the payload is of a type supporting access control, then the rules will be the same. We will allow delete only if the user is a superuser or owner of said object - all other instances will be rejected.
Delete by namespace
Delete by namespace is a workaround for bulk deleting all objects in a space. This function isn't exposed on the SO client but rather used on the repository which is used by the SpacesClient to be able to delete all objects in a space. For users with a role granting this permission, we will continue to allow them to do so even if there are objects owned by others in that space. This is product decision based on the Editor role being granted this privilege and if removed, would be considered a breaking change that's out of the scope of this project.
Misc:
Saved objects like dashboards that are shareable across spaces have the same rules assigned to them. A user cannot update/delete a dashboard in a space where they might have RBAC access but don't own said dashboard.
How to test
Since there's no associated UI changes in this PR, you can use the test
read only objectsplugin to test behavior of the different flows.Start the plugin in the integration test as:
Once done, you can open Kibana on
localhost:5620and set up a simple user with this role:kibana_savedobjects_editor. For your tests you can use three users:superusernon-superuserkibana_savedobjects_editorWith each of these users, you can try the following calls in Dev Tools
Creates and responds with an object owned by the creating user. Using the ID in the response, you can now perform the different operations and verify how access control works
You can run
at any time to see the current shape of the object including the access control metadata
Note for reviewers:
Follow up tasks
We have a few different follow up tasks documented on #230991
Checklist
Check the PR satisfies following conditions.
Reviewers should verify this PR satisfies this list as well.
release_note:*label is applied per the guidelinesIdentify risks
Risk matrix attached on parent PR: #224552 (comment)