Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion blacksheep/docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ the future.

/// admonition | 💡

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

## Dependency Injection in authentication handlers

Dependency Injection is supported in authentication handlers. To use it:

1. Configure `AuthenticationHandler` objects as types (not instances)
associated to the `AuthenticationStrategy` object.
2. Register dependencies in the DI container, and in the handler classes
according to the solution you are using for dependency injection.

The code below illustrates and example using the built-in solution for DI.

```python {linenums="1" hl_lines="7-8 11-13 23-26 28-30"}
from blacksheep import Application, Request, json
from guardpost import AuthenticationHandler, Identity

app = Application()


class ExampleDependency:
pass


class MyAuthenticationHandler(AuthenticationHandler):
def __init__(self, dependency: ExampleDependency) -> None:
self.dependency = dependency

def authenticate(self, context: Request) -> Identity | None:
# TODO: implement your own authentication logic
assert isinstance(self.dependency, ExampleDependency)
return Identity({"id": "example", "sub": "001"}, self.scheme)


auth = app.use_authentication() # AuthenticationStrategy

# The authentication handler will be instantiated by `app.services`,
# which can be any object implementing the ContainerProtocol
auth.add(MyAuthenticationHandler)

# We need to register the types in the DI container!
app.services.register(MyAuthenticationHandler)
app.services.register(ExampleDependency)


@app.router.get("/")
def home(request: Request):
assert request.user is not None
return json(request.user.claims)
```

/// admonition | ContainerProtocol.
type: tip

As documented in [_Container Protocol_](./dependency-injection.md#the-container-protocol), BlackSheep
supports the use of other DI containers as replacements for the built-in
library used for dependency injection.

///

## Next

While authentication focuses on *identifying* users, authorization determines
Expand Down
77 changes: 77 additions & 0 deletions blacksheep/docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,80 @@ returns:
- Status [`403 Forbidden`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) if
authentication succeeded as valid credentials were provided, but the user is
not authorized to perform an action.


## Dependency Injection in authorization requirements

Dependency Injection is supported in authorization code. To use it:

1. Configure `Requirement` objects as types (not instances)
associated to the policies of the `AuthorizationStrategy` object.
2. Register dependencies in the DI container, and in the handler classes
according to the solution you are using for dependency injection.

The code below illustrates and example using the built-in solution for DI.

```python {linenums="1" hl_lines="13-14 17-18 21 41-42 45-46"}
from blacksheep import Application, Request, json
from guardpost import (
AuthenticationHandler,
AuthorizationContext,
Identity,
Policy,
Requirement,
)

app = Application(show_error_details=True)


class ExampleDependency:
pass


class MyInjectedRequirement(Requirement):
dependency: ExampleDependency

def handle(self, context: AuthorizationContext): # Note: this can also be async!
assert isinstance(self.dependency, ExampleDependency)
#
# TODO: implement here the authorization logic
#
roles = context.identity.claims.get("roles", [])
if roles and "ADMIN" in roles:
context.succeed(self)
else:
context.fail("The user is not an ADMIN")


class MyAuthenticationHandler(AuthenticationHandler):
def authenticate(self, context: Request) -> Identity | None:
# TODO: implement your own authentication logic
return Identity({"id": "example", "sub": "001", "roles": []}, self.scheme)


authentication = app.use_authentication()
authentication.add(MyAuthenticationHandler)

authorization = app.use_authorization()
authorization.with_default_policy(Policy("default", MyInjectedRequirement))

# We need to register the types in the DI container!
app.services.register(MyInjectedRequirement)
app.services.register(ExampleDependency)
app.services.register(MyAuthenticationHandler)


@app.router.get("/")
def home(request: Request):
assert request.user is not None
return json(request.user.claims)
```

/// admonition | ContainerProtocol.
type: tip

As documented in [_Container Protocol_](./dependency-injection.md#the-container-protocol), BlackSheep
supports the use of other DI containers as replacements for the built-in
library used for dependency injection.

///
170 changes: 158 additions & 12 deletions blacksheep/docs/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ This page describes:

- [X] Controller methods.
- [X] API Controllers.
- [X] Controllers inheritance.

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

/// admonition | For Flask users
/// admonition | For Flask users.
type: tip

If you come from Flask, controllers in BlackSheep can be considered
equivalent to Flask's Blueprints, as they allow to group request handlers
in dedicated modules and classes.

///

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

```python
from blacksheep import Application
from blacksheep.server.controllers import Controller, get


app = Application()

Expand All @@ -108,11 +114,12 @@ class Settings:
self.greetings = greetings


app.services.add_instance(Settings(value))


class Home(Controller):

def __init__(self, settings: Settings):
# controllers are instantiated dynamically at every web request
self.settings = settings
settings: Settings

async def on_request(self, request: Request):
print("[*] Received a request!!")
Expand All @@ -123,13 +130,6 @@ class Home(Controller):
@get("/")
async def index(self, request: Request):
return text(self.greet())

# when configuring the application, register
# a singleton of the application settings,
# this service is automatically injected into request handlers
# having a signature parameter type annotated `: Settings`, or
# having name "settings", without type annotations
app.services.add_instance(Settings(value))
```

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

```py
```python
from blacksheep.server.routing import RoutesRegistry


Expand All @@ -155,6 +155,35 @@ app.controllers_router = RoutesRegistry()
get = app.controllers_router.get
```

### route classmethod

The `route` `classmethod` can be used to define base routes for all request
handlers defined on a Controller class. In the following example, the actual
routes become: `/home` and `/home/about`.

```python
from blacksheep import Application
from blacksheep.server.controllers import Controller, get


app = Application()


class Home(Controller):

@classmethod
def route(cls):
return "/home/"

@get("/")
def home(self):
return self.ok({"message": "Hello!"})

@get("/about")
def about(self):
return self.ok({"message": "About..."})
```

## The APIController class

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

...
```

## Controllers inheritance

Since version `2.3.0`, the framework supports routes inheritance in controllers.
Consider the following example:

```python {linenums="1" hl_lines="8-9 12-13 15 21-21"}
from blacksheep import Application
from blacksheep.server.controllers import Controller, get


app = Application()


class BaseController(Controller):
path: ClassVar[str] = "base"

@classmethod
def route(cls) -> Optional[str]:
return f"/api/{cls.path}"

@get("/foo") # /api/base/foo
def foo(self):
return self.ok(self.__class__.__name__)


class Derived(BaseController):
path = "derived"

# /api/derived/foo (inherited from the base class)
```

In the example above, the following routes are configured:

- `/api/base/foo`, defined in `BaseController`
- `/api/derived/foo`, defined in `Derived`

To exclude the routes registered in a base controller class, decorate the class
using the `@abstract()` decorator imported from `blacksheep.server.controllers`.

```python
from blacksheep.server.controllers import Controller, abstract, get


@abstract()
class BaseController(Controller):
@get("/hello-world")
```

The following example illustrates a scenario in which a base class defines a
`/hello-world` route, inherited in sub-classes that each apply a different
base route. The `ControllerTwo` class defines one more route, which is also
inherited by `ControllerTwoBis`; and this last class defines one more specific
route.

```python
from blacksheep import Application
from blacksheep.server.controllers import Controller, abstract, get


app = Application()


@abstract()
class BaseController(Controller):
@get("/hello-world")
def index(self):
# Note: the route /hello-world itself will not be registered in the
# router, because this class is decorated with @abstract()
return self.text(f"Hello, World! {self.__class__.__name__}")


class ControllerOne(BaseController):
@classmethod
def route(cls) -> str:
return "/one"

# /one/hello-world


class ControllerTwo(BaseController):
@classmethod
def route(cls) -> str:
return "/two"

# /two/hello-world

@get("/specific-route") # /two/specific-route
def specific_route(self):
return self.text(f"This is a specific route in {self.__class__.__name__}")


class ControllerTwoBis(ControllerTwo):
@classmethod
def route(cls) -> str:
return "/two-bis"

# /two-bis/hello-world

# /two-bis/specific-route

@get("/specific-route-2") # /two-bis/specific-route-2
def specific_route(self):
return self.text(f"This is another route in {self.__class__.__name__}")
```

All routes of this example, with their respective response texts, are:

- `/one/hello-world` :material-arrow-right: "Hello, World! ControllerOne"
- `/two/hello-world` :material-arrow-right: "Hello, World! ControllerTwo"
- `/two-bis/hello-world` :material-arrow-right: "Hello, World! ControllerTwoBis"
- `/two/specific-route` :material-arrow-right: "This is a specific route in ControllerTwo"
- `/two-bis/specific-route` :material-arrow-right: "This is a specific route in ControllerTwoBis"
- `/two-bis/specific-route-2` :material-arrow-right: "This is another route in ControllerTwoBis"

Controller types and their dependencies are resolved appropriately for each
request handler,
Loading