From 370bb5b36d66da6149a890ccabf9681fae34bed9 Mon Sep 17 00:00:00 2001 From: Shlomo Heigh Date: Fri, 20 May 2022 14:02:20 -0400 Subject: [PATCH] Abstract authentication flow into new interface --- CHANGELOG.md | 13 ++--- README.md | 39 +++++++++++--- conjur_api/__init__.py | 1 + conjur_api/client.py | 8 ++- conjur_api/errors/errors.py | 8 +++ conjur_api/http/api.py | 19 ++----- conjur_api/interface/__init__.py | 1 + .../authentication_strategy_interface.py | 26 +++++++++ conjur_api/models/__init__.py | 1 + conjur_api/models/enums/authn_types.py | 21 ++++++++ conjur_api/providers/__init__.py | 2 + .../authentication_strategy_factory.py | 28 ++++++++++ .../authn_authentication_strategy.py | 53 +++++++++++++++++++ tests/authentication_strategy/__init__.py | 0 ...st_unit_authentication_strategy_factory.py | 30 +++++++++++ ...test_unit_authn_authentication_provider.py | 17 ++++++ tests/integration/test_integration_vanila.py | 7 +-- 17 files changed, 243 insertions(+), 31 deletions(-) create mode 100644 conjur_api/interface/authentication_strategy_interface.py create mode 100644 conjur_api/models/enums/authn_types.py create mode 100644 conjur_api/providers/authentication_strategy_factory.py create mode 100644 conjur_api/providers/authn_authentication_strategy.py create mode 100644 tests/authentication_strategy/__init__.py create mode 100644 tests/authentication_strategy/test_unit_authentication_strategy_factory.py create mode 100644 tests/authentication_strategy/test_unit_authn_authentication_provider.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 725ddfc..edef2f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -### Added -- The `get_server_info` method is now available in SDK. It is only supported against Conjur enterprise server ### Changed -### Deprecated -### Removed -### Fixed -### Security +- Abstract authentication flow into new `AuthenticationStrategyInterface` + [conjur-api-python#20](https://github.com/cyberark/conjur-api-python/pull/20) + +## [8.0.0] - 2022-05-25 + +[Unreleased]: https://github.com/cyberark/cyberark-conjur-cli/compare/v8.0.0...HEAD +[8.0.0]: https://github.com/cyberark/cyberark-conjur-cli/releases/tag/v8.0.0 diff --git a/README.md b/README.md index 8f86970..ccc970c 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,10 @@ python API! The SDK can be installed via PyPI. Note that the SDK is a **Community level** project meaning that the SDK is subject to alterations that may result in breaking change. -``` +```sh + pip3 install conjur + ``` To avoid unanticipated breaking changes, make sure that you stay up-to-date on our latest releases and review the @@ -48,8 +50,10 @@ source and not necessarily an official release. If you wish to install the library from the source clone the [project](https://github.com/cyberark/conjur-api-python) and run: -``` +```sh + pip3 install . + ``` ### Configuring the client @@ -88,7 +92,8 @@ fit (`keyring` usage for example) We also provide the user with a simple implementation of such provider called `SimpleCredentialsProvider`. Example of creating such provider + storing credentials: -``` +```python + credentials = CredentialsData(username=username, password=password, machine=conjur_url) credentials_provider = SimpleCredentialsProvider() @@ -96,6 +101,21 @@ credentials_provider = SimpleCredentialsProvider() credentials_provider.save(credentials) del credentials + +``` + +#### Create authentication strategy + +The client also uses an authentication strategy in order to authenticate to conjur using the api_token received from the initial +login (or provided by the consuming application). This approach allows us to implement different authentication strategies +(e.g. `authn`, `authn-ldap`, `authn-k8s`) and to keep the authentication login separate from the client implementation. + +We provide the `AuthnAuthenticationStrategy` for the default Conjur authenticator. Example use: + +```python + +authn_provider = AuthnAuthenticationStrategy(conjur_url, account, username) + ``` #### Creating the client and use it @@ -103,8 +123,13 @@ del credentials Now that we have created `connection_info` and `credentials_provider` We can create our client -``` -client = Client(connection_info, credentials_provider=credentials_provider, ssl_verification_mode=ssl_verification_mode) +```python + +client = Client(connection_info, + credentials_provider=credentials_provider, + authn_strategy=authn_provider, + ssl_verification_mode=ssl_verification_mode) + ``` * ssl_verification_mode = `SslVerificationMode` enum that states what is the certificate verification technique we will @@ -112,10 +137,12 @@ client = Client(connection_info, credentials_provider=credentials_provider, ssl_ After creating the client we can login to conjur and start using it. Example of usage: -``` +```python + client.login() # login to conjur and return the api_key` client.list() # get list of all conjur resources that the user authorize to read` + ``` ## Supported Client methods diff --git a/conjur_api/__init__.py b/conjur_api/__init__.py index 6269ffc..7e8f5e7 100644 --- a/conjur_api/__init__.py +++ b/conjur_api/__init__.py @@ -7,6 +7,7 @@ from conjur_api.client import Client from conjur_api.interface import CredentialsProviderInterface +from conjur_api.interface import AuthenticationStrategyInterface from conjur_api import models from conjur_api import errors from conjur_api import providers diff --git a/conjur_api/client.py b/conjur_api/client.py index f13838e..7f68a4f 100644 --- a/conjur_api/client.py +++ b/conjur_api/client.py @@ -11,6 +11,7 @@ import json import logging from typing import Optional +from conjur_api.interface.authentication_strategy_interface import AuthenticationStrategyInterface # Internals from conjur_api.models import SslVerificationMode, CreateHostData, CreateTokenData, ListMembersOfData, \ @@ -42,6 +43,7 @@ def __init__( connection_info: ConjurConnectionInfo, ssl_verification_mode: SslVerificationMode = SslVerificationMode.TRUST_STORE, credentials_provider: CredentialsProviderInterface = None, + authn_strategy: AuthenticationStrategyInterface = None, debug: bool = False, http_debug: bool = False, async_mode: bool = True): @@ -50,6 +52,7 @@ def __init__( @param conjurrc_data: Connection metadata for conjur server @param ssl_verification_mode: Certificate validation stratagy @param credentials_provider: + @param authn_strategy: @param debug: @param http_debug: @param async_mode: This will make all of the class async functions run in sync mode (without need of await) @@ -69,7 +72,7 @@ def __init__( self.ssl_verification_mode = ssl_verification_mode self.connection_info = connection_info self.debug = debug - self._api = self._create_api(http_debug, credentials_provider) + self._api = self._create_api(http_debug, credentials_provider, authn_strategy) logging.debug("Client initialized") @@ -236,7 +239,7 @@ async def find_resource_by_identifier(self, resource_identifier: str) -> list: return resources[0] - def _create_api(self, http_debug, credentials_provider): + def _create_api(self, http_debug, credentials_provider, authn_strategy): credential_location = credentials_provider.get_store_location() logging.debug("Attempting to retrieve credentials from the '%s'...", credential_location) @@ -246,6 +249,7 @@ def _create_api(self, http_debug, credentials_provider): connection_info=self.connection_info, ssl_verification_mode=self.ssl_verification_mode, credentials_provider=credentials_provider, + authn_strategy=authn_strategy, debug=self.debug, http_debug=http_debug) diff --git a/conjur_api/errors/errors.py b/conjur_api/errors/errors.py index 1dc0802..e2832c4 100644 --- a/conjur_api/errors/errors.py +++ b/conjur_api/errors/errors.py @@ -84,6 +84,14 @@ def __init__(self, message: str = "Unknown OS"): super().__init__(self.message) +class UnknownAuthnTypeError(Exception): + """ Exception when using authentication specific logic for unknown authentication type """ + + def __init__(self, message: str = "Unknown authentication type"): + self.message = message + super().__init__(self.message) + + class MacCertificatesError(Exception): """ Exception when failing to get root CA certificates from keychain in mac """ diff --git a/conjur_api/http/api.py b/conjur_api/http/api.py index fb5ed81..32ebe2a 100644 --- a/conjur_api/http/api.py +++ b/conjur_api/http/api.py @@ -13,6 +13,7 @@ # Internals from conjur_api.http.endpoints import ConjurEndpoint +from conjur_api.interface.authentication_strategy_interface import AuthenticationStrategyInterface from conjur_api.interface.credentials_store_interface import CredentialsProviderInterface from conjur_api.wrappers.http_response import HttpResponse from conjur_api.wrappers.http_wrapper import HttpVerb, invoke_endpoint @@ -45,6 +46,7 @@ def __init__( self, connection_info: ConjurConnectionInfo, credentials_provider: CredentialsProviderInterface, + authn_strategy: AuthenticationStrategyInterface, ssl_verification_mode: SslVerificationMode = SslVerificationMode.TRUST_STORE, debug: bool = False, http_debug=False, @@ -57,6 +59,7 @@ def __init__( self._url = connection_info.conjur_url self._api_key = None self.credentials_provider: CredentialsProviderInterface = credentials_provider + self.authn_strategy: AuthenticationStrategyInterface = authn_strategy self.debug = debug self.http_debug = http_debug self.api_token_expiration = None @@ -140,23 +143,11 @@ async def authenticate(self) -> str: # password inside credentials_data await self.login() - if not self.login_id or not self.api_key: + if not self.api_key: raise MissingRequiredParameterException("Missing parameters in " "authentication invocation") - params = { - 'login': self.login_id - } - params.update(self._default_params) - - logging.debug("Authenticating to %s...", self._url) - response = await invoke_endpoint( - HttpVerb.POST, - ConjurEndpoint.AUTHENTICATE, - params, - self.api_key, - ssl_verification_metadata=self.ssl_verification_data) - return response.text + return await self.authn_strategy.authenticate(self.api_key, self.ssl_verification_data) async def resources_list(self, list_constraints: dict = None) -> dict: """ diff --git a/conjur_api/interface/__init__.py b/conjur_api/interface/__init__.py index f3d10fa..6a7780c 100644 --- a/conjur_api/interface/__init__.py +++ b/conjur_api/interface/__init__.py @@ -4,3 +4,4 @@ This module holds all the exposed interfaces of the SDK """ from conjur_api.interface.credentials_store_interface import CredentialsProviderInterface +from conjur_api.interface.authentication_strategy_interface import AuthenticationStrategyInterface diff --git a/conjur_api/interface/authentication_strategy_interface.py b/conjur_api/interface/authentication_strategy_interface.py new file mode 100644 index 0000000..42f6f2d --- /dev/null +++ b/conjur_api/interface/authentication_strategy_interface.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +AuthenticationStrategy Interface +This class describes a shared interface for authenticating to Conjur +""" + +# Builtins +import abc + +from conjur_api.models.ssl.ssl_verification_metadata import SslVerificationMetadata + +# pylint: disable=too-few-public-methods +class AuthenticationStrategyInterface(metaclass=abc.ABCMeta): # pragma: no cover + """ + AuthenticationStrategyInterface + This class is an interface that outlines a shared interface for authentication strategies + """ + + @abc.abstractmethod + async def authenticate(self, api_key: str, ssl_verification_data: SslVerificationMetadata) -> str: + """ + Authenticate uses the api_key to fetch a short-lived conjur_api token that + for a limited time will allow you to interact fully with the Conjur + vault. + """ diff --git a/conjur_api/models/__init__.py b/conjur_api/models/__init__.py index 0061077..a706daf 100644 --- a/conjur_api/models/__init__.py +++ b/conjur_api/models/__init__.py @@ -13,3 +13,4 @@ from conjur_api.models.hostfactory.create_host_data import CreateHostData from conjur_api.models.ssl.ssl_verification_mode import SslVerificationMode from conjur_api.models.general.credentials_data import CredentialsData +from conjur_api.models.enums.authn_types import AuthnTypes diff --git a/conjur_api/models/enums/authn_types.py b/conjur_api/models/enums/authn_types.py new file mode 100644 index 0000000..2d42e37 --- /dev/null +++ b/conjur_api/models/enums/authn_types.py @@ -0,0 +1,21 @@ +""" +AuthnTypes module +This module is used to represent different authentication methods. +""" + +from enum import Enum + + +class AuthnTypes(Enum): # pragma: no cover + """ + Represent possible authn methods that can be used. + """ + AUTHN = 0 + # Future: + # LDAP = 1 + + def __str__(self): + """ + Return string representation of AuthnTypes. + """ + return self.name.lower() diff --git a/conjur_api/providers/__init__.py b/conjur_api/providers/__init__.py index 9ac9a22..c4015d2 100644 --- a/conjur_api/providers/__init__.py +++ b/conjur_api/providers/__init__.py @@ -4,3 +4,5 @@ This module holds all the providers of the SDK """ from conjur_api.providers.simple_credentials_provider import SimpleCredentialsProvider +from conjur_api.providers.authn_authentication_strategy import AuthnAuthenticationStrategy +from conjur_api.providers.authentication_strategy_factory import create_authentication_strategy diff --git a/conjur_api/providers/authentication_strategy_factory.py b/conjur_api/providers/authentication_strategy_factory.py new file mode 100644 index 0000000..f3e9af4 --- /dev/null +++ b/conjur_api/providers/authentication_strategy_factory.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +""" +AuthenticationStrategyFactory module +This module job is to encapsulate the creation of SSLContext in the dependent of each os environment +""" + +from conjur_api.errors.errors import UnknownAuthnTypeError +from conjur_api.interface.authentication_strategy_interface import AuthenticationStrategyInterface +from conjur_api.models.enums.authn_types import AuthnTypes +from conjur_api.providers.authn_authentication_strategy import AuthnAuthenticationStrategy + +# pylint: disable=too-few-public-methods +def create_authentication_strategy( + authn_type: AuthnTypes, + url: str, + account: str, + username: str = None +) -> AuthenticationStrategyInterface: + """ + Factory method to create AuthenticationStrategyInterface + @return: AuthenticationStrategyInterface + """ + + if authn_type == AuthnTypes.AUTHN: + return AuthnAuthenticationStrategy(url, account, username) + + raise UnknownAuthnTypeError(f"Unknown authentication type '{authn_type}'") diff --git a/conjur_api/providers/authn_authentication_strategy.py b/conjur_api/providers/authn_authentication_strategy.py new file mode 100644 index 0000000..b566b22 --- /dev/null +++ b/conjur_api/providers/authn_authentication_strategy.py @@ -0,0 +1,53 @@ +""" +AuthnAuthenticationStrategy module + +This module holds the AuthnAuthenticationStrategy class +""" + +import logging +from conjur_api.errors.errors import MissingRequiredParameterException +from conjur_api.http.endpoints import ConjurEndpoint +from conjur_api.interface.authentication_strategy_interface import AuthenticationStrategyInterface +from conjur_api.wrappers.http_wrapper import HttpVerb, invoke_endpoint + +# pylint: disable=too-few-public-methods +class AuthnAuthenticationStrategy(AuthenticationStrategyInterface): + """ + AuthnAuthenticationStrategy class + + This class implement the "authn" strategy of authentication + """ + def __init__( + self, + url: str, + account: str, + login_id: str + ): + self._url = url + self._account = account + self._login_id = login_id + + async def authenticate(self, api_key: str, ssl_verification_data) -> str: + """ + Authenticate uses the api_key to fetch a short-lived conjur_api token that + for a limited time will allow you to interact fully with the Conjur + vault. + """ + + if self._login_id is None: + raise MissingRequiredParameterException("login_id is required") + + params = { + 'url': self._url, + 'account': self._account, + 'login': self._login_id + } + + logging.debug("Authenticating to %s...", self._url) + response = await invoke_endpoint( + HttpVerb.POST, + ConjurEndpoint.AUTHENTICATE, + params, + api_key, + ssl_verification_metadata=ssl_verification_data) + return response.text diff --git a/tests/authentication_strategy/__init__.py b/tests/authentication_strategy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/authentication_strategy/test_unit_authentication_strategy_factory.py b/tests/authentication_strategy/test_unit_authentication_strategy_factory.py new file mode 100644 index 0000000..497d8d2 --- /dev/null +++ b/tests/authentication_strategy/test_unit_authentication_strategy_factory.py @@ -0,0 +1,30 @@ +from unittest import TestCase +from conjur_api.errors.errors import UnknownAuthnTypeError +from conjur_api.models.enums.authn_types import AuthnTypes + +from conjur_api.providers.authentication_strategy_factory import create_authentication_strategy +from conjur_api.providers.authn_authentication_strategy import AuthnAuthenticationStrategy + +class AuthenticationStrategyFactoryTest(TestCase): + + def test_vanilla_flow(self): + provider = create_authentication_strategy( + AuthnTypes.AUTHN, + "https://conjur.example.com", + "some_account", + "some_username" + ) + self.assertIsInstance(provider, AuthnAuthenticationStrategy) + + def test_unknown_type(self): + with self.assertRaises(UnknownAuthnTypeError) as context: + create_authentication_strategy( + 99, + "https://conjur.example.com", + "some_account", + "some_username" + ) + self.assertEqual(context.exception.message, "Unknown authentication type '99'") + + def test_authn_type_str(self): + self.assertEqual(str(AuthnTypes.AUTHN), "authn") diff --git a/tests/authentication_strategy/test_unit_authn_authentication_provider.py b/tests/authentication_strategy/test_unit_authn_authentication_provider.py new file mode 100644 index 0000000..750364d --- /dev/null +++ b/tests/authentication_strategy/test_unit_authn_authentication_provider.py @@ -0,0 +1,17 @@ +from aiounittest import AsyncTestCase + +from conjur_api.errors.errors import MissingRequiredParameterException +from conjur_api.providers import AuthnAuthenticationStrategy + +class AuthnAuthenticationStrategyTest(AsyncTestCase): + + async def test_missing_username(self): + provider = AuthnAuthenticationStrategy( + "https://conjur.example.com", + "some_account", + None + ) + with self.assertRaises(MissingRequiredParameterException) as context: + await provider.authenticate("some_api_key", None) + + self.assertEqual(context.exception.message, "login_id is required") diff --git a/tests/integration/test_integration_vanila.py b/tests/integration/test_integration_vanila.py index 7cc53a7..27d569f 100644 --- a/tests/integration/test_integration_vanila.py +++ b/tests/integration/test_integration_vanila.py @@ -4,8 +4,8 @@ from aiounittest import AsyncTestCase from conjur_api import Client -from conjur_api.models import SslVerificationMode, ConjurConnectionInfo, CredentialsData -from conjur_api.providers import SimpleCredentialsProvider +from conjur_api.models import SslVerificationMode, ConjurConnectionInfo, CredentialsData, AuthnTypes +from conjur_api.providers import SimpleCredentialsProvider, create_authentication_strategy class TestIntegrationVanila(AsyncTestCase): @@ -25,9 +25,10 @@ async def test_integration_vanilla(self): account=account ) credentials_provider = SimpleCredentialsProvider() + authn_provider = create_authentication_strategy(AuthnTypes.AUTHN, conjur_url, account, username) credentials = CredentialsData(username=username, password=api_key,machine=conjur_url) credentials_provider.save(credentials) - c = Client(conjur_data, credentials_provider=credentials_provider, + c = Client(conjur_data, credentials_provider=credentials_provider, authn_strategy=authn_provider, ssl_verification_mode=SslVerificationMode.INSECURE) resources = await c.list() self.assertEqual(len(resources), 2)