Skip to content

Commit

Permalink
Refactor auth config file, add none auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Tjstretchalot committed Nov 18, 2024
1 parent d3ee868 commit 5eae899
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 136 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
/analyse.bat

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repository to be notified when it reaches the next stage!

## Overview

This library is for when you need at-most-once pub/sub behavior across the
This library is for when you need best-effort pub/sub behavior across the
network, but don't want to maintain open connections with your clients. For
example, when subscribing to restart/upgrade requests within CI/CD, or for cache
busting local instance disk caches, or when revoked JSON web tokens early due to
Expand Down
139 changes: 139 additions & 0 deletions src/httppubsubserver/config/auth_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from typing import TYPE_CHECKING, Literal, Optional, Protocol, Type


class IncomingAuthConfig(Protocol):
async def is_subscribe_exact_allowed(
self, /, *, url: str, exact: bytes, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
"""Determines if the given url can (un)subscribe to the given exact match.
Args:
url (str): the url that will receive notifications
exact (bytes): the exact topic they want to receive messages from
now (float): the current time in seconds since the epoch, as if from `time.time()`
authorization (str, None): the authorization header they provided
Returns:
`ok`: if the subscription is allowed
`unauthorized`: if the authorization header is required but not provided
`forbidden`: if the authorization header is provided but invalid
`unavailable`: if a service is required to check this isn't available
"""

async def is_subscribe_glob_allowed(
self, /, *, url: str, glob: str, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
"""Determines if the given url can (un)subscribe to the given glob-style match
Args:
url (str): the url that will receive notifications
glob (str): a glob for the topics that they want to receive notifications from
now (float): the current time in seconds since the epoch, as if from `time.time()`
authorization (str, None): the authorization header they provided
Returns:
`ok`: if the subscription is allowed
`unauthorized`: if the authorization header is required but not provided
`forbidden`: if the authorization header is provided but invalid
`unavailable`: if a service is required to check this isn't available
"""

async def is_notify_allowed(
self,
/,
*,
topic: bytes,
message_sha512: bytes,
now: float,
authorization: Optional[str],
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
"""Determines if the given message can be published to the given topic. As
we support very large messages, for authorization only the SHA-512 of
the message should be used, which will be fully verified before any
notifications go out.
Args:
topic (bytes): the topic that the message is being sent to
message_sha512 (bytes): the sha512 of the message being sent
now (float): the current time in seconds since the epoch, as if from `time.time()`
authorization (str, None): the authorization header they provided
Returns:
`ok`: if the message is allowed
`unauthorized`: if the authorization header is required but not provided
`forbidden`: if the authorization header is provided but invalid
`unavailable`: if a service is required to check this isn't available
"""


class OutgoingAuthConfig(Protocol):
async def setup_authorization(
self, /, *, url: str, topic: bytes, message_sha512: bytes, now: float
) -> Optional[str]:
"""Setups the authorization header that the broadcaster should use when
contacting the given url about a message with the given sha512 on the
given topic at approximately the given time.
Args:
url (str): the url that will receive the notification
topic (bytes): the topic that the message is being sent to
message_sha512 (bytes): the sha512 of the message being sent
now (float): the current time in seconds since the epoch, as if from `time.time()`
Returns:
str, None: the authorization header to use, if any
"""


class AuthConfig(IncomingAuthConfig, OutgoingAuthConfig, Protocol): ...


class AuthConfigFromParts:
"""Convenience class to combine an incoming and outgoing auth config into an
auth config
"""

def __init__(self, incoming: IncomingAuthConfig, outgoing: OutgoingAuthConfig):
self.incoming = incoming
self.outgoing = outgoing

async def is_subscribe_exact_allowed(
self, /, *, url: str, exact: bytes, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return await self.incoming.is_subscribe_exact_allowed(
url=url, exact=exact, now=now, authorization=authorization
)

async def is_subscribe_glob_allowed(
self, /, *, url: str, glob: str, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return await self.incoming.is_subscribe_glob_allowed(
url=url, glob=glob, now=now, authorization=authorization
)

async def is_notify_allowed(
self,
/,
*,
topic: bytes,
message_sha512: bytes,
now: float,
authorization: Optional[str],
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return await self.incoming.is_notify_allowed(
topic=topic,
message_sha512=message_sha512,
now=now,
authorization=authorization,
)

async def setup_authorization(
self, /, *, url: str, topic: bytes, message_sha512: bytes, now: float
) -> Optional[str]:
return await self.outgoing.setup_authorization(
url=url, topic=topic, message_sha512=message_sha512, now=now
)


if TYPE_CHECKING:
_: Type[AuthConfig] = AuthConfigFromParts
134 changes: 1 addition & 133 deletions src/httppubsubserver/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,138 +8,7 @@
TYPE_CHECKING,
)


class IncomingAuthConfig(Protocol):
async def is_subscribe_exact_allowed(
self, /, *, url: str, exact: bytes, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
"""Determines if the given url can (un)subscribe to the given exact match.
Args:
url (str): the url that will receive notifications
exact (bytes): the exact topic they want to receive messages from
now (float): the current time in seconds since the epoch, as if from `time.time()`
authorization (str, None): the authorization header they provided
Returns:
`ok`: if the subscription is allowed
`unauthorized`: if the authorization header is required but not provided
`forbidden`: if the authorization header is provided but invalid
`unavailable`: if a service is required to check this isn't available
"""

async def is_subscribe_glob_allowed(
self, /, *, url: str, glob: str, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
"""Determines if the given url can (un)subscribe to the given glob-style match
Args:
url (str): the url that will receive notifications
glob (str): a glob for the topics that they want to receive notifications from
now (float): the current time in seconds since the epoch, as if from `time.time()`
authorization (str, None): the authorization header they provided
Returns:
`ok`: if the subscription is allowed
`unauthorized`: if the authorization header is required but not provided
`forbidden`: if the authorization header is provided but invalid
`unavailable`: if a service is required to check this isn't available
"""

async def is_notify_allowed(
self,
/,
*,
topic: bytes,
message_sha512: bytes,
now: float,
authorization: Optional[str],
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
"""Determines if the given message can be published to the given topic. As
we support very large messages, for authorization only the SHA-512 of
the message should be used, which will be fully verified before any
notifications go out.
Args:
topic (bytes): the topic that the message is being sent to
message_sha512 (bytes): the sha512 of the message being sent
now (float): the current time in seconds since the epoch, as if from `time.time()`
authorization (str, None): the authorization header they provided
Returns:
`ok`: if the message is allowed
`unauthorized`: if the authorization header is required but not provided
`forbidden`: if the authorization header is provided but invalid
`unavailable`: if a service is required to check this isn't available
"""


class OutgoingAuthConfig(Protocol):
async def setup_authorization(
self, /, *, url: str, topic: bytes, message_sha512: bytes, now: float
) -> Optional[str]:
"""Setups the authorization header that the broadcaster should use when
contacting the given url about a message with the given sha512 on the
given topic at approximately the given time.
Args:
url (str): the url that will receive the notification
topic (bytes): the topic that the message is being sent to
message_sha512 (bytes): the sha512 of the message being sent
now (float): the current time in seconds since the epoch, as if from `time.time()`
Returns:
str, None: the authorization header to use, if any
"""


class AuthConfig(IncomingAuthConfig, OutgoingAuthConfig, Protocol): ...


class AuthConfigFromParts:
"""Convenience class to combine an incoming and outgoing auth config into an
auth config
"""

def __init__(self, incoming: IncomingAuthConfig, outgoing: OutgoingAuthConfig):
self.incoming = incoming
self.outgoing = outgoing

async def is_subscribe_exact_allowed(
self, /, *, url: str, exact: bytes, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return await self.incoming.is_subscribe_exact_allowed(
url=url, exact=exact, now=now, authorization=authorization
)

async def is_subscribe_glob_allowed(
self, /, *, url: str, glob: str, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return await self.incoming.is_subscribe_glob_allowed(
url=url, glob=glob, now=now, authorization=authorization
)

async def is_notify_allowed(
self,
/,
*,
topic: bytes,
message_sha512: bytes,
now: float,
authorization: Optional[str],
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return await self.incoming.is_notify_allowed(
topic=topic,
message_sha512=message_sha512,
now=now,
authorization=authorization,
)

async def setup_authorization(
self, /, *, url: str, topic: bytes, message_sha512: bytes, now: float
) -> Optional[str]:
return await self.outgoing.setup_authorization(
url=url, topic=topic, message_sha512=message_sha512, now=now
)
from httppubsubserver.config.auth_config import AuthConfig


class DBConfig(Protocol):
Expand Down Expand Up @@ -362,6 +231,5 @@ def outgoing_http_timeout_sock_connect(self) -> Optional[float]:


if TYPE_CHECKING:
_: Type[AuthConfig] = AuthConfigFromParts
__: Type[GenericConfig] = GenericConfigFromValues
___: Type[Config] = ConfigFromParts
65 changes: 65 additions & 0 deletions src/httppubsubserver/config/helpers/none_auth_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from typing import Literal, Optional, TYPE_CHECKING, Type

if TYPE_CHECKING:
from httppubsubserver.config.auth_config import (
IncomingAuthConfig,
OutgoingAuthConfig,
)


class IncomingNoneAuth:
"""Allows all incoming requests
In order for this to be secure it must only be possible for trusted clients
to connect to the server (e.g., by setting up TLS mutual auth at the binding
level)
"""

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
pass

async def is_subscribe_exact_allowed(
self, /, *, url: str, exact: bytes, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return "ok"

async def is_subscribe_glob_allowed(
self, /, *, url: str, glob: str, now: float, authorization: Optional[str]
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return "ok"

async def is_notify_allowed(
self,
/,
*,
topic: bytes,
message_sha512: bytes,
now: float,
authorization: Optional[str],
) -> Literal["ok", "unauthorized", "forbidden", "unavailable"]:
return "ok"


class OutgoingNoneAuth:
"""Doesn't set any authorization header. In order for this to be secure, the
subscribers must only be able to receive messages from trusted clients.
"""

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
pass

async def setup_authorization(
self, /, *, url: str, topic: bytes, message_sha512: bytes, now: float
) -> Optional[str]:
return None


if TYPE_CHECKING:
_: Type[IncomingAuthConfig] = IncomingNoneAuth
__: Type[OutgoingAuthConfig] = OutgoingNoneAuth
5 changes: 4 additions & 1 deletion src/httppubsubserver/config/helpers/token_auth_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from typing import Literal, Optional, TYPE_CHECKING, Type

if TYPE_CHECKING:
from httppubsubserver.config.config import IncomingAuthConfig, OutgoingAuthConfig
from httppubsubserver.config.auth_config import (
IncomingAuthConfig,
OutgoingAuthConfig,
)


class IncomingTokenAuth:
Expand Down
Loading

0 comments on commit 5eae899

Please sign in to comment.