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:
florianvazelle
2023-06-27 11:01:40 -04:00
committed by GitHub
parent db972e615b
commit c60c259d96
26 changed files with 2254 additions and 46 deletions
@@ -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
+14 -14
View File
@@ -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})