Skip to content

Commit ef909ce

Browse files
First update for 2.3.0 (#11)
* Document Neoteroi/BlackSheep#187 * Document controllers inheritance * Update documentation for routing and controllers Document Neoteroi/BlackSheep#490 * Update routing.md
1 parent a8668de commit ef909ce

4 files changed

Lines changed: 395 additions & 15 deletions

File tree

blacksheep/docs/authentication.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ the future.
171171

172172
/// admonition | 💡
173173

174-
It is possible to configure several JWTBearerAuthentication handlers,
174+
It is possible to configure several `JWTBearerAuthentication` handlers,
175175
for applications that need to support more than one identity provider. For
176176
example, for applications that need to support sign-in through Auth0, Azure
177177
Active Directory, Azure Active Directory B2C.
@@ -309,6 +309,64 @@ The example below shows how a user's identity can be read from the web request:
309309
# handler)
310310
```
311311

312+
## Dependency Injection in authentication handlers
313+
314+
Dependency Injection is supported in authentication handlers. To use it:
315+
316+
1. Configure `AuthenticationHandler` objects as types (not instances)
317+
associated to the `AuthenticationStrategy` object.
318+
2. Register dependencies in the DI container, and in the handler classes
319+
according to the solution you are using for dependency injection.
320+
321+
The code below illustrates and example using the built-in solution for DI.
322+
323+
```python {linenums="1" hl_lines="7-8 11-13 23-26 28-30"}
324+
from blacksheep import Application, Request, json
325+
from guardpost import AuthenticationHandler, Identity
326+
327+
app = Application()
328+
329+
330+
class ExampleDependency:
331+
pass
332+
333+
334+
class MyAuthenticationHandler(AuthenticationHandler):
335+
def __init__(self, dependency: ExampleDependency) -> None:
336+
self.dependency = dependency
337+
338+
def authenticate(self, context: Request) -> Identity | None:
339+
# TODO: implement your own authentication logic
340+
assert isinstance(self.dependency, ExampleDependency)
341+
return Identity({"id": "example", "sub": "001"}, self.scheme)
342+
343+
344+
auth = app.use_authentication() # AuthenticationStrategy
345+
346+
# The authentication handler will be instantiated by `app.services`,
347+
# which can be any object implementing the ContainerProtocol
348+
auth.add(MyAuthenticationHandler)
349+
350+
# We need to register the types in the DI container!
351+
app.services.register(MyAuthenticationHandler)
352+
app.services.register(ExampleDependency)
353+
354+
355+
@app.router.get("/")
356+
def home(request: Request):
357+
assert request.user is not None
358+
return json(request.user.claims)
359+
```
360+
361+
/// admonition | ContainerProtocol.
362+
type: tip
363+
364+
As documented in [_Container Protocol_](./dependency-injection.md#the-container-protocol), BlackSheep
365+
supports the use of other DI containers as replacements for the built-in
366+
library used for dependency injection.
367+
368+
///
369+
312370
## Next
313371

314372
While authentication focuses on *identifying* users, authorization determines

blacksheep/docs/authorization.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,80 @@ returns:
270270
- Status [`403 Forbidden`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) if
271271
authentication succeeded as valid credentials were provided, but the user is
272272
not authorized to perform an action.
273+
274+
275+
## Dependency Injection in authorization requirements
276+
277+
Dependency Injection is supported in authorization code. To use it:
278+
279+
1. Configure `Requirement` objects as types (not instances)
280+
associated to the policies of the `AuthorizationStrategy` object.
281+
2. Register dependencies in the DI container, and in the handler classes
282+
according to the solution you are using for dependency injection.
283+
284+
The code below illustrates and example using the built-in solution for DI.
285+
286+
```python {linenums="1" hl_lines="13-14 17-18 21 41-42 45-46"}
287+
from blacksheep import Application, Request, json
288+
from guardpost import (
289+
AuthenticationHandler,
290+
AuthorizationContext,
291+
Identity,
292+
Policy,
293+
Requirement,
294+
)
295+
296+
app = Application(show_error_details=True)
297+
298+
299+
class ExampleDependency:
300+
pass
301+
302+
303+
class MyInjectedRequirement(Requirement):
304+
dependency: ExampleDependency
305+
306+
def handle(self, context: AuthorizationContext): # Note: this can also be async!
307+
assert isinstance(self.dependency, ExampleDependency)
308+
#
309+
# TODO: implement here the authorization logic
310+
#
311+
roles = context.identity.claims.get("roles", [])
312+
if roles and "ADMIN" in roles:
313+
context.succeed(self)
314+
else:
315+
context.fail("The user is not an ADMIN")
316+
317+
318+
class MyAuthenticationHandler(AuthenticationHandler):
319+
def authenticate(self, context: Request) -> Identity | None:
320+
# TODO: implement your own authentication logic
321+
return Identity({"id": "example", "sub": "001", "roles": []}, self.scheme)
322+
323+
324+
authentication = app.use_authentication()
325+
authentication.add(MyAuthenticationHandler)
326+
327+
authorization = app.use_authorization()
328+
authorization.with_default_policy(Policy("default", MyInjectedRequirement))
329+
330+
# We need to register the types in the DI container!
331+
app.services.register(MyInjectedRequirement)
332+
app.services.register(ExampleDependency)
333+
app.services.register(MyAuthenticationHandler)
334+
335+
336+
@app.router.get("/")
337+
def home(request: Request):
338+
assert request.user is not None
339+
return json(request.user.claims)
340+
```
341+
342+
/// admonition | ContainerProtocol.
343+
type: tip
344+
345+
As documented in [_Container Protocol_](./dependency-injection.md#the-container-protocol), BlackSheep
346+
supports the use of other DI containers as replacements for the built-in
347+
library used for dependency injection.
348+
349+
///

blacksheep/docs/controllers.md

Lines changed: 158 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ This page describes:
1010

1111
- [X] Controller methods.
1212
- [X] API Controllers.
13+
- [X] Controllers inheritance.
1314

1415
It is recommended to follow the [MVC tutorial](mvc-project-template.md) before
1516
reading this page.
1617

17-
/// admonition | For Flask users
18+
/// admonition | For Flask users.
1819
type: tip
20+
1921
If you come from Flask, controllers in BlackSheep can be considered
2022
equivalent to Flask's Blueprints, as they allow to group request handlers
2123
in dedicated modules and classes.
24+
2225
///
2326

2427
## The Controller class
@@ -98,6 +101,9 @@ The following example shows how dependency injection can be used in
98101
controller constructors, and an implementation of the `on_request` method:
99102

100103
```python
104+
from blacksheep import Application
105+
from blacksheep.server.controllers import Controller, get
106+
101107

102108
app = Application()
103109

@@ -108,11 +114,12 @@ class Settings:
108114
self.greetings = greetings
109115

110116

117+
app.services.add_instance(Settings(value))
118+
119+
111120
class Home(Controller):
112121

113-
def __init__(self, settings: Settings):
114-
# controllers are instantiated dynamically at every web request
115-
self.settings = settings
122+
settings: Settings
116123

117124
async def on_request(self, request: Request):
118125
print("[*] Received a request!!")
@@ -123,13 +130,6 @@ class Home(Controller):
123130
@get("/")
124131
async def index(self, request: Request):
125132
return text(self.greet())
126-
127-
# when configuring the application, register
128-
# a singleton of the application settings,
129-
# this service is automatically injected into request handlers
130-
# having a signature parameter type annotated `: Settings`, or
131-
# having name "settings", without type annotations
132-
app.services.add_instance(Settings(value))
133133
```
134134

135135
The dependency can also be described as class property:
@@ -146,7 +146,7 @@ If route methods (e.g. `head`, `get`, `post`, `put`, `patch`) from
146146
instance for controllers is used. It is also possible to use a specific router,
147147
as long as this router is bound to the application object:
148148

149-
```py
149+
```python
150150
from blacksheep.server.routing import RoutesRegistry
151151

152152

@@ -155,6 +155,35 @@ app.controllers_router = RoutesRegistry()
155155
get = app.controllers_router.get
156156
```
157157

158+
### route classmethod
159+
160+
The `route` `classmethod` can be used to define base routes for all request
161+
handlers defined on a Controller class. In the following example, the actual
162+
routes become: `/home` and `/home/about`.
163+
164+
```python
165+
from blacksheep import Application
166+
from blacksheep.server.controllers import Controller, get
167+
168+
169+
app = Application()
170+
171+
172+
class Home(Controller):
173+
174+
@classmethod
175+
def route(cls):
176+
return "/home/"
177+
178+
@get("/")
179+
def home(self):
180+
return self.ok({"message": "Hello!"})
181+
182+
@get("/about")
183+
def about(self):
184+
return self.ok({"message": "About..."})
185+
```
186+
158187
## The APIController class
159188

160189
The `APIController` class is a kind of `Controller` dedicated to API
@@ -241,3 +270,120 @@ class Cats(APIController):
241270

242271
...
243272
```
273+
274+
## Controllers inheritance
275+
276+
Since version `2.3.0`, the framework supports routes inheritance in controllers.
277+
Consider the following example:
278+
279+
```python {linenums="1" hl_lines="8-9 12-13 15 21-21"}
280+
from blacksheep import Application
281+
from blacksheep.server.controllers import Controller, get
282+
283+
284+
app = Application()
285+
286+
287+
class BaseController(Controller):
288+
path: ClassVar[str] = "base"
289+
290+
@classmethod
291+
def route(cls) -> Optional[str]:
292+
return f"/api/{cls.path}"
293+
294+
@get("/foo") # /api/base/foo
295+
def foo(self):
296+
return self.ok(self.__class__.__name__)
297+
298+
299+
class Derived(BaseController):
300+
path = "derived"
301+
302+
# /api/derived/foo (inherited from the base class)
303+
```
304+
305+
In the example above, the following routes are configured:
306+
307+
- `/api/base/foo`, defined in `BaseController`
308+
- `/api/derived/foo`, defined in `Derived`
309+
310+
To exclude the routes registered in a base controller class, decorate the class
311+
using the `@abstract()` decorator imported from `blacksheep.server.controllers`.
312+
313+
```python
314+
from blacksheep.server.controllers import Controller, abstract, get
315+
316+
317+
@abstract()
318+
class BaseController(Controller):
319+
@get("/hello-world")
320+
```
321+
322+
The following example illustrates a scenario in which a base class defines a
323+
`/hello-world` route, inherited in sub-classes that each apply a different
324+
base route. The `ControllerTwo` class defines one more route, which is also
325+
inherited by `ControllerTwoBis`; and this last class defines one more specific
326+
route.
327+
328+
```python
329+
from blacksheep import Application
330+
from blacksheep.server.controllers import Controller, abstract, get
331+
332+
333+
app = Application()
334+
335+
336+
@abstract()
337+
class BaseController(Controller):
338+
@get("/hello-world")
339+
def index(self):
340+
# Note: the route /hello-world itself will not be registered in the
341+
# router, because this class is decorated with @abstract()
342+
return self.text(f"Hello, World! {self.__class__.__name__}")
343+
344+
345+
class ControllerOne(BaseController):
346+
@classmethod
347+
def route(cls) -> str:
348+
return "/one"
349+
350+
# /one/hello-world
351+
352+
353+
class ControllerTwo(BaseController):
354+
@classmethod
355+
def route(cls) -> str:
356+
return "/two"
357+
358+
# /two/hello-world
359+
360+
@get("/specific-route") # /two/specific-route
361+
def specific_route(self):
362+
return self.text(f"This is a specific route in {self.__class__.__name__}")
363+
364+
365+
class ControllerTwoBis(ControllerTwo):
366+
@classmethod
367+
def route(cls) -> str:
368+
return "/two-bis"
369+
370+
# /two-bis/hello-world
371+
372+
# /two-bis/specific-route
373+
374+
@get("/specific-route-2") # /two-bis/specific-route-2
375+
def specific_route(self):
376+
return self.text(f"This is another route in {self.__class__.__name__}")
377+
```
378+
379+
All routes of this example, with their respective response texts, are:
380+
381+
- `/one/hello-world` :material-arrow-right: "Hello, World! ControllerOne"
382+
- `/two/hello-world` :material-arrow-right: "Hello, World! ControllerTwo"
383+
- `/two-bis/hello-world` :material-arrow-right: "Hello, World! ControllerTwoBis"
384+
- `/two/specific-route` :material-arrow-right: "This is a specific route in ControllerTwo"
385+
- `/two-bis/specific-route` :material-arrow-right: "This is a specific route in ControllerTwoBis"
386+
- `/two-bis/specific-route-2` :material-arrow-right: "This is another route in ControllerTwoBis"
387+
388+
Controller types and their dependencies are resolved appropriately for each
389+
request handler,

0 commit comments

Comments
 (0)