-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extract handling of SASL auth configuration
Already handle OAuth (Bearer token) auth headers, but not yet use them in Kafka clients. The Basic authentication behaviour is unchanged, just extracted and unittested.
- Loading branch information
Mátyás Kuti
committed
Oct 4, 2023
1 parent
8a4ff32
commit 1e21729
Showing
3 changed files
with
138 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
""" | ||
Copyright (c) 2023 Aiven Ltd | ||
See LICENSE for details | ||
""" | ||
import aiohttp | ||
import dataclasses | ||
import enum | ||
from http import HTTPStatus | ||
from typing import NoReturn, Optional, TypedDict, Union | ||
|
||
from kafka.oauth.abstract import AbstractTokenProvider | ||
|
||
from karapace.config import Config | ||
from karapace.rapu import HTTPResponse, JSON_CONTENT_TYPE | ||
|
||
|
||
@enum.unique | ||
class TokenType(enum.Enum): | ||
BASIC = "Basic" | ||
BEARER = "Bearer" | ||
|
||
|
||
def raise_unauthorized() -> NoReturn: | ||
raise HTTPResponse( | ||
body='{"message": "Unauthorized"}', | ||
status=HTTPStatus.UNAUTHORIZED, | ||
content_type=JSON_CONTENT_TYPE, | ||
headers={"WWW-Authenticate": 'Basic realm="Karapace REST Proxy"'}, | ||
) | ||
|
||
|
||
class SASLPlainConfig(TypedDict): | ||
sasl_mechanism: Optional[str] | ||
sasl_plain_username: Optional[str] | ||
sasl_plain_password: Optional[str] | ||
|
||
|
||
class SASLOauthConfig(TypedDict): | ||
sasl_mechanism: Optional[str] | ||
sasl_oauth_token: Optional[str] | ||
|
||
|
||
def get_auth_config_from_header( | ||
auth_header: Optional[str], | ||
config: Config, | ||
) -> Union[SASLPlainConfig, SASLOauthConfig]: | ||
if auth_header is None: | ||
raise_unauthorized() | ||
|
||
token_type, _separator, token = auth_header.partition(" ") | ||
|
||
if token_type == TokenType.BEARER.value: | ||
return {"sasl_mechanism": "OAUTHBEARER", "sasl_oauth_token": token} | ||
|
||
if token_type == TokenType.BASIC.value: | ||
basic_auth = aiohttp.BasicAuth.decode(auth_header) | ||
sasl_mechanism = config["sasl_mechanism"] | ||
if sasl_mechanism is None: | ||
sasl_mechanism = "PLAIN" | ||
|
||
return { | ||
"sasl_mechanism": sasl_mechanism, | ||
"sasl_plain_username": basic_auth.login, | ||
"sasl_plain_password": basic_auth.password, | ||
} | ||
|
||
raise_unauthorized() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
""" | ||
Copyright (c) 2023 Aiven Ltd | ||
See LICENSE for details | ||
""" | ||
import base64 | ||
from http import HTTPStatus | ||
from typing import Optional | ||
|
||
import pytest | ||
|
||
from karapace.config import set_config_defaults, ConfigDefaults | ||
from karapace.kafka_rest_apis.auth_utils import ( | ||
get_auth_config_from_header, | ||
get_kafka_client_auth_parameters_from_config, | ||
SimpleOauthTokenProvider, | ||
) | ||
from karapace.rapu import HTTPResponse, JSON_CONTENT_TYPE | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"auth_header", | ||
(None, "Digest foo=bar"), | ||
) | ||
def test_get_auth_config_from_header_raises_unauthorized_on_invalid_header(auth_header: Optional[str]) -> None: | ||
config = set_config_defaults({}) | ||
|
||
with pytest.raises(HTTPResponse) as exc_info: | ||
get_auth_config_from_header(auth_header, config) | ||
|
||
http_resonse = exc_info.value | ||
assert http_resonse.body == '{"message": "Unauthorized"}' | ||
assert http_resonse.status == HTTPStatus.UNAUTHORIZED | ||
assert http_resonse.headers["Content-Type"] == JSON_CONTENT_TYPE | ||
assert http_resonse.headers["WWW-Authenticate"] == 'Basic realm="Karapace REST Proxy"' | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("auth_header", "config_override", "expected_auth_config"), | ||
( | ||
( | ||
f"Basic {base64.b64encode(b'username:password').decode()}", | ||
{"sasl_mechanism": None}, | ||
{"sasl_mechanism": "PLAIN", "sasl_plain_username": "username", "sasl_plain_password": "password"}, | ||
), | ||
( | ||
f"Basic {base64.b64encode(b'username:password').decode()}", | ||
{"sasl_mechanism": "PLAIN"}, | ||
{"sasl_mechanism": "PLAIN", "sasl_plain_username": "username", "sasl_plain_password": "password"}, | ||
), | ||
( | ||
f"Basic {base64.b64encode(b'username:password').decode()}", | ||
{"sasl_mechanism": "SCRAM"}, | ||
{"sasl_mechanism": "SCRAM", "sasl_plain_username": "username", "sasl_plain_password": "password"}, | ||
), | ||
( | ||
"Bearer <TOKEN>", | ||
{}, | ||
{"sasl_mechanism": "OAUTHBEARER", "sasl_oauth_token": "<TOKEN>"}, | ||
), | ||
), | ||
) | ||
def test_get_auth_config_from_header( | ||
auth_header: str, config_override: ConfigDefaults, expected_auth_config: ConfigDefaults | ||
) -> None: | ||
config = set_config_defaults(config_override) | ||
auth_config = get_auth_config_from_header(auth_header, config) | ||
assert auth_config == expected_auth_config |