From baaddc7a7587bdb9883d3c4094521da16bd54d0f Mon Sep 17 00:00:00 2001 From: Yunze Xu Date: Tue, 23 May 2023 21:23:15 +0800 Subject: [PATCH] Add docs and tests for AuthenticationOauth2 ### Modifications Add tests to verify the changes of https://github.com/apache/pulsar-client-cpp/pull/249 work for the Python client. Add docs to describe valid JSON fields used to create an `AuthenticationOauth2` instance. --- .github/workflows/ci-pr-validation.yaml | 8 ++ .../docker-compose-pulsar-oauth2.yml | 46 ++++++++++ pulsar/__init__.py | 35 +++++++- tests/oauth2_test.py | 90 +++++++++++++++++++ 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 build-support/docker-compose-pulsar-oauth2.yml create mode 100644 tests/oauth2_test.py diff --git a/.github/workflows/ci-pr-validation.yaml b/.github/workflows/ci-pr-validation.yaml index aae8551..098e342 100644 --- a/.github/workflows/ci-pr-validation.yaml +++ b/.github/workflows/ci-pr-validation.yaml @@ -70,6 +70,14 @@ jobs: WHEEL=$(find dist -name '*.whl') pip3 install ${WHEEL}[avro] + - name: Run Oauth2 tests + run: | + docker compose -f ./build-support/docker-compose-pulsar-oauth2.yml up -d + # Wait until the namespace is created, currently there is no good way to check it via CLI + sleep 10 + python3 tests/oauth2_test.py + docker compose -f ./build-support/docker-compose-pulsar-oauth2.yml down + - name: Start Pulsar service run: ./build-support/pulsar-test-service-start.sh diff --git a/build-support/docker-compose-pulsar-oauth2.yml b/build-support/docker-compose-pulsar-oauth2.yml new file mode 100644 index 0000000..0ab818e --- /dev/null +++ b/build-support/docker-compose-pulsar-oauth2.yml @@ -0,0 +1,46 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF 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. +# + +version: '3' +networks: + pulsar: + driver: bridge +services: + standalone: + image: apachepulsar/pulsar:latest + container_name: standalone + hostname: local + restart: "no" + networks: + - pulsar + environment: + - metadataStoreUrl=zk:localhost:2181 + - clusterName=standalone-oauth2 + - advertisedAddress=localhost + - advertisedListeners=external:pulsar://localhost:6650 + - PULSAR_MEM=-Xms512m -Xmx512m -XX:MaxDirectMemorySize=256m + - PULSAR_PREFIX_authenticationEnabled=true + - PULSAR_PREFIX_authenticationProviders=org.apache.pulsar.broker.authentication.AuthenticationProviderToken + - PULSAR_PREFIX_tokenPublicKey=data:;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2tZd/4gJda3U2Pc3tpgRAN7JPGWx/Gn17v/0IiZlNNRbP/Mmf0Vc6G1qsnaRaWNWOR+t6/a6ekFHJMikQ1N2X6yfz4UjMc8/G2FDPRmWjA+GURzARjVhxc/BBEYGoD0Kwvbq/u9CZm2QjlKrYaLfg3AeB09j0btNrDJ8rBsNzU6AuzChRvXj9IdcE/A/4N/UQ+S9cJ4UXP6NJbToLwajQ5km+CnxdGE6nfB7LWHvOFHjn9C2Rb9e37CFlmeKmIVFkagFM0gbmGOb6bnGI8Bp/VNGV0APef4YaBvBTqwoZ1Z4aDHy5eRxXfAMdtBkBupmBXqL6bpd15XRYUbu/7ck9QIDAQAB + - PULSAR_PREFIX_brokerClientAuthenticationPlugin=org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2 + - PULSAR_PREFIX_brokerClientAuthenticationParameters={"issuerUrl":"https://dev-kt-aa9ne.us.auth0.com","audience":"https://dev-kt-aa9ne.us.auth0.com/api/v2/","privateKey":"data:application/json;base64,ewogICAgICAgICAgICAiY2xpZW50X2lkIjoiWGQyM1JIc1VudlVsUDd3Y2hqTllPYUlmYXpnZUhkOXgiLAogICAgICAgICAgICAiY2xpZW50X3NlY3JldCI6InJUN3BzN1dZOHVoZFZ1QlRLV1prdHR3TGRRb3RtZEVsaWFNNXJMZm1nTmlidnF6aVotZzA3Wkg1Mk5fcG9HQWIiCiAgICAgICAgfQ=="} + ports: + - "6650:6650" + - "8080:8080" + command: bash -c "bin/apply-config-from-env.py conf/standalone.conf && exec bin/pulsar standalone -nss -nfw" diff --git a/pulsar/__init__.py b/pulsar/__init__.py index b1b8a9d..f7c05e2 100644 --- a/pulsar/__init__.py +++ b/pulsar/__init__.py @@ -289,15 +289,44 @@ class AuthenticationOauth2(Authentication): """ Oauth2 Authentication implementation """ - def __init__(self, auth_params_string): + def __init__(self, auth_params_string: str): """ Create the Oauth2 authentication provider instance. + You can create the instance by setting the necessary fields in the JSON string. + + .. code-block:: python + + auth = AuthenticationOauth2('{"issuer_url": "xxx", "private_key": "yyy"}') + + The valid JSON fields are: + + * issuer_url (required) + The URL of the authentication provider which allows the Pulsar client to obtain an + access token. + * private_key (required) + The URL to the JSON credentials file. It supports the following pattern formats: + + * ``/path/to/file`` + * ``file:///path/to/file`` + * ``file:/path/to/file`` + * ``data:application/json;base64,`` + + The file content or the based64 encoded value is the encoded JSON string that contains + the following fields: + + * ``client_id`` + * ``client_secret`` + * audience + The OAuth 2.0 "resource server" identifier for a Pulsar cluster. + * scope + The scope of an access request. + Parameters ---------- - - auth_params_string: str + auth_params_string : str JSON encoded configuration for Oauth2 client + """ _check_type(str, auth_params_string, 'auth_params_string') self.auth = _pulsar.AuthenticationOauth2.create(auth_params_string) diff --git a/tests/oauth2_test.py b/tests/oauth2_test.py new file mode 100644 index 0000000..1411ebc --- /dev/null +++ b/tests/oauth2_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF 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. +# + +from unittest import TestCase, main +from pulsar import AuthenticationOauth2, AuthenticationError, Client +import base64 +import os + +# This test should run against the standalone that is set up with +# build-support/docker-compose-pulsar-oauth2.yml +class Oauth2Test(TestCase): + + service_url = 'pulsar://localhost:6650' + + def test_invalid_private_key(self): + def test_create_client(auth_params_string): + client = Client(self.service_url, authentication=AuthenticationOauth2(auth_params_string)) + with self.assertRaises(AuthenticationError): + client.create_producer('oauth2-test-base64') + client.close() + + test_create_client('{"private_key":"xxx:yyy"}') + test_create_client('{"private_key":"data:"}') + test_create_client('{"private_key":"data:application/x-pem"}') + test_create_client('{"private_key":"data:application/json;xxx"}') + + def test_key_file(self): + path = (os.path.dirname(os.path.abspath(__file__)) + + '/test-conf/cpp_credentials_file.json') + auth = AuthenticationOauth2(f'''{{ + "issuer_url": "https://dev-kt-aa9ne.us.auth0.com", + "private_key": "{path}", + "audience": "https://dev-kt-aa9ne.us.auth0.com/api/v2/" + }}''') + client = Client(self.service_url, authentication=auth) + producer = client.create_producer('oauth2-test-base64') + producer.close() + client.close() + + def test_base64(self): + credentials = '''{ + "client_id":"Xd23RHsUnvUlP7wchjNYOaIfazgeHd9x", + "client_secret":"rT7ps7WY8uhdVuBTKWZkttwLdQotmdEliaM5rLfmgNibvqziZ-g07ZH52N_poGAb" + }''' + base64_credentials = base64.b64encode(credentials.encode()).decode() + auth = AuthenticationOauth2(f'''{{ + "issuer_url": "https://dev-kt-aa9ne.us.auth0.com", + "private_key": "data:application/json;base64,{base64_credentials}", + "audience": "https://dev-kt-aa9ne.us.auth0.com/api/v2/" + }}''') + client = Client(self.service_url, authentication=auth) + producer = client.create_producer('oauth2-test-base64') + producer.close() + client.close() + + def test_wrong_secret(self): + credentials = '''{ + "client_id": "my-id", + "client_secret":"my-secret" + }''' + base64_credentials = base64.b64encode(credentials.encode()).decode() + auth = AuthenticationOauth2(f'''{{ + "issuer_url": "https://dev-kt-aa9ne.us.auth0.com", + "private_key": "data:application/json;base64,{base64_credentials}", + "audience": "https://dev-kt-aa9ne.us.auth0.com/api/v2/" + }}''') + client = Client(self.service_url, authentication=auth) + with self.assertRaises(AuthenticationError): + client.create_producer('oauth2-test-base64') + client.close() + +if __name__ == '__main__': + main()