Skip to content

Commit

Permalink
Abstract authentication flow into new interface
Browse files Browse the repository at this point in the history
  • Loading branch information
szh committed May 23, 2022
1 parent 6a5f738 commit d122bf9
Show file tree
Hide file tree
Showing 16 changed files with 216 additions and 24 deletions.
39 changes: 33 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -88,34 +92,57 @@ 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()

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

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
use when making the api request

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
Expand Down
1 change: 1 addition & 0 deletions conjur_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 6 additions & 2 deletions conjur_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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")

Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions conjur_api/errors/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """

Expand Down
19 changes: 5 additions & 14 deletions conjur_api/http/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down
1 change: 1 addition & 0 deletions conjur_api/interface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions conjur_api/interface/authentication_strategy_interface.py
Original file line number Diff line number Diff line change
@@ -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.
"""
1 change: 1 addition & 0 deletions conjur_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions conjur_api/models/enums/authn_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
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
2 changes: 2 additions & 0 deletions conjur_api/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 28 additions & 0 deletions conjur_api/providers/authentication_strategy_factory.py
Original file line number Diff line number Diff line change
@@ -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}'")
51 changes: 51 additions & 0 deletions conjur_api/providers/authn_authentication_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
AuthnAuthenticationStrategy module
This module holds the AuthnAuthenticationStrategy class
"""

import logging
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.
"""

# TODO: What if login_id is Nothing

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
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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'")
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from unittest import TestCase

from conjur_api.providers.authn_authentication_strategy import AuthnAuthenticationStrategy

class AuthnAuthenticationStrategyTest(TestCase):

def test_vanilla_flow(self):
# TODO: What can we test here that would not really be an integration test?
return
Loading

0 comments on commit d122bf9

Please sign in to comment.