Skip to content

Commit

Permalink
Merge pull request #32 from icgood/config
Browse files Browse the repository at this point in the history
Reorganize to allow config object
  • Loading branch information
icgood committed Sep 25, 2022
2 parents b803fcc + 75affe9 commit 3416162
Show file tree
Hide file tree
Showing 25 changed files with 398 additions and 300 deletions.
3 changes: 2 additions & 1 deletion doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
.. toctree::

pysasl
pysasl.config
pysasl.creds
pysasl.exception
pysasl.hashing
pysasl.identity
pysasl.mechanisms
pysasl.mechanism
pysasl.prep


Expand Down
6 changes: 6 additions & 0 deletions doc/source/pysasl.config.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

``pysasl.config`` Package
=========================

.. automodule:: pysasl.config
:members:
21 changes: 21 additions & 0 deletions doc/source/pysasl.mechanism.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

``pysasl.mechanism`` Package
============================

.. automodule:: pysasl.mechanism
:members:

.. automodule:: pysasl.mechanism.plain
:members:

.. automodule:: pysasl.mechanism.login
:members:

.. automodule:: pysasl.mechanism.crammd5
:members:

.. automodule:: pysasl.mechanism.oauth
:members:

.. automodule:: pysasl.mechanism.external
:members:
18 changes: 0 additions & 18 deletions doc/source/pysasl.mechanisms.rst

This file was deleted.

184 changes: 24 additions & 160 deletions pysasl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,177 +1,35 @@

from abc import abstractmethod, ABCMeta
from collections import OrderedDict
from typing import Union, Optional, Iterable, Tuple, Sequence
from typing import Optional, Iterable, Sequence
from typing_extensions import Final

import pkg_resources

from . import mechanisms
from .creds.client import ClientCredentials
from .creds.server import ServerCredentials

__all__ = ['__version__', 'ServerChallenge', 'ChallengeResponse',
'ServerMechanism', 'ClientMechanism', 'SASLAuth']
from . import mechanism
from .config import default_config, SASLConfig
from .mechanism import Mechanism, ServerMechanism, ClientMechanism

__all__ = ['__version__', 'SASLConfig', 'SASLAuth']

#: The pysasl package version.
__version__: str = pkg_resources.require(__package__)[0].version

_Mechanism = Union['ServerMechanism', 'ClientMechanism']


class ServerChallenge(Exception):
"""Raised by :meth:`~ServerMechanism.server_attempt` to provide server
challenges.
Args:
data: The challenge string that should be sent to the client.
"""

__slots__ = ['_data']

def __init__(self, data: bytes) -> None:
super().__init__(data)
self._data = data

@property
def data(self) -> bytes:
"""The server challenge that should be sent to the client."""
return self._data

def __repr__(self) -> str:
return f'ServerChallenge({self.data!r})'


class ChallengeResponse:
"""A challenge-response exchange between server and client.
Args:
challenge: The server challenge string.
response: The client response string.
"""

__slots__ = ['_challenge', '_response']

def __init__(self, challenge: bytes, response: bytes) -> None:
super().__init__()
self._challenge = challenge
self._response = response

@property
def challenge(self) -> bytes:
"""The server challenge string."""
return self._challenge

@property
def response(self) -> bytes:
"""The client response string."""
return self._response

def __repr__(self) -> str:
return f'ChallengeResponse({self.challenge!r}, {self.response!r})'


class _BaseMechanism:

__slots__: Sequence[str] = ['_name']

def __init__(self, name: Union[str, bytes]) -> None:
super().__init__()
if isinstance(name, str):
name = name.encode('ascii')
self._name = name

@property
def name(self) -> bytes:
"""The SASL name for this mechanism."""
return self._name

def __eq__(self, other: object) -> bool:
if isinstance(other, _BaseMechanism):
return self.name == other.name
return NotImplemented


class ServerMechanism(_BaseMechanism, metaclass=ABCMeta):
"""Base class for implementing SASL mechanisms that support server-side
credential verification.
"""

__slots__: Sequence[str] = []

@abstractmethod
def server_attempt(self, responses: Sequence[ChallengeResponse]) \
-> Tuple[ServerCredentials, Optional[bytes]]:
"""For SASL server-side credential verification, receives responses
from the client and issues challenges until it has everything needed to
verify the credentials.
If a challenge is necessary, a :class:`ServerChallenge` exception will
be raised. The response to this challenge must then be added to
*responses* in the next call to :meth:`.server_attempt`.
Args:
responses: The challenge-response exchanges thus far.
Returns:
A tuple of the authentication credentials received from the client
once no more challenges are necessary, and an optional final
response string from the server used by some mechanisms.
Raises:
ServerChallenge: The server challenge needing a client response.
InvalidResponse: The server received an invalid client response.
"""
...


