Skip to content

Commit 916a186

Browse files
committed
fixed test-cov and added sample for authentication with auth handlers
1 parent b2109c0 commit 916a186

Some content is hidden

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

43 files changed

+1776
-164
lines changed

ellar/auth/guard.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import starlette.status
44
from ellar.common import GuardCanActivate, IExecutionContext, constants
55

6+
if t.TYPE_CHECKING: # pragma: no cover
7+
from ellar.common.routing import RouteOperation
8+
69

710
class AuthenticatedRequiredGuard(GuardCanActivate):
811
status_code = starlette.status.HTTP_401_UNAUTHORIZED
@@ -16,9 +19,16 @@ def __init__(
1619
self.openapi_scope = openapi_scope or []
1720
self.reflector = Reflector()
1821

19-
def openapi_security_scheme(self) -> t.Dict:
22+
def openapi_security_scheme(
23+
self, route: t.Optional["RouteOperation"] = None
24+
) -> t.Dict:
2025
# this will only add security scope to the applied controller or route function
21-
if self.authentication_scheme:
26+
skip_auth: t.Any = False
27+
if route:
28+
skip_auth = self.reflector.get_all_and_override(
29+
constants.SKIP_AUTH, route.endpoint, route.get_controller_type()
30+
)
31+
if not skip_auth and self.authentication_scheme:
2232
return {self.authentication_scheme: {}}
2333

2434
return {}

ellar/auth/handlers/schemes/base.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from starlette.status import HTTP_401_UNAUTHORIZED
1313

1414
if t.TYPE_CHECKING: # pragma: no cover
15+
from ellar.common.routing import RouteOperation
1516
from ellar.core.connection import HTTPConnection
1617

1718

@@ -36,7 +37,9 @@ async def _authentication_check(
3637

3738
@classmethod
3839
@abstractmethod
39-
def openapi_security_scheme(cls) -> t.Dict:
40+
def openapi_security_scheme(
41+
cls, route: t.Optional["RouteOperation"] = None
42+
) -> t.Dict:
4043
"""Override and provide OPENAPI Security Scheme"""
4144

4245
async def run_authentication_check(self, context: IHostContext) -> t.Any:
@@ -95,7 +98,9 @@ async def authentication_handler(
9598
pass # pragma: no cover
9699

97100
@classmethod
98-
def openapi_security_scheme(cls) -> t.Dict:
101+
def openapi_security_scheme(
102+
cls, route: t.Optional["RouteOperation"] = None
103+
) -> t.Dict:
99104
assert cls.openapi_in, "openapi_in is required"
100105
return {
101106
cls.openapi_name
@@ -143,7 +148,9 @@ def _get_credentials(
143148
pass # pragma: no cover
144149

145150
@classmethod
146-
def openapi_security_scheme(cls) -> t.Dict:
151+
def openapi_security_scheme(
152+
cls, route: t.Optional["RouteOperation"] = None
153+
) -> t.Dict:
147154
assert cls.scheme, "openapi_scheme is required"
148155
return {
149156
cls.openapi_name

ellar/auth/handlers/schemes/http.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .base import BaseHttpAuth
1313

1414
if t.TYPE_CHECKING: # pragma: no cover
15+
from ellar.common.routing import RouteOperation
1516
from ellar.core.connection import HTTPConnection
1617

1718

@@ -22,8 +23,10 @@ class HttpBearerAuth(BaseHttpAuth, ABC):
2223
header: str = "Authorization"
2324

2425
@classmethod
25-
def openapi_security_scheme(cls) -> t.Dict:
26-
scheme = super().openapi_security_scheme()
26+
def openapi_security_scheme(
27+
cls, route: t.Optional["RouteOperation"] = None
28+
) -> t.Dict:
29+
scheme = super().openapi_security_scheme(route)
2730
scheme[cls.openapi_name or cls.__name__].update(
2831
bearerFormat=cls.openapi_bearer_format
2932
)

ellar/openapi/route_doc_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def _get_openapi_security_scheme(
200200
for item in self.guards:
201201
if not hasattr(item, "openapi_security_scheme"):
202202
continue
203-
security_scheme = item.openapi_security_scheme()
203+
security_scheme = item.openapi_security_scheme(self.route)
204204
security_definitions.update(security_scheme)
205205

206206
keys = list(security_scheme.keys())

examples/01-carapp/carapp/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from ellar.openapi import (
66
OpenAPIDocumentBuilder,
77
OpenAPIDocumentModule,
8-
ReDocDocumentGenerator,
9-
SwaggerDocumentGenerator,
8+
ReDocsUI,
9+
SwaggerUI,
1010
)
1111

1212
from .root_module import ApplicationModule
@@ -25,7 +25,7 @@
2525

2626
document = document_builder.build_document(application)
2727
module = OpenAPIDocumentModule.setup(
28-
document_generator=[SwaggerDocumentGenerator(), ReDocDocumentGenerator()],
28+
docs_ui=[ReDocsUI(), SwaggerUI()],
2929
document=document,
3030
guards=[],
3131
)

examples/02-socketio-app/socketio_app/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ellar.common.constants import ELLAR_CONFIG_MODULE
44
from ellar.core.factory import AppFactory
55

6-
# from ellar.openapi import OpenAPIDocumentModule, OpenAPIDocumentBuilder, SwaggerDocumentGenerator
6+
# from ellar.openapi import OpenAPIDocumentModule, OpenAPIDocumentBuilder, SwaggerUI
77
from .root_module import ApplicationModule
88

99
application = AppFactory.create_from_app_module(
@@ -25,7 +25,7 @@
2525
# document = document_builder.build_document(application)
2626
# module = OpenAPIDocumentModule.setup(
2727
# document=document,
28-
# document_generator=SwaggerDocumentGenerator(),
28+
# docs_ui=SwaggerUI(),
2929
# guards=[]
3030
# )
3131
# application.install_module(module)

examples/03-auth-with-guards/auth_project/auth/controllers.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,29 @@ class MyController(ControllerBase):
88
def index(self):
99
return {'detail': "Welcome Dog's Resources"}
1010
"""
11-
from ellar.common import Body, Controller, ControllerBase, UseGuards, get, post
11+
from ellar.common import Body, Controller, ControllerBase, get, post
12+
from ellar.openapi import ApiTags
1213

13-
from .guards import AllowAnyGuard
14-
from .schemas import UserCredentials
14+
from .guards import allow_any
1515
from .services import AuthService
1616

1717

18-
@Controller("/auth")
18+
@Controller
19+
@ApiTags(name="Authentication", description="User Authentication Endpoints")
1920
class AuthController(ControllerBase):
2021
def __init__(self, auth_service: AuthService) -> None:
2122
self.auth_service = auth_service
2223

23-
@post("/sign-in")
24-
@UseGuards(AllowAnyGuard)
25-
async def sign_in(self, payload: UserCredentials = Body()):
26-
return await self.auth_service.sign_in(payload)
24+
@post("/login")
25+
@allow_any()
26+
async def sign_in(self, username: Body[str], password: Body[str]):
27+
return await self.auth_service.sign_in(username=username, password=password)
2728

2829
@get("/profile")
29-
def get_profile(self):
30+
async def get_profile(self):
3031
return self.context.user
32+
33+
@allow_any()
34+
@post("/refresh")
35+
async def refresh_token(self, payload: str = Body(embed=True)):
36+
return await self.auth_service.refresh_token(payload)

examples/03-auth-with-guards/auth_project/auth/guards.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import typing as t
22

33
from ellar.auth import UserIdentity
4-
from ellar.common import GuardCanActivate, IExecutionContext
4+
from ellar.common import (
5+
GuardCanActivate,
6+
IExecutionContext,
7+
constants,
8+
logger,
9+
set_metadata,
10+
)
511
from ellar.common.serializer.guard import (
612
HTTPAuthorizationCredentials,
7-
HTTPBasicCredentials,
813
)
914
from ellar.core.guards import GuardHttpBearerAuth
1015
from ellar.di import injectable
1116
from ellar_jwt import JWTService
1217

13-
if t.TYPE_CHECKING:
14-
from ellar.core import HTTPConnection
18+
19+
def allow_any() -> t.Callable:
20+
return set_metadata(constants.GUARDS_KEY, [AllowAny()])
21+
22+
23+
class AllowAny(GuardCanActivate):
24+
async def can_activate(self, context: IExecutionContext) -> bool:
25+
return True
1526

1627

1728
@injectable
@@ -21,17 +32,12 @@ def __init__(self, jwt_service: JWTService) -> None:
2132

2233
async def authentication_handler(
2334
self,
24-
connection: "HTTPConnection",
25-
credentials: t.Union[HTTPBasicCredentials, HTTPAuthorizationCredentials],
35+
context: IExecutionContext,
36+
credentials: HTTPAuthorizationCredentials,
2637
) -> t.Optional[t.Any]:
2738
try:
2839
data = await self.jwt_service.decode_async(credentials.credentials)
29-
return UserIdentity(auth_type="bearer", **dict(data))
30-
except Exception:
40+
return UserIdentity(auth_type="bearer", **data)
41+
except Exception as ex:
42+
logger.logger.error(f"[AuthGuard] Exception: {ex}")
3143
self.raise_exception()
32-
33-
34-
@injectable
35-
class AllowAnyGuard(GuardCanActivate):
36-
async def can_activate(self, context: IExecutionContext) -> bool:
37-
return True
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import typing as t
2+
3+
from ellar.auth import UserIdentity
4+
from ellar.common import IExecutionContext, logger, set_metadata
5+
from ellar.common.serializer.guard import (
6+
HTTPAuthorizationCredentials,
7+
)
8+
from ellar.core import Reflector
9+
from ellar.core.guards import GuardHttpBearerAuth
10+
from ellar.di import injectable
11+
from ellar_jwt import JWTService
12+
13+
IS_ANONYMOUS = "is_anonymous"
14+
15+
16+
def allow_any() -> t.Callable:
17+
return set_metadata(IS_ANONYMOUS, True)
18+
19+
20+
@injectable
21+
class AuthGuard(GuardHttpBearerAuth):
22+
def __init__(self, jwt_service: JWTService, reflector: Reflector) -> None:
23+
self.jwt_service = jwt_service
24+
self.reflector = reflector
25+
26+
async def authentication_handler(
27+
self,
28+
context: IExecutionContext,
29+
credentials: HTTPAuthorizationCredentials,
30+
) -> t.Optional[t.Any]:
31+
is_anonymous = self.reflector.get_all_and_override(
32+
IS_ANONYMOUS, context.get_handler(), context.get_class()
33+
)
34+
35+
if is_anonymous:
36+
return True
37+
38+
try:
39+
data = await self.jwt_service.decode_async(credentials.credentials)
40+
return UserIdentity(auth_type=self.scheme, **data)
41+
except Exception as ex:
42+
logger.logger.error(f"[AuthGuard] Exception: {ex}")
43+
self.raise_exception()

examples/03-auth-with-guards/auth_project/auth/module.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,30 @@ def register_providers(self, container: Container) -> None:
1717
pass
1818
1919
"""
20-
from ellar.common import Module
20+
from datetime import timedelta
21+
22+
from ellar.common import GlobalGuard, Module
2123
from ellar.core import ModuleBase
22-
from ellar.di import Container
24+
from ellar.di import ProviderConfig
25+
from ellar_jwt import JWTModule
2326

27+
from ..users.module import UsersModule
2428
from .controllers import AuthController
29+
from .guards import AuthGuard
2530
from .services import AuthService
2631

2732

2833
@Module(
34+
modules=[
35+
UsersModule,
36+
JWTModule.setup(
37+
signing_secret_key="my_poor_secret_key_lol", lifetime=timedelta(minutes=5)
38+
),
39+
],
2940
controllers=[AuthController],
30-
providers=[AuthService],
31-
routers=[],
41+
providers=[AuthService, ProviderConfig(GlobalGuard, use_class=AuthGuard)],
3242
)
3343
class AuthModule(ModuleBase):
3444
"""
3545
Auth Module
3646
"""
37-
38-
def register_providers(self, container: Container) -> None:
39-
"""for more complicated provider registrations, use container.register_instance(...)"""

0 commit comments

Comments
 (0)