Skip to content

[RFC] Refined index authorization (replacement for do_not_fail_on_forbidden) #3905

@nibix

Description

@nibix

Background

The do_not_fail_on_forbidden setting switches globally between two different modes how OpenSearch reacts to cases when users have no privileges for indices.

By default, OpenSearch will resolve any patterns specified in action requests against all indices on the cluster. If then the current user does not have access privileges for any of these indices, OpenSearch will yield a 403 error.

If do_not_fail_on_forbidden is enabled, the behavior is changed: The resolution is limited to indices to which the user has access. Thus, the operation will succeed.

A very common example for the difference is a _search request to all indices (GET /_search, or GET /*/_search). For users which have not complete read-access to all indices on the cluster, this request will always fail with a 403 error if do_not_fail_on_forbidden disabled. If do_not_fail_on_forbidden is enabled, these users will get the search result limited to the indices they have access to.

Historically, the do_not_fail_on_forbidden mode was implemented to make working in Dashboards more smooth. However, this is a very minimal implementation. This leads to confusion regarding the behavior of operations which are not supported (cf #1815, #2257, #2113).

Additionally, the OpenSearch documentation is quite sparse on the option. All documentation regarding the option is located in sections related to multi-tenancy (cf https://opensearch.org/docs/latest/security/multi-tenancy/multi-tenancy-config/). It just states:

If true, the security plugin removes any content that a user is not allowed to see from search results. If false, the plugin returns a security exception. Default is false.

This is not really helpful because:

  • The documentation does not describe use-cases for this option and does not explain scenarios in which it should be enabled and in which it should be disabled.
  • Even though it is only mentioned in the multi-tenancy chapters of the documentation, the setting might be also useful for users without multi-tenancy enabled, or even for users which do not utilize Dashboards.
  • There are nuances in the behavior which are left unclear. This pertains for example to behavior when using aggregations or when referring to concrete indices (like GET /_search/index_i_have_no_privileges_for).

To make matters more complicated, there is also the do_not_fail_on_forbidden_empty setting which is undocumented. If it is false, _search operations will fail with a 403 error if the given index expression matches no indices the user has access to. If it is true, _search operations will yield an empty result set if the given index expression matches no indices the user has access to.

One reason why no recommendations are given how these settings shall be used, might be that it is actually quite difficult to make good recommendation. The reason is that there are competing pro/con arguments for these settings:

  • Pro do_not_fail_on_forbidden:
    • Enables the use of GET /_search, GET /_cat/indices and similar operations for all users.
    • A user does not have to think about indices they do not have access to.
    • The presence of the security plugin gets transparent to applications like Dashboards which expect to be able to issue requests to * indices.
  • Con do_not_fail_on_forbidden:
    • Having a fail-fast mechanism like do_not_fail_on_forbidden: false can be helpful to verify that a user actually has all permissions to indices they are supposed to. The operation of do_not_fail_on_forbidden: true might conceal issues in the privileges configuration.
    • If do_not_fail_on_forbidden and do_not_fail_on_forbidden_empty is true, a search request on any concrete index (like GET /_search/index_i_have_no_privileges_for) will result in an empty result set, if the user does not have privileges for that index. That might be counter-intuitive.
    • If do_not_fail_on_forbidden_empty is false, a similar issue still exists if a user performs a search containing an existing index and a non-existing index like GET /existing_index,idexx_with_typo/_search. The index idexx_with_typo will be silently dropped without giving the user a notice of the typo.

Despite these pros and cons, a quick research over the OpenSearch forums provides the impression that it is usually recommended to activate the setting:

Path to a improved solution

To improve the situation, one should seek for a solution which fulfills the following requirements:

  • The solution has well-defined and well-documented semantics
  • The behavior matches user expectations
  • Applications like Dashboards work well with out-of-the-box default settings
  • User errors like typos shall not be concealed

Additional desirable properties are:

  • Reduced number of configuration options. Any configuration option has the following disadvantage:
    • It increases the need for documentation.
    • It increases the need for consideration by the user and thus the chance that a user needs support.
    • It increases the amount of code.
    • It increases the amount of tests and thus development round trip time.
  • A generic implementation which does not need significant special cases

Looking at the pro/con list above, a possible synthesis for these could be moving the choice for the desired mode from a global option to the request level. Thus, a user could choose for any request which behavior seems to be the most fitting.

Existing OpenSearch options

Coincidentally, OpenSearch already provides request-level index options which are supported for most operations: https://github.com/opensearch-project/OpenSearch/blob/main/server/src/main/java/org/opensearch/action/support/IndicesOptions.java

One of these options is ignore_unavailable which is documented as follows:

Specifies whether to include missing or closed indexes in the response. Default is false.

The behavior which is controlled by this option seems to be at least similar to the behavior of do_not_fail_on_forbidden.

Another one of these options is allow_no_indices:

Whether to ignore wildcards that don’t match any indexes. Default is true.

The behavior which is controlled by this option seems to be at least similar to the behavior of do_not_fail_on_forbidden_empty.

One might consider whether it is possible to re-use these options for a replacement for do_not_fail_on_forbidden.

Proposed solution

I propose that the security plugin re-uses the existing index options ignore_unavailable and allow_no_indices to provide a request-level replacement for controlling the behavior in case a user requests indices they do not have privileges for. (Thus, the behavior control which is currently supplied by the global configuration options do_not_fail_on_forbidden and do_not_fail_on_forbidden_empty).

The options do_not_fail_on_forbidden and do_not_fail_on_forbidden_empty shall be put on a deprecation path and eventually removed.

The security plugin should follow the current semantics of the OpenSearch index resolution algorithm which implements ignore_unavailable and allow_no_indices (cf https://github.com/opensearch-project/OpenSearch/blob/main/server/src/main/java/org/opensearch/cluster/metadata/IndexNameExpressionResolver.java )

Specification of behavior

This specification follows the behavior currently implemented in the class IndexNameExpressionResolver.java.

The behavior depends on two factors:

  • The index expression in the considered action request specified concrete index names or uses patterns
  • The index options of the considered action request

The specified behavior applies for all actions which operate on index name expressions. These actions are:

  • search
  • any action wrapping search such as msearch, search templates, etc.
  • delete by query
  • update by query
  • count
  • diverse cat action
  • index level actions except create index

The specified behavior explicitly not applies for:

  • get
  • mget
  • delete document by id
  • update document by id

Concrete index name, ignore_unavailable=false, allow_no_indices=true

An index request references one or more indices by their concrete names. The index option ignore_unavailable has its default value false. The index option allow_no_indices has its default value true.

Examples: GET /index1/_search, GET /index1,index2/_search

  • If the user has privileges for the specified indices, the operation shall succeed
  • If the user does not have privileges for any of the specified indices, the operation shall fail with an error 403 or 404 (see open questions below).

Concrete index name, ignore_unavailable=true, allow_no_indices=true

An index request references one or more indices by their concrete names. The index option ignore_unavailable has the value true. The index option allow_no_indices has its default value true.

Examples: GET /index1/_search?ignore_unavailable=true, GET /index1,index2/_search?ignore_unavailable=true

  • If the user has privileges for the specified indices, the operation shall succeed
  • If the user has privileges only for a subset of the specified indices, the operation shall proceed limited to the indices with privileges
  • If the user does not have privileges for any of the specified indices, the operation shall yield an empty result set.

Concrete index name, ignore_unavailable=true, allow_no_indices=false

An index request references one or more indices by their concrete names. The index option ignore_unavailable has the value true. The index option allow_no_indices has the value false.

Examples: GET /index1/_search?ignore_unavailable=true&allow_no_indices=false, GET /index1,index2/_search?ignore_unavailable=true&allow_no_indices=false

  • If the user has privileges for the specified indices, the operation shall succeed
  • If the user has privileges only for a subset of the specified indices, the operation shall proceed limited to the indices with privileges
  • If the user does not have privileges for any of the specified indices, the operation shall fail with an error 403 or 404 (see open questions below).

Index pattern, allow_no_indices=true

An index request references indices by a pattern. The index option allow_no_indices has its default value true. The index option ignore_unavailable is disregarded when index patterns are used.

Examples: GET /index*/_search

  • If the user has privileges for all indices matched by the specified patterns, the operation shall succeed
  • If the user has privileges only for a subset of the indices matched by the specified patterns, the operation shall proceed limited to the indices with privileges.
  • If the user does not have privileges for any of indices matched by the specified patterns, the operation shall yield an empty result set.

Index pattern, allow_no_indices=false

An index request references indices by a pattern. The index option allow_no_indices has the value false. The index option ignore_unavailable is disregarded when index patterns are used.

Examples: GET /index*/_search?allow_no_indices=false

  • If the user has privileges for all indices matched by the specified patterns, the operation shall succeed
  • If the user has privileges only for a subset of the indices matched by the specified patterns, the operation shall proceed limited to the indices with privileges.
  • If the user does not have privileges for any of the specified indices, the operation shall fail with an error 403 or 404 (see open questions below).

Mixture of concrete index names and patterns

An index request contains both concrete index names and patterns.

Examples: GET /one_index,other_indices*/_search

In case of mixed concrete index names and patterns, the aforementioned rules are combined. Failures caused by ignore_unavailable=false on concrete index names take precedence. This means:

  • If the user has privileges for all indices matched by the specified expression, the operation shall succeed
  • If ignore_unavailable is false, and there are concrete index names without privileges, the request shall fail with 403 or 404.
  • Otherwise, if the user has privileges for a subset of the matched indices, the operation shall proceed limited to the indices with privileges.
  • If allow_no_indices is false and no indices with privileges are present, the request shall fail with 403 or 404.
  • Otherwise, an empty result set shall be returned

Transition path

This RFC specifies a breaking change. The specified behavior is different to either of the currently configurable behaviors of the security plugin. Is such a breaking change feasible? How could a transition path look like?

Open questions

This sections lists questions which might require further discussion.

  • Shall operations which fail due to missing index privileges fail with an error 403 or 404? More precisely, shall the security plugin expose the fact to the user that privileges are missing - or shall it pretend to the user that the index does not exist?
  • Are there any other factors to be considered?

Complications

This section lists cases where special consideration might be necessary when exploring the details of the implementation

  • Some actions implement custom index resolution algorithms. Examples for such actions are actions operating on data streams. The security plugin might also require special considerations for these.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttriagedIssues labeled as 'Triaged' have been reviewed and are deemed actionable.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions