This proposal has seen few edits of designs, please read through comments to understand the latest approach.
Updated as of: 28-Aug-2025
[HLD] Resource Sharing & Access Control in OpenSearch
1) Overview
Starting in OpenSearch v3.2.0 , the Resource Sharing & Access Control framework enables document-level authorization for plugin-defined resources .
Key highlights:
1:1 backing sharing indices per resource index (clean ownership & isolation).
Automatic evaluation via ResourceAccessEvaluator (no plugin in-house verification via filter_by_backend_role).
Dashboards-native APIs to fetch, update, and list sharing info.
Migration API to move legacy per-plugin sharing into security-owned indices.
Action-groups defined by plugins will be the ones available for sharing.
Feature flag (experimental, default: false):
plugins.security.experimental.resource_sharing.enabled : true
2) Motivation
Problems: index-scoped RBAC can’t restrict specific documents; plugins built custom auth; no central UX for sharing.
Solution: standardize sharing with:
Backing sharing index per resource index
Central evaluation in Security Plugin
First-class Dashboards APIs & UI for access management
3) Architecture & Components
3.1 Architecture (with Dashboards flows)
flowchart TD
U[User / OpenSearch Dashboards] -->|Create/Read/Update/Delete| PL[Resource Plugin]
subgraph Security[Security Plugin]
API[Security REST Endpoints - Dashboards Share & List-Accessible]
RAE[ResourceAccessEvaluator - automatic evaluation]
MAP[Index - SharingIndex Mapping]
end
subgraph Data[System Indices - Per Plugin]
RIDX1[(Resource Index A)]
RSIDX1[(Sharing Index A)]
RIDX2[(Resource Index B)]
RSIDX2[(Sharing Index B)]
end
PL --> RIDX1
PL --> RIDX2
%% Auto-eval for resource requests
PL --> RAE
RAE --> MAP
MAP -->|resolve| RSIDX1
MAP -->|resolve| RSIDX2
%% Dashboards Access Mgmt flows
U -->|Share UI GET/PUT/PATCH| API
API --> MAP
MAP --> RSIDX1 & RSIDX2
Loading
4) Data Model (Per-Plugin Backing Sharing Index)
Each resource index has a backing sharing index storing the sharing document:
{
"resource_id" : " resource-123" ,
"created_by" : {
"user" : " abc"
},
"share_with" : {
"READ_ONLY" : {
"users" : [" user1" , " user2" ],
"roles" : [" viewer_role" ],
"backend_roles" : [" data_analyst" ]
},
"READ_WRITE" : {
"users" : [" admin_user" ],
"roles" : [" editor_role" ],
"backend_roles" : [" content_manager" ]
}
}
}
Field
Type
Description
resource_id
String
Unique document ID in the resource index
created_by
Object
{ "user": <username> }
share_with
Object
Access-level → principals (READ_ONLY, READ_WRITE)
Mapping: Resource index → Sharing index (convention):
.plugins-ml-model-group → .plugins-ml-model-group-sharing
ad-detectors → ad-detectors-sharing
Note: The relationship between entries in this index and the resource metadata stored in plugin’s index is many-to-one.
Access scope is defined in 3 ways:
public: A shared_with entry exists, and contains users entry mapped to * .
restricted: A shared_with entry exists, and contains users entry not mapped to * .
private: A shared_with entry is empty or doesn’t exist.
Here are 3 examples of entries in resource_sharing index, one for each scope.
Private:
{
"resource_id" : " <doc_id>" ,
"created_by" : {
"user" : " darshit"
}
}
Restricted:
{
"resource_id" : " <doc_id>" ,
"created_by" : {
"user" : " darshit"
},
"share_with" : {
"read_only" : {
"users" : [
" derek"
],
"roles" : [],
"backend_roles" : []
},
"read_write" : {
"users" : [
" craig"
],
"roles" : [],
"backend_roles" : []
}
}
}
Public:
{
"resource_id" : " <doc_id>" ,
"created_by" : {
"user" : " darshit"
},
"share_with" : {
"read_only" : {
"users" : [" *" ],
"roles" : [" *" ],
"backend_roles" : [" *" ]
},
"read_write" : {
"users" : [
" *"
],
"roles" : [" *" ],
"backend_roles" : [" *" ]
}
}
}
NOTE: Each user, role and backend_role must be individually added as there is no pattern matching at present by design.
5) Automatic Access Evaluation
All DocRequest paths are intercepted by the Security Filter and evaluated by ResourceAccessEvaluator .
5.1 Evaluation flow
sequenceDiagram
participant User
participant Plugin as Resource Plugin
participant SecFilter as Security Filter
participant RAE as ResourceAccessEvaluator
participant Map as Index→SharingIndex Mapping
participant RSIDX as Backing Sharing Index
User->>Plugin: Request (e.g., GET /resource/{id})
Plugin->>SecFilter: Routed through security
SecFilter->>RAE: Evaluate (user, roles, backend_roles, action)
RAE->>Map: Resolve sharing index for resource_type
Map-->>RAE: sharing_index_name
RAE->>RSIDX: Fetch sharing doc by resource_id
RSIDX-->>RAE: share_with + created_by
RAE->>RAE: Check required access level
alt Allowed
RAE-->>SecFilter: ALLOW
SecFilter-->>Plugin: Proceed
Plugin-->>User: 200 OK
else Denied
RAE-->>SecFilter: DENY
SecFilter-->>Plugin: 403 Forbidden
Plugin-->>User: 403
end
Loading
6) Dashboards APIs (Access Management UI)
Base path (security dashboards endpoints):
/_plugins/_security/api/resource/share
6.1 Fetch Sharing Info (Dashboards)
GET /_plugins/_security/api/resource/share?resource_id=<id>&resource_type=<idx>
Response:
{
"sharing_info" : {
"resource_id" : " resource-123" ,
"created_by" : { "user" : " abc" },
"share_with" : {
"READ_ONLY" : { "users" : [" alice" ], "roles" : [], "backend_roles" : [] },
"READ_WRITE" : { "users" : [" bob" ] }
}
}
}
6.2 Replace Sharing (PUT)
PUT /_plugins/_security/api/resource/share
Body:
{
"resource_id" : " resource-123" ,
"resource_type" : " sample-resource-index" ,
"share_with" : {
"READ_ONLY" : { "users" : [" alice" ] },
"READ_WRITE" : { "users" : [" bob" ] }
}
}
6.3 Patch Sharing (non-destructive)
PATCH /_plugins/_security/api/resource/share
Body (example):
{
"resource_id" : " resource-123" ,
"resource_type" : " sample-resource-index" ,
"add" : {
"READ_ONLY" : { "users" : [" charlie" ] }
"FULL_ACCESS" : { "users" : [" alice" ] }
},
"revoke" : {
"READ_ONLY" : { "users" : [" alice" ] },
"READ_WRITE" : { "users" : [" bob" ] }
}
}
6.4 List Available Resource Types
(available as drop-down in UI to fetch corresponding sharing records)
GET /_plugins/_security/api/resource/types
As user alice, above http request would return following response:
Response:
{
"types" : [
{
"type" : " org.opensearch.sample.SampleResource" ,
"index" : " .sample_resource" ,
"action_groups" : [" sample_read_only" , " sample_read_write" , " sample_full_access" ]
}
]
}
6.5 List Accessible Resources (for UI tables)
GET /_plugins/_security/api/resource/list?resource_type=<idx>
As user alice, above http request would return following response:
Response:
{
"resources" : [
{
"resource_id" : " resource-123" ,
"created_by" : {
"user" : " abc"
},
"share_with" : {
"READ_ONLY" : {
"users" : [
" alice"
],
"roles" : [],
"backend_roles" : []
}
},
"can_share" : false
},
{
"resource_id" : " resource-456" ,
"created_by" : {
"user" : " def"
},
"share_with" : {
"FULL_ACCESS" : {
"users" : [
" alice"
]
}
}
"can_share" : true
}
]
}
6.5 Dashboards flows (integrated)
sequenceDiagram
participant UI as Dashboards UI (Access Mgmt)
participant API as Security Dashboards Share API
participant Core as Security Plugin Core
participant Map as Index→SharingIndex Mapping
participant RSIDX as Backing Sharing Index
rect rgb(120,120,120)
UI->>API: GET /share?resource_id&resource_type
API->>Core: Authz (owner/admin/share-manager)
Core->>Map: Resolve sharing index
Map-->>Core: sharing_index_name
Core->>RSIDX: GET sharing doc
RSIDX-->>Core: sharing_info
Core-->>UI: 200 (sharing_info)
end
rect rgb(120,120,120)
UI->>API: PUT/PATCH /share
API->>Core: Validate + authorize
Core->>Map: Resolve sharing index
Map-->>Core: sharing_index_name
Core->>RSIDX: Upsert / Patch sharing doc
RSIDX-->>Core: ack (updated doc)
Core-->>UI: 200 (updated sharing_info)
end
rect rgb(120,120,120)
UI->>API: GET /share/accessible?resource_type&from&size
API->>Core: Compute principals (user/roles/backend_roles)
Core->>RSIDX: Query share_with contains principals
RSIDX-->>Core: [resource_ids...], total
Core-->>UI: 200 {resource_ids, total, page}
end
Loading
7) Plugin Developer Requirements
Mark resource indices as system indices (prevents direct access).
Implement ResourceSharingExtension and register via:
src/main/resources/META-INF/services/org.opensearch.security.spi.ResourceSharingExtension
Declare action-groups per resource in resource-action-groups.yml file:
src/main/resources/resource-action-groups.yml
Depend on opensearch-security-spi (compileOnly) and extend security plugin (optional).
Rely on ResourceAccessEvaluator via Security Filter for access verification. Do not call verifyAccess unless a hierarchical evaluation is needed.
Use client methods where applicable (share, revoke, getAccessibleResourceIds) for non-filter flows (e.g., management tasks).
8) Migration API (to per-index backing sharing)
POST /_plugins/_security/api/resources/migrate
Request (example):
{
"source_index" : " sample-plugin-index" ,
"username_path" : " /owner" ,
"backend_roles_path" : " /access/backend_roles" ,
"default_access_level" : " READ_ONLY"
}
Summary response:
{
"summary" : " Migration complete. migrated 10; skippedNoUser 2; failed 1" ,
"skippedResources" : [" doc-17" , " doc-22" ]
}
Migration flow
sequenceDiagram
participant Admin as Cluster Admin
participant MAPI as Migration API
participant Core as Security Plugin (Migration)
participant Src as Resource Index
participant RSIDX as Backing Sharing Index
Admin->>MAPI: POST /resources/migrate {...}
MAPI->>Core: Validate + resolve backing sharing index
Core->>Src: Scroll documents
loop each doc
Core->>RSIDX: Upsert sharing doc<br>{resource_id, created_by.user,share_with[default].backend_roles}
end
RSIDX-->>Core: Stats
Core-->>Admin: 200 OK (summary)
Loading
9) Security Rules & Best Practices
Keep system index protection ON to prevent direct index access.
Least privilege : share only required principals; avoid wildcards unless necessary.
Owners & super-admins retain full control irrespective of config.
Ensure consistent resource_id handling between the resource index and sharing index.
10) End-to-End Lifecycle (happy path)
sequenceDiagram
participant Dev as Plugin Dev
participant Plugin as Resource Plugin
participant Sec as Security Plugin
participant UI as Dashboards
participant User as End User
Dev->>Plugin: Implement ResourceSharingExtension + DocRequest
Plugin->>Sec: Register (SPI) on startup
User->>Plugin: Create resource (doc in resource index)
UI->>Sec: PUT/PATCH /share (configure share_with)
Sec->>Sec: Write to backing sharing index
User->>Plugin: Read/Search resource
Plugin->>Sec: (Security Filter) → ResourceAccessEvaluator
Sec-->>Plugin: ALLOW / DENY
Plugin-->>User: Response (200 / 403)
Loading
11) License
Apache 2.0 — © OpenSearch Contributors.
Visual representations: #4500 (comment)
Old designs: #4500 (comment)
This proposal has seen few edits of designs, please read through comments to understand the latest approach.
Updated as of: 28-Aug-2025
[HLD] Resource Sharing & Access Control in OpenSearch
1) Overview
Starting in OpenSearch v3.2.0, the Resource Sharing & Access Control framework enables document-level authorization for plugin-defined resources.
Key highlights:
filter_by_backend_role).Feature flag (experimental, default: false):
2) Motivation
Problems: index-scoped RBAC can’t restrict specific documents; plugins built custom auth; no central UX for sharing.
Solution: standardize sharing with:
3) Architecture & Components
Resource Plugins define resources (stored in system indices) and register a
ResourceSharingExtension.Security Plugin:
3.1 Architecture (with Dashboards flows)
flowchart TD U[User / OpenSearch Dashboards] -->|Create/Read/Update/Delete| PL[Resource Plugin] subgraph Security[Security Plugin] API[Security REST Endpoints - Dashboards Share & List-Accessible] RAE[ResourceAccessEvaluator - automatic evaluation] MAP[Index - SharingIndex Mapping] end subgraph Data[System Indices - Per Plugin] RIDX1[(Resource Index A)] RSIDX1[(Sharing Index A)] RIDX2[(Resource Index B)] RSIDX2[(Sharing Index B)] end PL --> RIDX1 PL --> RIDX2 %% Auto-eval for resource requests PL --> RAE RAE --> MAP MAP -->|resolve| RSIDX1 MAP -->|resolve| RSIDX2 %% Dashboards Access Mgmt flows U -->|Share UI GET/PUT/PATCH| API API --> MAP MAP --> RSIDX1 & RSIDX24) Data Model (Per-Plugin Backing Sharing Index)
Each resource index has a backing sharing index storing the sharing document:
{ "resource_id": "resource-123", "created_by": { "user": "abc" }, "share_with": { "READ_ONLY": { "users": ["user1", "user2"], "roles": ["viewer_role"], "backend_roles": ["data_analyst"] }, "READ_WRITE": { "users": ["admin_user"], "roles": ["editor_role"], "backend_roles": ["content_manager"] } } }resource_idcreated_by{ "user": <username> }share_withREAD_ONLY,READ_WRITE)Mapping: Resource index → Sharing index (convention):
.plugins-ml-model-group→.plugins-ml-model-group-sharingad-detectors→ad-detectors-sharingNote: The relationship between entries in this index and the resource metadata stored in plugin’s index is many-to-one.
Access scope is defined in 3 ways:
public: A shared_with entry exists, and contains users entry mapped to * .restricted: A shared_with entry exists, and contains users entry not mapped to * .private: A shared_with entry is empty or doesn’t exist.Here are 3 examples of entries in resource_sharing index, one for each scope.
{ "resource_id": "<doc_id>", "created_by": { "user": "darshit" } }{ "resource_id": "<doc_id>", "created_by": { "user": "darshit" }, "share_with": { "read_only": { "users": [ "derek" ], "roles": [], "backend_roles": [] }, "read_write": { "users": [ "craig" ], "roles": [], "backend_roles": [] } } }{ "resource_id": "<doc_id>", "created_by": { "user": "darshit" }, "share_with": { "read_only": { "users": ["*"], "roles": ["*"], "backend_roles": ["*"] }, "read_write": { "users": [ "*" ], "roles": ["*"], "backend_roles": ["*"] } } }NOTE: Each user, role and backend_role must be individually added as there is no pattern matching at present by design.
5) Automatic Access Evaluation
All DocRequest paths are intercepted by the Security Filter and evaluated by ResourceAccessEvaluator.
5.1 Evaluation flow
sequenceDiagram participant User participant Plugin as Resource Plugin participant SecFilter as Security Filter participant RAE as ResourceAccessEvaluator participant Map as Index→SharingIndex Mapping participant RSIDX as Backing Sharing Index User->>Plugin: Request (e.g., GET /resource/{id}) Plugin->>SecFilter: Routed through security SecFilter->>RAE: Evaluate (user, roles, backend_roles, action) RAE->>Map: Resolve sharing index for resource_type Map-->>RAE: sharing_index_name RAE->>RSIDX: Fetch sharing doc by resource_id RSIDX-->>RAE: share_with + created_by RAE->>RAE: Check required access level alt Allowed RAE-->>SecFilter: ALLOW SecFilter-->>Plugin: Proceed Plugin-->>User: 200 OK else Denied RAE-->>SecFilter: DENY SecFilter-->>Plugin: 403 Forbidden Plugin-->>User: 403 end6) Dashboards APIs (Access Management UI)
Base path (security dashboards endpoints):
6.1 Fetch Sharing Info (Dashboards)
Response:
{ "sharing_info": { "resource_id": "resource-123", "created_by": { "user": "abc" }, "share_with": { "READ_ONLY": { "users": ["alice"], "roles": [], "backend_roles": [] }, "READ_WRITE": { "users": ["bob"] } } } }6.2 Replace Sharing (PUT)
Body:
{ "resource_id": "resource-123", "resource_type": "sample-resource-index", "share_with": { "READ_ONLY": { "users": ["alice"] }, "READ_WRITE": { "users": ["bob"] } } }6.3 Patch Sharing (non-destructive)
Body (example):
{ "resource_id": "resource-123", "resource_type": "sample-resource-index", "add": { "READ_ONLY": { "users": ["charlie"] } "FULL_ACCESS": { "users": ["alice"] } }, "revoke": { "READ_ONLY": { "users": ["alice"] }, "READ_WRITE": { "users": ["bob"] } } }6.4 List Available Resource Types
(available as drop-down in UI to fetch corresponding sharing records)
As user
alice, above http request would return following response:Response:
{ "types": [ { "type": "org.opensearch.sample.SampleResource", "index": ".sample_resource", "action_groups": ["sample_read_only", "sample_read_write", "sample_full_access"] } ] }6.5 List Accessible Resources (for UI tables)
As user
alice, above http request would return following response:Response:
{ "resources": [ { "resource_id": "resource-123", "created_by": { "user": "abc" }, "share_with": { "READ_ONLY": { "users": [ "alice" ], "roles": [], "backend_roles": [] } }, "can_share": false }, { "resource_id": "resource-456", "created_by": { "user": "def" }, "share_with": { "FULL_ACCESS": { "users": [ "alice" ] } } "can_share": true } ] }6.5 Dashboards flows (integrated)
sequenceDiagram participant UI as Dashboards UI (Access Mgmt) participant API as Security Dashboards Share API participant Core as Security Plugin Core participant Map as Index→SharingIndex Mapping participant RSIDX as Backing Sharing Index rect rgb(120,120,120) UI->>API: GET /share?resource_id&resource_type API->>Core: Authz (owner/admin/share-manager) Core->>Map: Resolve sharing index Map-->>Core: sharing_index_name Core->>RSIDX: GET sharing doc RSIDX-->>Core: sharing_info Core-->>UI: 200 (sharing_info) end rect rgb(120,120,120) UI->>API: PUT/PATCH /share API->>Core: Validate + authorize Core->>Map: Resolve sharing index Map-->>Core: sharing_index_name Core->>RSIDX: Upsert / Patch sharing doc RSIDX-->>Core: ack (updated doc) Core-->>UI: 200 (updated sharing_info) end rect rgb(120,120,120) UI->>API: GET /share/accessible?resource_type&from&size API->>Core: Compute principals (user/roles/backend_roles) Core->>RSIDX: Query share_with contains principals RSIDX-->>Core: [resource_ids...], total Core-->>UI: 200 {resource_ids, total, page} end7) Plugin Developer Requirements
Mark resource indices as system indices (prevents direct access).
Implement
ResourceSharingExtensionand register via:Declare action-groups per resource in
resource-action-groups.ymlfile:Depend on
opensearch-security-spi(compileOnly) and extend security plugin (optional).Rely on ResourceAccessEvaluator via Security Filter for access verification. Do not call
verifyAccessunless a hierarchical evaluation is needed.Use client methods where applicable (
share,revoke,getAccessibleResourceIds) for non-filter flows (e.g., management tasks).8) Migration API (to per-index backing sharing)
Request (example):
{ "source_index": "sample-plugin-index", "username_path": "/owner", "backend_roles_path": "/access/backend_roles", "default_access_level": "READ_ONLY" }Summary response:
{ "summary": "Migration complete. migrated 10; skippedNoUser 2; failed 1", "skippedResources": ["doc-17", "doc-22"] }Migration flow
sequenceDiagram participant Admin as Cluster Admin participant MAPI as Migration API participant Core as Security Plugin (Migration) participant Src as Resource Index participant RSIDX as Backing Sharing Index Admin->>MAPI: POST /resources/migrate {...} MAPI->>Core: Validate + resolve backing sharing index Core->>Src: Scroll documents loop each doc Core->>RSIDX: Upsert sharing doc<br>{resource_id, created_by.user,share_with[default].backend_roles} end RSIDX-->>Core: Stats Core-->>Admin: 200 OK (summary)9) Security Rules & Best Practices
10) End-to-End Lifecycle (happy path)
sequenceDiagram participant Dev as Plugin Dev participant Plugin as Resource Plugin participant Sec as Security Plugin participant UI as Dashboards participant User as End User Dev->>Plugin: Implement ResourceSharingExtension + DocRequest Plugin->>Sec: Register (SPI) on startup User->>Plugin: Create resource (doc in resource index) UI->>Sec: PUT/PATCH /share (configure share_with) Sec->>Sec: Write to backing sharing index User->>Plugin: Read/Search resource Plugin->>Sec: (Security Filter) → ResourceAccessEvaluator Sec-->>Plugin: ALLOW / DENY Plugin-->>User: Response (200 / 403)11) License
Apache 2.0 — © OpenSearch Contributors.
Visual representations: #4500 (comment)
Old designs: #4500 (comment)