Security plugin support (#399)
* feat(plugins): add security client plugin Signed-off-by: florian <[email protected]> * test(plugins): skip security plugin tests when disabled Signed-off-by: florian <[email protected]> * fix(security): remove non-ASCII character Signed-off-by: florian <[email protected]> * chore(CHANGELOG): added entry for security api support in changelog Signed-off-by: florian <[email protected]> * test(plugins): add asynchronous tests version Signed-off-by: florian <[email protected]> * test: remove some warnings Signed-off-by: florian <[email protected]> * chore(USER_GUIDE): add a security plugin part Signed-off-by: florian <[email protected]> * test(security): Split out security plugin tests in its own file Signed-off-by: florian <[email protected]> * chore: apply reviews Signed-off-by: florian <[email protected]> --------- Signed-off-by: florian <[email protected]>
This commit is contained in:
@@ -9,15 +9,25 @@
|
||||
# GitHub history for details.
|
||||
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from ..test_cases import TestCase
|
||||
from mock import Mock
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestAsyncSigner(TestCase):
|
||||
class TestAsyncSigner:
|
||||
def mock_session(self):
|
||||
access_key = uuid.uuid4().hex
|
||||
secret_key = uuid.uuid4().hex
|
||||
token = uuid.uuid4().hex
|
||||
dummy_session = Mock()
|
||||
dummy_session.access_key = access_key
|
||||
dummy_session.secret_key = secret_key
|
||||
dummy_session.token = token
|
||||
return dummy_session
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 6), reason="AWSV4SignerAsyncAuth requires python3.6+"
|
||||
)
|
||||
@@ -27,10 +37,10 @@ class TestAsyncSigner(TestCase):
|
||||
from opensearchpy.helpers.asyncsigner import AWSV4SignerAsyncAuth
|
||||
|
||||
auth = AWSV4SignerAsyncAuth(self.mock_session(), region)
|
||||
headers = auth("GET", "http://localhost")
|
||||
self.assertIn("Authorization", headers)
|
||||
self.assertIn("X-Amz-Date", headers)
|
||||
self.assertIn("X-Amz-Security-Token", headers)
|
||||
headers = auth("GET", "http://localhost", {}, {})
|
||||
assert "Authorization" in headers
|
||||
assert "X-Amz-Date" in headers
|
||||
assert "X-Amz-Security-Token" in headers
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+"
|
||||
@@ -60,9 +70,6 @@ class TestAsyncSigner(TestCase):
|
||||
AWSV4SignerAsyncAuth(None, region)
|
||||
assert str(e.value) == "Credentials cannot be empty"
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
assert str(e.value) == "Credentials cannot be empty"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 6), reason="AWSV4SignerAsyncAuth requires python3.6+"
|
||||
)
|
||||
@@ -73,8 +80,8 @@ class TestAsyncSigner(TestCase):
|
||||
from opensearchpy.helpers.asyncsigner import AWSV4SignerAsyncAuth
|
||||
|
||||
auth = AWSV4SignerAsyncAuth(self.mock_session(), region, service)
|
||||
headers = auth("GET", "http://localhost")
|
||||
self.assertIn("Authorization", headers)
|
||||
self.assertIn("X-Amz-Date", headers)
|
||||
self.assertIn("X-Amz-Security-Token", headers)
|
||||
self.assertIn("X-Amz-Content-SHA256", headers)
|
||||
headers = auth("GET", "http://localhost", {}, {})
|
||||
assert "Authorization" in headers
|
||||
assert "X-Amz-Date" in headers
|
||||
assert "X-Amz-Security-Token" in headers
|
||||
assert "X-Amz-Content-SHA256" in headers
|
||||
|
||||
@@ -100,7 +100,7 @@ class TestAIOHttpConnection:
|
||||
assert con.use_ssl
|
||||
assert con.session.connector._ssl == context
|
||||
|
||||
def test_opaque_id(self):
|
||||
async def test_opaque_id(self):
|
||||
con = AIOHttpConnection(opaque_id="app-1")
|
||||
assert con.headers["x-opaque-id"] == "app-1"
|
||||
|
||||
@@ -154,18 +154,18 @@ class TestAIOHttpConnection:
|
||||
method, yarl_url = con.session.request.call_args[0]
|
||||
assert method == "GET" and str(yarl_url) == "http://localhost:9200/_search/"
|
||||
|
||||
def test_default_user_agent(self):
|
||||
async def test_default_user_agent(self):
|
||||
con = AIOHttpConnection()
|
||||
assert con._get_default_user_agent() == "opensearch-py/%s (Python %s)" % (
|
||||
__versionstr__,
|
||||
python_version(),
|
||||
)
|
||||
|
||||
def test_timeout_set(self):
|
||||
async def test_timeout_set(self):
|
||||
con = AIOHttpConnection(timeout=42)
|
||||
assert 42 == con.timeout
|
||||
|
||||
def test_keep_alive_is_on_by_default(self):
|
||||
async def test_keep_alive_is_on_by_default(self):
|
||||
con = AIOHttpConnection()
|
||||
assert {
|
||||
"connection": "keep-alive",
|
||||
@@ -173,7 +173,7 @@ class TestAIOHttpConnection:
|
||||
"user-agent": con._get_default_user_agent(),
|
||||
} == con.headers
|
||||
|
||||
def test_http_auth(self):
|
||||
async def test_http_auth(self):
|
||||
con = AIOHttpConnection(http_auth="username:secret")
|
||||
assert {
|
||||
"authorization": "Basic dXNlcm5hbWU6c2VjcmV0",
|
||||
@@ -182,7 +182,7 @@ class TestAIOHttpConnection:
|
||||
"user-agent": con._get_default_user_agent(),
|
||||
} == con.headers
|
||||
|
||||
def test_http_auth_tuple(self):
|
||||
async def test_http_auth_tuple(self):
|
||||
con = AIOHttpConnection(http_auth=("username", "secret"))
|
||||
assert {
|
||||
"authorization": "Basic dXNlcm5hbWU6c2VjcmV0",
|
||||
@@ -191,7 +191,7 @@ class TestAIOHttpConnection:
|
||||
"user-agent": con._get_default_user_agent(),
|
||||
} == con.headers
|
||||
|
||||
def test_http_auth_list(self):
|
||||
async def test_http_auth_list(self):
|
||||
con = AIOHttpConnection(http_auth=["username", "secret"])
|
||||
assert {
|
||||
"authorization": "Basic dXNlcm5hbWU6c2VjcmV0",
|
||||
@@ -200,7 +200,7 @@ class TestAIOHttpConnection:
|
||||
"user-agent": con._get_default_user_agent(),
|
||||
} == con.headers
|
||||
|
||||
def test_uses_https_if_verify_certs_is_off(self):
|
||||
async def test_uses_https_if_verify_certs_is_off(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
con = AIOHttpConnection(use_ssl=True, verify_certs=False)
|
||||
assert 1 == len(w)
|
||||
@@ -223,17 +223,17 @@ class TestAIOHttpConnection:
|
||||
|
||||
assert isinstance(con.session, aiohttp.ClientSession)
|
||||
|
||||
def test_doesnt_use_https_if_not_specified(self):
|
||||
async def test_doesnt_use_https_if_not_specified(self):
|
||||
con = AIOHttpConnection()
|
||||
assert not con.use_ssl
|
||||
|
||||
def test_no_warning_when_using_ssl_context(self):
|
||||
async def test_no_warning_when_using_ssl_context(self):
|
||||
ctx = ssl.create_default_context()
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
AIOHttpConnection(ssl_context=ctx)
|
||||
assert w == [], str([x.message for x in w])
|
||||
|
||||
def test_warns_if_using_non_default_ssl_kwargs_with_ssl_context(self):
|
||||
async def test_warns_if_using_non_default_ssl_kwargs_with_ssl_context(self):
|
||||
for kwargs in (
|
||||
{"ssl_show_warn": False},
|
||||
{"ssl_show_warn": True},
|
||||
@@ -256,21 +256,21 @@ class TestAIOHttpConnection:
|
||||
)
|
||||
|
||||
@patch("ssl.SSLContext.load_verify_locations")
|
||||
def test_uses_given_ca_certs(self, load_verify_locations, tmp_path):
|
||||
async def test_uses_given_ca_certs(self, load_verify_locations, tmp_path):
|
||||
path = tmp_path / "ca_certs.pem"
|
||||
path.touch()
|
||||
AIOHttpConnection(use_ssl=True, ca_certs=str(path))
|
||||
load_verify_locations.assert_called_once_with(cafile=str(path))
|
||||
|
||||
@patch("ssl.SSLContext.load_verify_locations")
|
||||
def test_uses_default_ca_certs(self, load_verify_locations):
|
||||
async def test_uses_default_ca_certs(self, load_verify_locations):
|
||||
AIOHttpConnection(use_ssl=True)
|
||||
load_verify_locations.assert_called_once_with(
|
||||
cafile=Connection.default_ca_certs()
|
||||
)
|
||||
|
||||
@patch("ssl.SSLContext.load_verify_locations")
|
||||
def test_uses_no_ca_certs(self, load_verify_locations):
|
||||
async def test_uses_no_ca_certs(self, load_verify_locations):
|
||||
AIOHttpConnection(use_ssl=True, verify_certs=False)
|
||||
load_verify_locations.assert_not_called()
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# The OpenSearch Contributors require contributions made to
|
||||
# this file be licensed under the Apache-2.0 license or a
|
||||
# compatible open source license.
|
||||
#
|
||||
# Modifications Copyright OpenSearch Contributors. See
|
||||
# GitHub history for details.
|
||||
#
|
||||
# Licensed to Elasticsearch B.V. under one or more contributor
|
||||
# license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright
|
||||
# ownership. Elasticsearch B.V. licenses this file to you under
|
||||
# the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
@@ -83,7 +83,7 @@ async def test_cloned_index_has_analysis_attribute():
|
||||
assert i.to_dict()["settings"]["analysis"] == i2.to_dict()["settings"]["analysis"]
|
||||
|
||||
|
||||
def test_settings_are_saved():
|
||||
async def test_settings_are_saved():
|
||||
i = AsyncIndex("i")
|
||||
i.settings(number_of_replicas=0)
|
||||
i.settings(number_of_shards=1)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# The OpenSearch Contributors require contributions made to
|
||||
# this file be licensed under the Apache-2.0 license or a
|
||||
# compatible open source license.
|
||||
#
|
||||
# Modifications Copyright OpenSearch Contributors. See
|
||||
# GitHub history for details.
|
||||
#
|
||||
# Licensed to Elasticsearch B.V. under one or more contributor
|
||||
# license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright
|
||||
# ownership. Elasticsearch B.V. licenses this file to you under
|
||||
# the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
@@ -13,18 +13,18 @@ from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from pytest import fixture
|
||||
from test_data import (
|
||||
|
||||
from opensearchpy._async.helpers.actions import async_bulk
|
||||
from opensearchpy._async.helpers.test import get_test_client
|
||||
from opensearchpy.connection.async_connections import add_connection
|
||||
from test_opensearchpy.test_async.test_server.test_helpers.test_data import (
|
||||
DATA,
|
||||
FLAT_DATA,
|
||||
TEST_GIT_DATA,
|
||||
create_flat_git_index,
|
||||
create_git_index,
|
||||
)
|
||||
|
||||
from opensearchpy._async.helpers.actions import async_bulk
|
||||
from opensearchpy._async.helpers.test import get_test_client
|
||||
from opensearchpy.connection.async_connections import add_connection
|
||||
from test_opensearchpy.test_server.test_helpers.test_document import (
|
||||
from test_opensearchpy.test_async.test_server.test_helpers.test_document import (
|
||||
Comment,
|
||||
History,
|
||||
PullRequest,
|
||||
@@ -79,8 +79,8 @@ async def data_client(client):
|
||||
|
||||
|
||||
@fixture
|
||||
def pull_request(write_client):
|
||||
PullRequest.init()
|
||||
async def pull_request(write_client):
|
||||
await PullRequest.init()
|
||||
pr = PullRequest(
|
||||
_id=42,
|
||||
comments=[
|
||||
@@ -98,7 +98,7 @@ def pull_request(write_client):
|
||||
],
|
||||
created_at=datetime(2018, 1, 9, 9, 17, 3, 21184),
|
||||
)
|
||||
pr.save(refresh=True)
|
||||
await pr.save(refresh=True)
|
||||
return pr
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from opensearchpy import Date, Keyword, Q, Text, TransportError
|
||||
from opensearchpy._async.helpers.document import AsyncDocument
|
||||
from opensearchpy._async.helpers.search import AsyncMultiSearch, AsyncSearch
|
||||
from opensearchpy.helpers.response import aggs
|
||||
from test_opensearchpy.test_server.test_helpers.test_data import FLAT_DATA
|
||||
from test_opensearchpy.test_async.test_server.test_helpers.test_data import FLAT_DATA
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# The OpenSearch Contributors require contributions made to
|
||||
# this file be licensed under the Apache-2.0 license or a
|
||||
# compatible open source license.
|
||||
#
|
||||
# Modifications Copyright OpenSearch Contributors. See
|
||||
# GitHub history for details.
|
||||
@@ -0,0 +1,160 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# The OpenSearch Contributors require contributions made to
|
||||
# this file be licensed under the Apache-2.0 license or a
|
||||
# compatible open source license.
|
||||
#
|
||||
# Modifications Copyright OpenSearch Contributors. See
|
||||
# GitHub history for details.
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from unittest import IsolatedAsyncioTestCase
|
||||
|
||||
import pytest
|
||||
|
||||
from opensearchpy._async.helpers.test import get_test_client
|
||||
from opensearchpy.connection.async_connections import add_connection
|
||||
from opensearchpy.exceptions import NotFoundError
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestSecurityPlugin(IsolatedAsyncioTestCase):
|
||||
ROLE_NAME = "test-role"
|
||||
ROLE_CONTENT = {
|
||||
"cluster_permissions": ["cluster_monitor"],
|
||||
"index_permissions": [
|
||||
{
|
||||
"index_patterns": ["index", "test-*"],
|
||||
"allowed_actions": [
|
||||
"data_access",
|
||||
"indices_monitor",
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
USER_NAME = "test-user"
|
||||
USER_CONTENT = {"password": "test_password", "opendistro_security_roles": []}
|
||||
|
||||
async def asyncSetUp(self):
|
||||
self.client = await get_test_client(
|
||||
verify_certs=False, http_auth=("admin", "admin")
|
||||
)
|
||||
await add_connection("default", self.client)
|
||||
|
||||
async def asyncTearDown(self):
|
||||
if self.client:
|
||||
await self.client.close()
|
||||
|
||||
async def test_create_role(self):
|
||||
# Test to create role
|
||||
response = await self.client.security.put_role(
|
||||
self.ROLE_NAME, body=self.ROLE_CONTENT
|
||||
)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
self.assertIn(response.get("status"), ["CREATED", "OK"])
|
||||
|
||||
async def test_get_role(self):
|
||||
# Create a role
|
||||
await self.test_create_role()
|
||||
|
||||
# Test to fetch the role
|
||||
response = await self.client.security.get_role(self.ROLE_NAME)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
self.assertIn(self.ROLE_NAME, response)
|
||||
|
||||
async def test_update_role(self):
|
||||
# Create a role
|
||||
await self.test_create_role()
|
||||
|
||||
role_content = self.ROLE_CONTENT.copy()
|
||||
role_content["cluster_permissions"] = ["cluster_all"]
|
||||
|
||||
# Test to update role
|
||||
response = await self.client.security.put_role(
|
||||
self.ROLE_NAME, body=role_content
|
||||
)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
self.assertEqual("OK", response.get("status"))
|
||||
|
||||
async def test_delete_role(self):
|
||||
# Create a role
|
||||
await self.test_create_role()
|
||||
|
||||
# Test to delete the role
|
||||
response = await self.client.security.delete_role(self.ROLE_NAME)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
|
||||
# Try fetching the role
|
||||
with self.assertRaises(NotFoundError):
|
||||
response = await self.client.security.get_role(self.ROLE_NAME)
|
||||
|
||||
async def test_create_user(self):
|
||||
# Test to create user
|
||||
response = await self.client.security.put_user(
|
||||
self.USER_NAME, body=self.USER_CONTENT
|
||||
)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
self.assertIn(response.get("status"), ["CREATED", "OK"])
|
||||
|
||||
async def test_create_user_with_role(self):
|
||||
await self.test_create_role()
|
||||
|
||||
# Test to create user
|
||||
response = await self.client.security.put_user(
|
||||
self.USER_NAME,
|
||||
body={
|
||||
"password": "test_password",
|
||||
"opendistro_security_roles": [self.ROLE_NAME],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
self.assertIn(response.get("status"), ["CREATED", "OK"])
|
||||
|
||||
async def test_get_user(self):
|
||||
# Create a user
|
||||
await self.test_create_user()
|
||||
|
||||
# Test to fetch the user
|
||||
response = await self.client.security.get_user(self.USER_NAME)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
self.assertIn(self.USER_NAME, response)
|
||||
|
||||
async def test_update_user(self):
|
||||
# Create a user
|
||||
await self.test_create_user()
|
||||
|
||||
user_content = self.USER_CONTENT.copy()
|
||||
user_content["password"] = "password_test"
|
||||
|
||||
# Test to update user
|
||||
response = await self.client.security.put_user(
|
||||
self.USER_NAME, body=user_content
|
||||
)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
self.assertEqual("OK", response.get("status"))
|
||||
|
||||
async def test_delete_user(self):
|
||||
# Create a user
|
||||
await self.test_create_user()
|
||||
|
||||
# Test to delete the user
|
||||
response = await self.client.security.delete_user(self.USER_NAME)
|
||||
|
||||
self.assertNotIn("errors", response)
|
||||
|
||||
# Try fetching the user
|
||||
with self.assertRaises(NotFoundError):
|
||||
response = await self.client.security.get_user(self.USER_NAME)
|
||||
@@ -259,7 +259,7 @@ class TestTransport:
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert isinstance(t.connection_pool.connections[0], MyConnection)
|
||||
|
||||
def test_add_connection(self):
|
||||
async def test_add_connection(self):
|
||||
t = AsyncTransport([{}], randomize_hosts=False)
|
||||
t.add_connection({"host": "google.com", "port": 1234})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user