Discover not possible anymore with multi tenancy and readonly users

Hello,

**Elasticsearch version: 8.7.1

**Kibana version (if relevant): 8.7.1

Describe the issue:

With Kibana 8.7.1, multi tenancy enabled, we get big trouble with tenants that have the action kibana:saved_objects/*/read (SGS_KIBANA_ALL_READ action group).

When a user with a role that have this action kibana:saved_objects/*/read is trying to do a discover, the user is getting a permission deny.

I set up my logger to debug mode and found that when a user is trying to do a discover, KIBANA need to do the action indices:data/write/bulk on .kibana indice.

SG is then verifying that the user have kibana:saved_objects/*/write in the isTenantAllowed function: search-guard/dlic-fe-multi-tenancy/src/main/java/com/floragunn/searchguard/enterprise/femt/PrivilegesInterceptorImpl.java at sg-flx-1.6.0-es-8.7.x · floragunncom/search-guard · GitHub

You can see in the logs I provided a user k006898 trying to access discover on the tenant PT_MDW_RONLY and being rejected because he does not have the kibana:saved_objects/*/write permission on this tenant.

Why do you check that the user have kibana:saved_objects/*/write permission onto a tenant instead of checking that the user have the indices:data/write/bulk permission onto the .kibana indice ? It seems like a bug.

All our readonly users can’t see the discover anymore which is a very big issue for my company.

Thanks for your reply.
Mickaël

elasticsearch/config/elasticsearch.yml

#SG
searchguard.restapi.roles_enabled: [SGS_ALL_ACCESS, xxx_admin]


searchguard.ssl.http.clientauth_mode: OPTIONAL
searchguard.ssl.transport.pemcert_filepath: xxx.crt
searchguard.ssl.transport.pemkey_filepath: xxx.key
searchguard.ssl.transport.pemtrustedcas_filepath: xx.ca
searchguard.ssl.transport.enforce_hostname_verification: false
searchguard.ssl.transport.resolve_hostname: false
searchguard.ssl.http.enabled: true
searchguard.ssl.http.pemcert_filepath: xxx.crt
searchguard.ssl.http.pemkey_filepath: xxx.key
searchguard.ssl.http.pemtrustedcas_filepath: xx.ca
searchguard.enterprise_modules_enabled: true
searchguard.nodes_dn:  ['*xxxx*']
searchguard.authcz.admin_dn: [xxxx]

/sgconfig/sg_config.yml

sg_config:
  dynamic:
    filtered_alias_mode: "warn"
    disable_rest_auth: false
    disable_intertransport_auth: false
    respect_request_indices_options: false
    license: "xxxxxxxxxxxxxxx"
    kibana:
      multitenancy_enabled: true
      server_username: "kibanaserver"
      index: ".kibana"
      rbac_enabled: false
    http:
      anonymous_auth_enabled: false
      xff:
        enabled: true
        internalProxies: ".*"
        remoteIpHeader: "X-Forwarded-For"
    authc:
      openid_auth_domain:
        http_enabled: false
        transport_enabled: false
        order: 0
        http_authenticator:
          challenge: false
          type: "openid"
          config:
            roles_key: "profile"
            openid_connect_url: "xxxxxxxxxxxxxxxxxx"
        authentication_backend:
          type: "noop"
          config: {}
        description: "Authenticate via OpenId Connect"
        skip_users: []
      jwt_auth_domain:
        http_enabled: false
        transport_enabled: false
        order: 4
        http_authenticator:
          challenge: false
          type: "jwt"
          config:
            signing_key: "base64 encoded HMAC key or public RSA/ECDSA pem key"
            jwt_header: "Authorization"
        authentication_backend:
          type: "noop"
          config: {}
        description: "Authenticate via Json Web Token"
        skip_users: []
      ldap:
        http_enabled: false
        transport_enabled: false
        order: 5
        http_authenticator:
          challenge: false
          type: "basic"
          config: {}
        authentication_backend:
          type: "ldap"
          config:
            enable_ssl: false
            enable_start_tls: false
            enable_ssl_client_auth: false
            verify_hostnames: true
            hosts:
            - "localhost:8389"
            userbase: "ou=people,dc=example,dc=com"
            usersearch: "(sAMAccountName={0})"
        description: "Authenticate via LDAP or Active Directory"
        skip_users: []
      basic_internal_auth_domain:
        http_enabled: false
        transport_enabled: false
        order: 3
        http_authenticator:
          challenge: true
          type: "basic"
          config: {}
        authentication_backend:
          type: "intern"
          config: {}
        description: "Authenticate via HTTP Basic against internal users database"
        skip_users: []
      proxy_auth_domain:
        http_enabled: true
        transport_enabled: true
        order: 0
        http_authenticator:
          challenge: false
          type: "proxy"
          config:
            user_header: "x-proxy-user"
            roles_header: "x-proxy-roles"
        authentication_backend:
          type: "noop"
          config: {}
        description: "Authenticate via proxy"
        skip_users: []
      clientcert_auth_domain:
        http_enabled: true
        transport_enabled: true
        order: 1
        http_authenticator:
          challenge: false
          type: "clientcert"
          config:
            username_attribute: "cn"
        authentication_backend:
          type: "noop"
          config: {}
        description: "Authenticate via SSL client certificates"
        skip_users: []
      kerberos_auth_domain:
        http_enabled: false
        transport_enabled: false
        order: 6
        http_authenticator:
          challenge: true
          type: "kerberos"
          config:
            krb_debug: false
            strip_realm_from_principal: true
        authentication_backend:
          type: "noop"
          config: {}
        skip_users: []
    authz:
      roles_from_another_ldap:
        http_enabled: false
        transport_enabled: false
        authorization_backend:
          type: "ldap"
          config: {}
        description: "Authorize via another Active Directory"
        skipped_users: []
      roles_from_myldap:
        http_enabled: false
        transport_enabled: false
        authorization_backend:
          type: "ldap"
          config:
            enable_ssl: false
            enable_start_tls: false
            enable_ssl_client_auth: false
            verify_hostnames: true
            hosts:
            - "localhost:8389"
            rolebase: "ou=groups,dc=example,dc=com"
            rolesearch: "(member={0})"
            userrolename: "disabled"
            rolename: "cn"
            resolve_nested_roles: true
            userbase: "ou=people,dc=example,dc=com"
            usersearch: "(uid={0})"
        description: "Authorize via LDAP or Active Directory"
        skipped_users: []
    auth_failure_listeners: {}
    do_not_fail_on_forbidden: true
    multi_rolespan_enabled: true
    hosts_resolver_mode: "ip-only"
    do_not_fail_on_forbidden_empty: false
    auth_token_provider: {}

Provide logs:

[2024-11-08T09:29:14,480][DEBUG][c.f.s.a.r.a.HttpClientCertAuthenticationFrontend] [xxx] No client cert provided
[2024-11-08T09:29:14,480][DEBUG][c.f.s.a.b.UserMapping    ] [xxx] Mapping user using attributes {credentials={user_name=n/a}, request={headers=org.elasticsearch.http.netty4.Netty4HttpRequest$HttpHeadersMap@3d6bc5b9, direct_ip_address=10.76.0.124, originating_ip_address=127.0.0.1}} for AuthCredentials [username=n/a, subUserName=null, authDomainInfo=AuthDomainInfo [authDomainId=null, authenticatorType=trusted_origin, authBackendType=null], password=null, nativeCredentials=null, backendRoles=[], searchGuardRoles=[], complete=true, authzComplete=false, redirectUri=null, attributes={}, structuredAttributes={}, claims={}, attributesForUserMapping={credentials={user_name=n/a}, request={headers=org.elasticsearch.http.netty4.Netty4HttpRequest$HttpHeadersMap@3d6bc5b9, direct_ip_address=10.76.0.124, originating_ip_address=127.0.0.1}}]
[2024-11-08T09:29:14,480][DEBUG][c.f.s.a.b.UserMapping    ] [xxx] Mapped user name: k006898
[2024-11-08T09:29:14,480][DEBUG][c.f.s.a.b.UserMapping    ] [xxx] Mapping user using attributes {credentials={user_name=n/a}, request={headers=org.elasticsearch.http.netty4.Netty4HttpRequest$HttpHeadersMap@3d6bc5b9, direct_ip_address=10.76.0.124, originating_ip_address=127.0.0.1}} for AuthCredentials [username=k006898, subUserName=null, authDomainInfo=AuthDomainInfo [authDomainId=null, authenticatorType=trusted_origin, authBackendType=noop], password=null, nativeCredentials=null, backendRoles=[], searchGuardRoles=[], complete=true, authzComplete=false, redirectUri=null, attributes={}, structuredAttributes={}, claims={}, attributesForUserMapping={credentials={user_name=n/a}, request={headers=org.elasticsearch.http.netty4.Netty4HttpRequest$HttpHeadersMap@3d6bc5b9, direct_ip_address=10.76.0.124, originating_ip_address=127.0.0.1}}]
[2024-11-08T09:29:14,480][DEBUG][c.f.s.a.b.UserMapping    ] [xxx] Mapped roles: [P_RONLY_MDW]
[2024-11-08T09:29:14,480][DEBUG][c.f.s.a.b.RequestAuthenticationProcessor] [xxx] Authentication successful for User k006898 <trusted_origin/noop> [P_RONLY_MDW]/[] {} on trusted_origin[67a55e62] using com.floragunn.searchguard.authc.rest.RestRequestAuthenticationProcessor@6a567231
requestedTenant: PT_MDW_TMP
[2024-11-08T09:29:14,481][DEBUG][c.f.s.a.PrivilegesEvaluator] [xxx] ### evaluate indices:data/write/bulk (org.elasticsearch.action.bulk.BulkRequest)
User: User k006898 <trusted_origin/noop> [backend_roles=[P_RONLY_MDW] requestedTenant=PT_MDW_TMP]
specialPrivilegesEvaluationContext: null
Resolved: local: [.kibana_8.7.1_001]; remote: []
Uresolved: [[indices=[.kibana_8.7.1], indicesOptions=IndicesOptions[ignore_unavailable=false, allow_no_indices=false, expand_wildcards_open=false, expand_wildcards_closed=false, expand_wildcards_hidden=false, allow_aliases_to_multiple_indices=false, forbid_closed_indices=true, ignore_aliases=false, ignore_throttled=false], allowsRemoteIndices=false, includeDataStreams=false, role=null]]
IgnoreUnauthorizedIndices: true
[2024-11-08T09:29:14,481][DEBUG][c.f.s.e.f.PrivilegesInterceptorImpl] [xxx] replaceKibanaIndex(indices:data/write/bulk, User k006898 <trusted_origin/noop> [backend_roles=[P_RONLY_MDW] requestedTenant=PT_MDW_TMP])
requestedResolved: local: [.kibana_8.7.1_001]; remote: []
requestedTenant: PT_MDW_TMP
[2024-11-08T09:29:14,482][DEBUG][c.f.s.e.f.PrivilegesInterceptorImpl] [xxx] IndexInfo: IndexInfo [originalName=.kibana_8.7.1, prefix=.kibana, suffix=_8.7.1, tenantInfoPart=null]
[2024-11-08T09:29:14,482][WARN ][c.f.s.e.f.PrivilegesInterceptorImpl] [xxx] Tenant PT_MDW_TMP is not allowed to write (user: k006898)
[2024-11-08T09:29:14,482][DEBUG][c.f.s.a.PrivilegesEvaluator] [xxx] Result from privileges interceptor for cluster perm: DENY
[2024-11-08T09:29:14,482][DEBUG][r.suppressed             ] [xxx] path: /_bulk, params: {require_alias=true, refresh=false}
org.elasticsearch.ElasticsearchSecurityException: Insufficient permissions
        at com.floragunn.searchguard.authz.PrivilegesEvaluationResult.toSecurityException(PrivilegesEvaluationResult.java:305) ~[?:?]

Hi @mcarlin,

Thanks for reporting it, I’ll take a look and get back to you shortly.

best,
mj

@mcarlin, how do you map these permissions to a user?

could you share the output of the following (for the role described above):

GET /_searchguard/api/roles/{rolename}

thanks,
Mantas

Hello Mantas,

We are mapping user to permissions using backend roles :

k006898 is member of “P_XXX_RONLY_MDW”

in sg_roles_mapping.yml, we have:

MDW_RO:
  reserved: false
  hidden: false
  backend_roles:
  - "P_XXX_RONLY_MDW"
  hosts: []
  users: []
  and_backend_roles: []
  description: ""

then MDW_RO role (sg_roles.yml) is configured as follows:

MDW_RO:
  description: ""
  cluster_permissions:
  - "SGS_CLUSTER_COMPOSITE_OPS_RO"
  index_permissions:
  - allowed_actions:
    - "SGS_READ"
    index_patterns:
    - "*"
    fls: []
    masked_fields: []
  tenant_permissions:
  - allowed_actions:
    - "SGS_KIBANA_ALL_READ"
    tenant_patterns:
    - "PT_MDW"
    - "SGS_GLOBAL_TENANT"
  exclude_cluster_permissions: []
  exclude_index_permissions: []

If we replace SGS_KIBANA_ALL_READ by SGS_KIBANA_ALL_WRITE it resolve the issue but give permissions to users to modify/create/delete any saved object in the tenant what we don’t want.

Here is the return of GET /_searchguard/api/roles/MDW_RO:

{"MDW_RO":{"description":"","cluster_permissions":["SGS_CLUSTER_COMPOSITE_OPS_RO","SGS_CLUSTER_MANAGE_PIPELINES","SGS_SIGNALS_ACCOUNT_READ"],"index_permissions":[{"allowed_actions":["SGS_READ"],"index_patterns":["*"],"fls":[],"masked_fields":[]}],"tenant_permissions":[{"allowed_actions":["SGS_KIBANA_ALL_READ","tenant_patterns":["PT_MDW","SGS_GLOBAL_TENANT"]}],"exclude_cluster_permissions":[],"exclude_index_permissions":[]}}

Mickaël

Hi Mickaël,

Have you tired defining read-only roles as per: https://docs.search-guard.com/latest/kibana-read-only

Best,
Mantas