Skip to content

Commit e34bc00

Browse files
committed
Refactored auth feature and increased test coverage
1 parent 83d7d2b commit e34bc00

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+911
-830
lines changed

ellar/auth/__init__.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
from .decorators import CheckPolicies
22
from .guard import AuthorizationGuard
33
from .handlers import BaseAuthenticationHandler
4-
from .identity_provider import BaseIdentitySchemeProvider
5-
from .interfaces import IAuthConfig, IIdentitySchemeProvider
6-
from .policy import BasePolicyHandler, BasePolicyHandlerWithRequirement
4+
from .identity import UserIdentity
5+
from .interfaces import IIdentitySchemes
6+
from .policy import (
7+
BasePolicyHandler,
8+
BasePolicyHandlerWithRequirement,
9+
RequiredClaimsPolicy,
10+
RequiredRolePolicy,
11+
)
12+
from .services import AppIdentitySchemes, IdentityAuthenticationService
713

814
__all__ = [
915
"CheckPolicies",
10-
"BaseIdentitySchemeProvider",
1116
"AuthorizationGuard",
1217
"BaseAuthenticationHandler",
1318
"CheckPolicies",
1419
"BasePolicyHandler",
1520
"BasePolicyHandlerWithRequirement",
16-
"IAuthConfig",
17-
"IIdentitySchemeProvider",
21+
"IIdentitySchemes",
22+
"UserIdentity",
23+
"RequiredClaimsPolicy",
24+
"RequiredRolePolicy",
25+
"AppIdentitySchemes",
26+
"IdentityAuthenticationService",
1827
]

ellar/auth/config/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

ellar/auth/handlers/api_key.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,23 @@
1-
import typing as t
21
from abc import ABC
32

4-
from .base import BaseAPIKeyAuthenticationHandler
3+
from .mixin import BaseAuthenticationHandlerMixin
4+
from .model import BaseAuthenticationHandler
5+
from .schemes import APIKeyCookie, APIKeyHeader, APIKeyQuery
56

6-
if t.TYPE_CHECKING: # pragma: no cover
7-
from ellar.core import HTTPConnection
87

8+
class QueryAPIKeyAuthenticationHandler(
9+
BaseAuthenticationHandlerMixin, APIKeyQuery, BaseAuthenticationHandler, ABC
10+
):
11+
scheme: str = "query"
912

10-
class QueryAPIKeyAuthenticationHandler(BaseAPIKeyAuthenticationHandler, ABC):
11-
parameter_name: str = "key"
12-
openapi_in: str = "query"
1313

14-
def _get_key(self, connection: "HTTPConnection") -> t.Optional[t.Any]:
15-
return connection.query_params.get(self.parameter_name)
14+
class CookieAPIKeyAuthenticationHandler(
15+
BaseAuthenticationHandlerMixin, APIKeyCookie, BaseAuthenticationHandler, ABC
16+
):
17+
scheme: str = "cookie"
1618

1719

18-
class CookieAPIKeyAuthenticationHandler(BaseAPIKeyAuthenticationHandler, ABC):
19-
parameter_name: str = "key"
20-
openapi_in: str = "query"
21-
22-
def _get_key(self, connection: "HTTPConnection") -> t.Optional[t.Any]:
23-
return connection.cookies.get(self.parameter_name)
24-
25-
26-
class HeaderAPIKeyAuthenticationHandler(BaseAPIKeyAuthenticationHandler, ABC):
27-
parameter_name: str = "key"
28-
openapi_in: str = "query"
29-
30-
def _get_key(self, connection: "HTTPConnection") -> t.Optional[t.Any]:
31-
return connection.headers.get(self.parameter_name)
20+
class HeaderAPIKeyAuthenticationHandler(
21+
BaseAuthenticationHandlerMixin, APIKeyHeader, BaseAuthenticationHandler, ABC
22+
):
23+
scheme: str = "header"

ellar/auth/handlers/base.py

Lines changed: 0 additions & 101 deletions
This file was deleted.

ellar/auth/handlers/http.py

Lines changed: 11 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,17 @@
1-
import binascii
2-
import typing as t
31
from abc import ABC
4-
from base64 import b64decode
52

6-
from ellar.common import APIException
7-
from ellar.common.serializer.guard import (
8-
HTTPAuthorizationCredentials,
9-
HTTPBasicCredentials,
10-
)
3+
from .mixin import BaseAuthenticationHandlerMixin
4+
from .model import BaseAuthenticationHandler
5+
from .schemes import HttpBasicAuth, HttpBearerAuth
116

12-
from .base import BaseHttpAuthenticationHandler
137

14-
if t.TYPE_CHECKING: # pragma: no cover
15-
from ellar.core import HTTPConnection
8+
class HttpBearerAuthenticationHandler(
9+
BaseAuthenticationHandlerMixin, HttpBearerAuth, BaseAuthenticationHandler, ABC
10+
):
11+
scheme: str = "bearer"
1612

1713

