From 7e59ce5b63ae67553dd7ecf8e65fff6c0204a3e2 Mon Sep 17 00:00:00 2001 From: Roma Frolov Date: Sun, 28 Jul 2024 16:49:57 +0300 Subject: [PATCH 1/6] feat: GSSAPI security impl for kafka --- docs/docs_src/kafka/security/sasl_gssapi.py | 9 ++++++ faststream/kafka/security.py | 11 ++++++++ faststream/security.py | 31 +++++++++++++++++++++ tests/asyncapi/kafka/test_security.py | 24 ++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 docs/docs_src/kafka/security/sasl_gssapi.py diff --git a/docs/docs_src/kafka/security/sasl_gssapi.py b/docs/docs_src/kafka/security/sasl_gssapi.py new file mode 100644 index 0000000000..08658f73d6 --- /dev/null +++ b/docs/docs_src/kafka/security/sasl_gssapi.py @@ -0,0 +1,9 @@ +import ssl + +from faststream.kafka import KafkaBroker +from faststream.security import SASLGSSAPI + +ssl_context = ssl.create_default_context() +security = SASLGSSAPI(ssl_context=ssl_context) + +broker = KafkaBroker("localhost:9092", security=security) diff --git a/faststream/kafka/security.py b/faststream/kafka/security.py index c53a37f373..76b5cc7dac 100644 --- a/faststream/kafka/security.py +++ b/faststream/kafka/security.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, Optional from faststream.security import ( + SASLGSSAPI, BaseSecurity, SASLPlaintext, SASLScram256, @@ -20,6 +21,8 @@ def parse_security(security: Optional[BaseSecurity]) -> "AnyDict": return _parse_sasl_scram256(security) elif isinstance(security, SASLScram512): return _parse_sasl_scram512(security) + elif isinstance(security, SASLGSSAPI): + return _parse_sasl_gssapi(security) elif isinstance(security, BaseSecurity): return _parse_base_security(security) else: @@ -61,3 +64,11 @@ def _parse_sasl_scram512(security: SASLScram512) -> "AnyDict": "sasl_plain_username": security.username, "sasl_plain_password": security.password, } + + +def _parse_sasl_gssapi(security: SASLGSSAPI) -> "AnyDict": + return { + "security_protocol": "SASL_SSL" if security.use_ssl else "SASL_PLAINTEXT", + "ssl_context": security.ssl_context, + "sasl_mechanism": "GSSAPI", + } diff --git a/faststream/security.py b/faststream/security.py index 2ce4d1d4fc..751c109586 100644 --- a/faststream/security.py +++ b/faststream/security.py @@ -151,3 +151,34 @@ def get_requirement(self) -> List["AnyDict"]: def get_schema(self) -> Dict[str, Dict[str, str]]: """Get the security schema for SASL/SCRAM-SHA-512 authentication.""" return {"scram512": {"type": "scramSha512"}} + + +class SASLGSSAPI(BaseSecurity): + """Security configuration for SASL/GSSAPI authentication. + + This class defines security configuration for SASL/GSSAPI authentication. + """ + + # TODO: mv to SecretStr + __slots__ = ( + "use_ssl", + "ssl_context", + ) + + def __init__( + self, + ssl_context: Optional["SSLContext"] = None, + use_ssl: Optional[bool] = None, + ) -> None: + super().__init__( + ssl_context=ssl_context, + use_ssl=use_ssl, + ) + + def get_requirement(self) -> List["AnyDict"]: + """Get the security requirements for SASL/GSSAPI authentication.""" + return [{"gssapi": []}] + + def get_schema(self) -> Dict[str, Dict[str, str]]: + """Get the security schema for SASL/GSSAPI authentication.""" + return {"gssapi": {"type": "gssapi"}} diff --git a/tests/asyncapi/kafka/test_security.py b/tests/asyncapi/kafka/test_security.py index bd469cb05f..c87a47eba5 100644 --- a/tests/asyncapi/kafka/test_security.py +++ b/tests/asyncapi/kafka/test_security.py @@ -5,6 +5,7 @@ from faststream.asyncapi.generate import get_app_schema from faststream.kafka import KafkaBroker from faststream.security import ( + SASLGSSAPI, BaseSecurity, SASLPlaintext, SASLScram256, @@ -167,3 +168,26 @@ async def test_topic(msg: str) -> str: } assert schema == sasl512_security_schema + + +def test_gssapi_security_schema(): + ssl_context = ssl.create_default_context() + security = SASLGSSAPI( + ssl_context=ssl_context, + ) + + broker = KafkaBroker("localhost:9092", security=security) + app = FastStream(broker) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = get_app_schema(app).to_jsonable() + + gssapi_security_schema = deepcopy(basic_schema) + gssapi_security_schema["servers"]["development"]["security"] = [{"gssapi": []}] + gssapi_security_schema["components"]["securitySchemes"] = {"gssapi": {"type": "gssapi"}} + + assert schema == gssapi_security_schema From 570833d8dacc1ce6a321f7aa956187c76d276680 Mon Sep 17 00:00:00 2001 From: Roma Frolov Date: Tue, 30 Jul 2024 16:57:16 +0300 Subject: [PATCH 2/6] feat: linting --- tests/asyncapi/kafka/test_security.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/asyncapi/kafka/test_security.py b/tests/asyncapi/kafka/test_security.py index c87a47eba5..a8bd6878d6 100644 --- a/tests/asyncapi/kafka/test_security.py +++ b/tests/asyncapi/kafka/test_security.py @@ -188,6 +188,8 @@ async def test_topic(msg: str) -> str: gssapi_security_schema = deepcopy(basic_schema) gssapi_security_schema["servers"]["development"]["security"] = [{"gssapi": []}] - gssapi_security_schema["components"]["securitySchemes"] = {"gssapi": {"type": "gssapi"}} + gssapi_security_schema["components"]["securitySchemes"] = { + "gssapi": {"type": "gssapi"} + } assert schema == gssapi_security_schema From bbc64f291ef484802b6c83428fc8d0cadc7d0002 Mon Sep 17 00:00:00 2001 From: Roma Frolov Date: Tue, 30 Jul 2024 17:43:34 +0300 Subject: [PATCH 3/6] feat: test for parsing SASLGSSAPI --- tests/brokers/kafka/test_security.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/brokers/kafka/test_security.py diff --git a/tests/brokers/kafka/test_security.py b/tests/brokers/kafka/test_security.py new file mode 100644 index 0000000000..ee83c19ff2 --- /dev/null +++ b/tests/brokers/kafka/test_security.py @@ -0,0 +1,38 @@ +from contextlib import contextmanager +from typing import Tuple +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@contextmanager +def _patch_aio_producer() -> Tuple[MagicMock, MagicMock]: + try: + producer = MagicMock(return_value=AsyncMock()) + + with patch( + "aiokafka.AIOKafkaProducer", + new=producer, + ): + yield producer + finally: + pass + + +@pytest.mark.kafka() +@pytest.mark.asyncio() +async def test_gssapi(): + from docs.docs_src.kafka.security.sasl_gssapi import ( + broker as gssapi_broker, + ) + + with _patch_aio_producer() as producer: + async with gssapi_broker: + producer_call_kwargs = producer.call_args.kwargs + + call_kwargs = { + "sasl_mechanism": "GSSAPI", + "security_protocol": "SASL_SSL", + } + + assert call_kwargs.items() <= producer_call_kwargs.items() From 609601f01579dd426b0ef8f9ef9415845ce6652d Mon Sep 17 00:00:00 2001 From: Roma Frolov Date: Wed, 31 Jul 2024 09:08:05 +0300 Subject: [PATCH 4/6] feat: added SASLGSSAPI example in security.md --- docs/docs/en/kafka/security.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/docs/en/kafka/security.md b/docs/docs/en/kafka/security.md index fd66f79a56..d1a0947c39 100644 --- a/docs/docs/en/kafka/security.md +++ b/docs/docs/en/kafka/security.md @@ -51,3 +51,13 @@ This chapter discusses the security options available in **FastStream** and how ```python linenums="1" {!> docs_src/kafka/security/sasl_scram512.py [ln:1-10.25,11-] !} ``` + +### 4. SASLGSSAPI Object with SSL/TLS + +**Purpose:** The `SASLGSSAPI` object is used for authentication using Kerberos. + +**Usage:** + +```python linenums="1" +{!> docs_src/kafka/security/sasl_gssapi.py [ln:1-10.25,11-] !} +``` From 7ede80bcfdae8cc1fb7dbadf0daefe9fb089ce6d Mon Sep 17 00:00:00 2001 From: Roma Frolov Date: Thu, 1 Aug 2024 16:03:23 +0300 Subject: [PATCH 5/6] feat: added test_gssapi in test_security.py --- tests/brokers/kafka/test_security.py | 38 ---------------------------- tests/docs/kafka/test_security.py | 21 +++++++++++++++ 2 files changed, 21 insertions(+), 38 deletions(-) delete mode 100644 tests/brokers/kafka/test_security.py diff --git a/tests/brokers/kafka/test_security.py b/tests/brokers/kafka/test_security.py deleted file mode 100644 index ee83c19ff2..0000000000 --- a/tests/brokers/kafka/test_security.py +++ /dev/null @@ -1,38 +0,0 @@ -from contextlib import contextmanager -from typing import Tuple -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - - -@contextmanager -def _patch_aio_producer() -> Tuple[MagicMock, MagicMock]: - try: - producer = MagicMock(return_value=AsyncMock()) - - with patch( - "aiokafka.AIOKafkaProducer", - new=producer, - ): - yield producer - finally: - pass - - -@pytest.mark.kafka() -@pytest.mark.asyncio() -async def test_gssapi(): - from docs.docs_src.kafka.security.sasl_gssapi import ( - broker as gssapi_broker, - ) - - with _patch_aio_producer() as producer: - async with gssapi_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = { - "sasl_mechanism": "GSSAPI", - "security_protocol": "SASL_SSL", - } - - assert call_kwargs.items() <= producer_call_kwargs.items() diff --git a/tests/docs/kafka/test_security.py b/tests/docs/kafka/test_security.py index 69b3a9c04a..ed2fda2b82 100644 --- a/tests/docs/kafka/test_security.py +++ b/tests/docs/kafka/test_security.py @@ -98,3 +98,24 @@ async def test_plaintext(): assert call_kwargs.items() <= producer_call_kwargs.items() assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext + + +@pytest.mark.kafka() +@pytest.mark.asyncio() +async def test_gssapi(): + from docs.docs_src.kafka.security.sasl_gssapi import ( + broker as gssapi_broker, + ) + + with patch_aio_consumer_and_producer() as producer: + async with gssapi_broker: + producer_call_kwargs = producer.call_args.kwargs + + call_kwargs = { + "sasl_mechanism": "GSSAPI", + "security_protocol": "SASL_SSL", + } + + assert call_kwargs.items() <= producer_call_kwargs.items() + + assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext From 4fc8e922d4d7c08831501f127ad3f8cf0c61d2bd Mon Sep 17 00:00:00 2001 From: Nikita Pastukhov Date: Thu, 1 Aug 2024 18:41:10 +0300 Subject: [PATCH 6/6] docs: generate API --- docs/docs/SUMMARY.md | 1 + docs/docs/en/api/faststream/security/SASLGSSAPI.md | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/docs/en/api/faststream/security/SASLGSSAPI.md diff --git a/docs/docs/SUMMARY.md b/docs/docs/SUMMARY.md index 92a04da868..35ef344731 100644 --- a/docs/docs/SUMMARY.md +++ b/docs/docs/SUMMARY.md @@ -953,6 +953,7 @@ search: - [build_message](api/faststream/redis/testing/build_message.md) - security - [BaseSecurity](api/faststream/security/BaseSecurity.md) + - [SASLGSSAPI](api/faststream/security/SASLGSSAPI.md) - [SASLPlaintext](api/faststream/security/SASLPlaintext.md) - [SASLScram256](api/faststream/security/SASLScram256.md) - [SASLScram512](api/faststream/security/SASLScram512.md) diff --git a/docs/docs/en/api/faststream/security/SASLGSSAPI.md b/docs/docs/en/api/faststream/security/SASLGSSAPI.md new file mode 100644 index 0000000000..8b6eec2741 --- /dev/null +++ b/docs/docs/en/api/faststream/security/SASLGSSAPI.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.security.SASLGSSAPI