Skip to content

Commit 97bdfe9

Browse files
authored
Merge pull request #104 from eadwinCode/auth_shield
Authentication and Authorization
2 parents abb461c + c93a8a7 commit 97bdfe9

File tree

129 files changed

+2622
-629
lines changed

Some content is hidden

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

129 files changed

+2622
-629
lines changed

docs/index.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ Also, Ellar took some concepts from [FastAPI](https://fastapi.tiangolo.com/){tar
2525
The objective of Ellar is to provide a high level of abstracted interface to the web, along with a well-structured project setup, give room for object-oriented approach to web application design,
2626
allow you chose your desired application architecture, and ultimately, deliver speedy handling to requests.
2727

28-
As developers, we never know how big a project can become or evolve over time but following some design patterns and architecture makes it easier to build a more testable and maintainable application.
29-
30-
3128

3229
## **Features Summary**
3330

docs/stylesheets/extra.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.md-header {
2-
background-color: #212529 !important;
2+
background-color: #a50a3e !important;
33
}
44
nav.md-nav--primary ul.md-nav__list {
55
text-transform: uppercase;

docs/techniques/caching.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,8 @@ For example, lets make `CacheService` available in our route function.
413413

414414

415415
## **Using Cache Decorator**
416-
Ellar provides a cache decorator that can be used to cache the responses of route functions. The cache decorator can be applied to a route function to automatically cache its response data for a specified amount of time.
416+
Ellar provides a cache decorator that can be used to cache the responses of route functions.
417+
The cache decorator can be applied to a route function to automatically cache its response data for a specified amount of time.
417418

418419
The cache decorator takes the following arguments:
419420

@@ -423,25 +424,30 @@ The cache decorator takes the following arguments:
423424
- `backend` (optional): the name of the cache backend to use for storing the cached data. By default, the `default` cache backend is used.
424425
- `make_key_callback` (optional): a callback function that can be used to generate a custom cache key. This function takes an `IExecutionContext` instance (which contains information about the request context) and key prefix, and should return the custom cache key to use.
425426

427+
!!! info
428+
`Cache` Decorator can also be applied to any controller class.
429+
When this is done, all the routes response of that controller will be cached
430+
426431
We can rewrite the above example using `cache` decorator:
427432
=== "Synchronous Route Function"
428433
```python
429434
from ellar.common import get
430-
from ellar.cache import cache
435+
from ellar.cache import Cache
431436
...
432437
@get('/cache-test')
433-
@cache(ttl=300, version='v1', key_prefix='project_name')
438+
@Cache(ttl=300, version='v1', key_prefix='project_name')
434439
def my_route_function(self):
435440
processed_value = 'some-value'
436441
return processed_value
437442
```
438443
=== "Asynchronous Route Function"
439444
```python
440-
from ellar.common import get, cache
445+
from ellar.common import get
446+
from ellar.cache import Cache
441447

442448
...
443449
@get('/cache-test')
444-
@cache(ttl=300, version='v1', key_prefix='project_name')
450+
@Cache(ttl=300, version='v1', key_prefix='project_name')
445451
async def my_route_function(self):
446452
processed_value = 'some-value'
447453
return processed_value
@@ -457,7 +463,7 @@ Here's an example of how to use a custom `make_key_callback` function with the c
457463
=== "Synchronous Route Function"
458464
```python
459465
from ellar.common import get
460-
from ellar.cache import cache
466+
from ellar.cache import Cache
461467
from ellar.core import ExecutionContext
462468
from ellar.common.helper import get_name
463469

@@ -467,7 +473,7 @@ Here's an example of how to use a custom `make_key_callback` function with the c
467473

468474
...
469475
@get("/my_endpoint")
470-
@cache(ttl=60, make_key_callback=make_key_function)
476+
@Cache(ttl=60, make_key_callback=make_key_function)
471477
def my_endpoint(self):
472478
# Code to generate response data here
473479
processed_value = 'some-value'
@@ -478,7 +484,7 @@ Here's an example of how to use a custom `make_key_callback` function with the c
478484

479485
```python
480486
from ellar.common import get
481-
from ellar.cache import cache
487+
from ellar.cache import Cache
482488
from ellar.core import ExecutionContext
483489
from ellar.common.helper import get_name
484490

@@ -488,7 +494,7 @@ Here's an example of how to use a custom `make_key_callback` function with the c
488494

489495
...
490496
@get("/my_endpoint")
491-
@cache(ttl=60, make_key_callback=make_key_function)
497+
@Cache(ttl=60, make_key_callback=make_key_function)
492498
async def my_endpoint(self):
493499
# Code to generate response data here
494500
processed_value = 'some-value'

ellar/auth/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from .decorators import CheckPolicies
2+
from .guard import AuthorizationGuard
3+
from .handlers import BaseAuthenticationHandler
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
13+
14+
__all__ = [
15+
"CheckPolicies",
16+
"AuthorizationGuard",
17+
"BaseAuthenticationHandler",
18+
"CheckPolicies",
19+
"BasePolicyHandler",
20+
"BasePolicyHandlerWithRequirement",
21+
"IIdentitySchemes",
22+
"UserIdentity",
23+
"RequiredClaimsPolicy",
24+
"RequiredRolePolicy",
25+
"AppIdentitySchemes",
26+
"IdentityAuthenticationService",
27+
]

ellar/auth/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
POLICY_KEYS = "controller_and_route_function_policies"

ellar/auth/decorators.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import typing as t
2+
3+
from ellar.common import set_metadata as set_meta
4+
5+
from .constants import POLICY_KEYS
6+
from .policy import PolicyType
7+
8+
9+
def CheckPolicies(*policies: t.Union[str, PolicyType]) -> t.Callable:
10+
"""
11+
========= CONTROLLER AND ROUTE FUNCTION DECORATOR ==============
12+
13+
:param policies:
14+
:return:
15+
"""
16+
17+
def _decorator(target: t.Callable) -> t.Union[t.Callable, t.Any]:
18+
set_meta(POLICY_KEYS, list(policies))(target)
19+
return target
20+
21+
return _decorator

ellar/auth/guard.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import typing as t
2+
from functools import partial
3+
4+
from starlette import status
5+
6+
from ellar.common import APIException, GuardCanActivate, IExecutionContext
7+
from ellar.di import injectable
8+
9+
from .constants import POLICY_KEYS
10+
from .policy import BasePolicyHandler, PolicyType
11+
12+
13+
@injectable
14+
class AuthorizationGuard(GuardCanActivate):
15+
status_code = status.HTTP_403_FORBIDDEN
16+
exception_class = APIException
17+
18+
__slots__ = ()
19+
20+
@t.no_type_check
21+
def get_route_handler_policy(
22+
self, context: IExecutionContext
23+
) -> t.Optional[t.List[t.Union[PolicyType, str]]]:
24+
return context.get_app().reflector.get_all_and_override(
25+
POLICY_KEYS, context.get_handler(), context.get_class()
26+
)
27+
28+
def _get_policy_instance(
29+
self, context: IExecutionContext, policy: t.Union[t.Type, t.Any, str]
30+
) -> t.Union[BasePolicyHandler, t.Any]:
31+
if isinstance(policy, type):
32+
return context.get_service_provider().get(policy)
33+
return policy
34+
35+
async def can_activate(self, context: IExecutionContext) -> bool:
36+
if not context.user.is_authenticated:
37+
raise self.exception_class(status_code=401)
38+
39+
policies = self.get_route_handler_policy(context)
40+
41+
if not policies:
42+
return True
43+
44+
partial_get_policy_instance = partial(self._get_policy_instance, context)
45+
46+
for policy in map(partial_get_policy_instance, policies):
47+
result = await policy.handle(context)
48+
49+
if not result:
50+
self.raise_exception()
51+
52+
return True
53+
54+
def raise_exception(self) -> None:
55+
raise self.exception_class(status_code=self.status_code)

ellar/auth/handlers/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from .api_key import (
2+
CookieAPIKeyAuthenticationHandler,
3+
HeaderAPIKeyAuthenticationHandler,
4+
QueryAPIKeyAuthenticationHandler,
5+
)
6+
from .http import HttpBasicAuthenticationHandler, HttpBearerAuthenticationHandler
7+
from .model import AuthenticationHandlerType, BaseAuthenticationHandler
8+
9+
__all__ = [
10+
"AuthenticationHandlerType",
11+
"BaseAuthenticationHandler",
12+
"HeaderAPIKeyAuthenticationHandler",
13+
"QueryAPIKeyAuthenticationHandler",
14+
"CookieAPIKeyAuthenticationHandler",
15+
"HttpBasicAuthenticationHandler",
16+
"HttpBearerAuthenticationHandler",
17+
]

ellar/auth/handlers/api_key.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from abc import ABC
2+
3+
from .mixin import BaseAuthenticationHandlerMixin
4+
from .model import BaseAuthenticationHandler
5+
from .schemes import APIKeyCookie, APIKeyHeader, APIKeyQuery
6+
7+
8+
class QueryAPIKeyAuthenticationHandler(
9+
BaseAuthenticationHandlerMixin, APIKeyQuery, BaseAuthenticationHandler, ABC
10+
):
11+
scheme: str = "query"
12+
13+
14+
class CookieAPIKeyAuthenticationHandler(
15+
BaseAuthenticationHandlerMixin, APIKeyCookie, BaseAuthenticationHandler, ABC
16+
):
17+
scheme: str = "cookie"
18+
19+
20+
class HeaderAPIKeyAuthenticationHandler(
21+
BaseAuthenticationHandlerMixin, APIKeyHeader, BaseAuthenticationHandler, ABC
22+
):
23+
scheme: str = "header"

ellar/auth/handlers/http.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from abc import ABC
2+
3+
from .mixin import BaseAuthenticationHandlerMixin
4+
from .model import BaseAuthenticationHandler
5+
from .schemes import HttpBasicAuth, HttpBearerAuth
6+
7+
8+
class HttpBearerAuthenticationHandler(
9+
BaseAuthenticationHandlerMixin, HttpBearerAuth, BaseAuthenticationHandler, ABC
10+
):
11+
scheme: str = "bearer"
12+
13+
14+
class HttpBasicAuthenticationHandler(
15+
BaseAuthenticationHandlerMixin, HttpBasicAuth, BaseAuthenticationHandler, ABC
16+
):
17+
scheme: str = "basic"

0 commit comments

Comments
 (0)