18-
class HttpBearerAuthenticationHandler(BaseHttpAuthenticationHandler, ABC):
19-
exception_class = APIException
20-
openapi_scheme: str = "bearer"
21-
openapi_bearer_format: t.Optional[str] = None
22-
header: str = "Authorization"
23-
24-
@classmethod
25-
def openapi_security_scheme(cls) -> t.Dict:
26-
scheme = super().openapi_security_scheme()
27-
scheme[cls.openapi_name or cls.__name__].update(
28-
bearerFormat=cls.openapi_bearer_format
29-
)
30-
return scheme
31-
32-
def _get_credentials(
33-
self, connection: "HTTPConnection"
34-
) -> t.Optional[HTTPAuthorizationCredentials]:
35-
authorization: t.Optional[str] = connection.headers.get(self.header)
36-
scheme, _, credentials = self.authorization_partitioning(authorization)
37-
38-
if not (authorization and scheme and credentials):
39-
return None
40-
41-
if scheme and str(scheme).lower() != self.openapi_scheme:
42-
raise self.exception_class(
43-
status_code=404,
44-
detail="Invalid authentication credentials",
45-
)
46-
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
47-
48-
49-
class HttpBasicAuthenticationHandler(BaseHttpAuthenticationHandler, ABC):
50-
exception_class = APIException
51-
openapi_scheme: str = "basic"
52-
realm: t.Optional[str] = None
53-
header = "Authorization"
54-
55-
def _not_unauthorized_exception(self, message: str) -> None:
56-
if self.realm:
57-
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
58-
else:
59-
unauthorized_headers = {"WWW-Authenticate": "Basic"}
60-
raise self.exception_class(
61-
status_code=404,
62-
detail=message,
63-
headers=unauthorized_headers,
64-
)
65-
66-
def _get_credentials(
67-
self, connection: "HTTPConnection"
68-
) -> t.Optional[HTTPBasicCredentials]:
69-
authorization: t.Optional[str] = connection.headers.get(self.header)
70-
parts = authorization.split(" ") if authorization else []
71-
scheme, credentials = str(), str()
72-
73-
if len(parts) == 1:
74-
credentials = parts[0]
75-
scheme = "basic"
76-
elif len(parts) == 2:
77-
credentials = parts[1]
78-
scheme = parts[0].lower()
79-
80-
if (
81-
not (authorization and scheme and credentials)
82-
or scheme.lower() != self.openapi_scheme
83-
):
84-
return None
85-
86-
data: t.Optional[t.Union[str, bytes]] = None
87-
try:
88-
data = b64decode(credentials).decode("ascii")
89-
except (ValueError, UnicodeDecodeError, binascii.Error):
90-
self._not_unauthorized_exception("Invalid authentication credentials")
91-
92-
username, separator, password = (
93-
str(data).partition(":") if data else (None, None, None)
94-
)
95-
96-
if not separator:
97-
self._not_unauthorized_exception("Invalid authentication credentials")
98-
return HTTPBasicCredentials(username=username, password=password)
14+
class HttpBasicAuthenticationHandler(
15+
BaseAuthenticationHandlerMixin, HttpBasicAuth, BaseAuthenticationHandler, ABC
16+
):
17+
scheme: str = "basic"

ellar/auth/handlers/mixin.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import typing as t
2+
3+
from ellar.common import Identity, IHostContext
4+
5+
if t.TYPE_CHECKING: # pragma: no cover
6+
from ellar.core import HTTPConnection
7+
8+
9+
class BaseAuthenticationHandlerMixin:
10+
scheme: str = "apiKey"
11+
run_authentication_check: t.Callable[..., t.Coroutine]
12+
13+
@t.no_type_check
14+
async def authenticate(self, context: IHostContext) -> t.Optional[Identity]:
15+
return await self.run_authentication_check(context)
16+
17+
def handle_authentication_result(
18+
self, connection: "HTTPConnection", result: t.Optional[t.Any]
19+
) -> t.Any:
20+
if result:
21+
return result
22+
return None
23+
24+
def handle_invalid_request(self) -> t.Any:
25+
return None

ellar/auth/handlers/model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ class BaseAuthenticationHandler(ABC):
88
scheme: str
99

1010
def __init_subclass__(cls, **kwargs: str) -> None:
11-
if not cls.scheme:
12-
raise Exception("Authentication Scheme is required")
11+
if not hasattr(cls, "scheme"):
12+
raise RuntimeError(f"'{cls.__name__}' has no attribute 'scheme'")
1313

1414
@classmethod
1515
def openapi_security_scheme(cls) -> t.Optional[t.Dict]:
1616
return None
1717

1818
@abstractmethod
1919
async def authenticate(self, context: IHostContext) -> t.Optional[Identity]:
20-
...
20+
"""Authenticate Action goes here"""
2121

2222

2323
AuthenticationHandlerType = t.Union[
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from .api_key import APIKeyCookie, APIKeyHeader, APIKeyQuery
2+
from .base import BaseAPIKey, BaseAuth, BaseHttpAuth
3+
from .http import HttpBasicAuth, HttpBearerAuth, HttpDigestAuth
4+
5+
__all__ = [
6+
"APIKeyCookie",
7+
"APIKeyHeader",
8+
"APIKeyQuery",
9+
"HttpBasicAuth",
10+
"HttpBearerAuth",
11+
"HttpDigestAuth",
12+
"BaseAuth",
13+
"BaseHttpAuth",
14+
"BaseAPIKey",
15+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import typing as t
2+
from abc import ABC
3+
4+
from .base import BaseAPIKey
5+
6+
if t.TYPE_CHECKING: # pragma: no cover
7+
from ellar.core.connection import HTTPConnection
8+
9+
10+
class APIKeyQuery(BaseAPIKey, ABC):
11+
openapi_in: str = "query"
12+
13+
def _get_key(self, connection: "HTTPConnection") -> t.Optional[t.Any]:
14+
return connection.query_params.get(self.parameter_name)
15+
16+
17+
class APIKeyCookie(BaseAPIKey, ABC):
18+
openapi_in: str = "cookie"
19+
20+
def _get_key(self, connection: "HTTPConnection") -> t.Optional[t.Any]:
21+
return connection.cookies.get(self.parameter_name)
22+
23+
24+
class APIKeyHeader(BaseAPIKey, ABC):
25+
openapi_in: str = "header"
26+
27+
def _get_key(self, connection: "HTTPConnection") -> t.Optional[t.Any]:
28+
return connection.headers.get(self.parameter_name)

0 commit comments

Comments
 (0)