class ClientMechanism(_BaseMechanism, metaclass=ABCMeta):
"""Base class for implementing SASL mechanisms that support client-side
credential verification.
"""

__slots__: Sequence[str] = []

@abstractmethod
def client_attempt(self, creds: ClientCredentials,
challenges: Sequence[ServerChallenge]) \
-> ChallengeResponse:
"""For SASL client-side credential verification, produce responses to
send to the server and react to its challenges until the server returns
a final success or failure.
Args:
creds: The credentials to attempt authentication with.
challenges: The server challenges received.
Returns:
The response to the most recent server challenge.
Raises:
UnexpectedChallenge: The server has issued a challenge the client
mechanism does not recognize.
"""
...


class SASLAuth:
class SASLAuth(SASLConfig):
"""Manages the mechanisms available for authentication attempts.
Args:
mechanisms: List of available SASL mechanism objects.
config: The configuration object.
"""

__slots__ = ['_server_mechanisms', '_client_mechanisms']
__slots__ = ['config', '_server_mechanisms', '_client_mechanisms']

def __init__(self, mechanisms: Sequence[_Mechanism]) -> None:
def __init__(self, mechanisms: Sequence[Mechanism], *,
config: SASLConfig = default_config) -> None:
super().__init__()
self.config: Final = config
self._server_mechanisms = OrderedDict(
(mech.name, mech)
for mech in mechanisms if isinstance(mech, ServerMechanism))
Expand All @@ -180,23 +38,28 @@ def __init__(self, mechanisms: Sequence[_Mechanism]) -> None:
for mech in mechanisms if isinstance(mech, ClientMechanism))

@classmethod
def defaults(cls) -> 'SASLAuth':
def defaults(cls, *, config: SASLConfig = default_config) -> 'SASLAuth':
"""Uses the default built-in authentication mechanisms, ``PLAIN`` and
``LOGIN``.
Args:
config: The configuration object.
Returns:
A new :class:`SASLAuth` object.
"""
return cls.named([b'PLAIN', b'LOGIN'])

@classmethod
def named(cls, names: Iterable[bytes]) -> 'SASLAuth':
def named(cls, names: Iterable[bytes], *,
config: SASLConfig = default_config) -> 'SASLAuth':
"""Uses the built-in authentication mechanisms that match a provided
name.
Args:
names: The authentication mechanism names.
config: The configuration object.
Returns:
A new :class:`SASLAuth` object.
Expand All @@ -205,15 +68,16 @@ def named(cls, names: Iterable[bytes]) -> 'SASLAuth':
KeyError: A mechanism name was not recognized.
"""
builtin = {m.name: m for m in cls._get_builtin_mechanisms()}
builtin = {m.name: m for m in cls._get_builtin_mechanisms(config)}
return SASLAuth([builtin[name] for name in names])

@classmethod
def _get_builtin_mechanisms(cls) -> Iterable[_Mechanism]:
group = mechanisms.__package__
def _get_builtin_mechanisms(cls, config: SASLConfig) \
-> Iterable[Mechanism]:
group = mechanism.__package__
for entry_point in pkg_resources.iter_entry_points(group):
mech_cls = entry_point.load()
yield mech_cls(entry_point.name)
yield mech_cls(entry_point.name, config)

@property
def server_mechanisms(self) -> Sequence[ServerMechanism]:
Expand Down
33 changes: 33 additions & 0 deletions pysasl/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

from dataclasses import dataclass
from typing_extensions import Final

from .prep import default_prep, Preparation

__all__ = ['SASLConfig', 'default_config']


@dataclass(frozen=True)
class SASLConfig:
"""Provides any configuration necessary for
:class:`~pysasl.mechanism.ServerMechanism` or
:class:`~pysasl.mechanism.ClientMechanism` instances.
"""

prepare: Preparation = default_prep
"""The preparation algorithm function."""


@dataclass(frozen=True, init=False, repr=False)
class _DefaultConfig(SASLConfig):

def __init__(self) -> None:
super().__init__()

def __repr__(self) -> str:
return 'SASLConfig()'


#: A configuration instance with all defaults.
default_config: Final = _DefaultConfig()
4 changes: 2 additions & 2 deletions pysasl/creds/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@


class Credentials(Protocol):
"""SASL authentication credentials consist of an authenticatin identity and
an authorization identity, the identity to be assumed.
"""SASL authentication credentials consist of an authentication identity
and an authorization identity, the identity to be assumed.
Consider a UNIX system where ``root`` is the superuser and only it may
assume the identity of other users. With an authentication identity of
Expand Down
Loading

0 comments on commit 3416162

Please sign in to comment.