Skip to content

[Proposal] Resource Permissions and Sharing #4500

@DarshitChanpura

Description

@DarshitChanpura

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

  • Resource Plugins define resources (stored in system indices) and register a ResourceSharingExtension.

  • Security Plugin:

    • Maintains Index→SharingIndex mapping
    • Stores sharing docs in per-plugin backing indices
    • Runs ResourceAccessEvaluator for auto authz
    • Exposes Dashboards Share APIs and List-Accessible API

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-detectorsad-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.
  1. Private:
{
  "resource_id": "<doc_id>",
  "created_by": {
    "user": "darshit"
  }
}
  1. 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": []
    }
  }
}
  1. 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)

Metadata

Metadata

Labels

Roadmap:SecurityProject-wide roadmap labelenhancementNew feature or requestresource-permissionsLabel to track all items related to resource permissionstriagedIssues labeled as 'Triaged' have been reviewed and are deemed actionable.v3.0.0

Type

No type

Projects

Status

New

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions