Autorização
Você pode escrever verificações de autorização em seu aplicativo AdonisJS usando o pacote @adonisjs/bouncer
. O Bouncer fornece uma API JavaScript first para escrever verificações de autorização como habilidades e políticas.
O objetivo das habilidades e políticas é abstrair a lógica de autorizar uma ação para um único lugar e reutilizá-la no restante da base de código.
Habilidades são definidas como funções e podem ser uma ótima opção se seu aplicativo tiver menos verificações de autorização e mais simples.
Políticas são definidas como classes, e você deve criar uma política para cada recurso em seu aplicativo. As políticas também podem se beneficiar de
NOTA
O Bouncer não é uma implementação de RBAC ou ACL. Em vez disso, ele fornece uma API de baixo nível com controle refinado para autorizar ações em seus aplicativos AdonisJS.
Instalação
Instale e configure o pacote usando o seguinte comando:
node ace add @adonisjs/bouncer
Veja os passos realizados pelo comando add
Instala o pacote
@adonisjs/bouncer
usando o gerenciador de pacotes detectado.Registra o seguinte provedor de serviço e comando dentro do arquivo
adonisrc.ts
.ts{ commands: [ // ...outros comandos () => import('@adonisjs/bouncer/commands') ], providers: [ // ...outros provedores () => import('@adonisjs/bouncer/bouncer_provider') ] }
1
2
3
4
5
6
7
8
9
10Cria o arquivo
app/abilities/main.ts
para definir e exportar habilidades.Cria o arquivo
app/policies/main.ts
para exportar todas as políticas como uma coleção.Cria
initialize_bouncer_middleware
dentro do diretóriomiddleware
.Registre o seguinte middleware dentro do arquivo
start/kernel.ts
.tsrouter.use([ () => import('#middleware/initialize_bouncer_middleware') ])
1
2
3
DICA
Você aprende mais visualmente? - Confira a série de screencasts gratuitos AdonisJS Bouncer dos nossos amigos da Adocasts.
O middleware Initialize bouncer
Durante a configuração, criamos e registramos o middleware #middleware/initialize_bouncer_middleware
dentro do seu aplicativo. O middleware initialize é responsável por criar uma instância da classe Bouncer para o usuário atualmente autenticado e a compartilha por meio da propriedade ctx.bouncer
com o restante da solicitação.
Além disso, compartilhamos a mesma instância do Bouncer com os modelos do Edge usando o método ctx.view.share
. Sinta-se à vontade para remover as seguintes linhas de código do middleware se não estiver usando o Edge dentro do seu aplicativo.
NOTA
Você é o proprietário do código-fonte do seu aplicativo, incluindo os arquivos criados durante a configuração inicial. Portanto, não hesite em alterá-los e fazê-los funcionar com o ambiente do seu aplicativo.
async handle(ctx: HttpContext, next: NextFn) {
ctx.bouncer = new Bouncer(
() => ctx.auth.user || null,
abilities,
policies
).setContainerResolver(ctx.containerResolver)
/**
* Remover se não estiver usando o Edge
*/
if ('view' in ctx) {
ctx.view.share(ctx.bouncer.edgeHelpers)
}
return next()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Definindo habilidades
Habilidades são funções JavaScript geralmente escritas dentro do arquivo ./app/abilities/main.ts
. Você pode exportar várias habilidades deste arquivo.
No exemplo a seguir, definimos uma habilidade chamada editPost
usando o método Bouncer.ability
. O retorno de chamada de implementação deve retornar true
para autorizar o usuário e retornar false
para negar acesso.
NOTA
Uma habilidade deve sempre aceitar o Usuário
como o primeiro parâmetro, seguido por parâmetros adicionais necessários para a verificação de autorização.
// app/abilities/main.ts
import User from '#models/user'
import Post from '#models/post'
import { Bouncer } from '@adonisjs/bouncer'
export const editPost = Bouncer.ability((user: User, post: Post) => {
return user.id === post.userId
})
2
3
4
5
6
7
8
9
Executando autorização
Depois de definir uma habilidade, você pode executar uma verificação de autorização usando o método ctx.bouncer.allows
.
O Bouncer passará automaticamente o usuário atualmente logado para o callback de habilidade como o primeiro parâmetro, e você deve fornecer o restante dos parâmetros manualmente.
import Post from '#models/post'
import { editPost } from '#abilities/main'
import router from '@adonisjs/core/services/router'
router.put('posts/:id', async ({ bouncer, params, response }) => {
/**
* Encontre uma postagem por ID para que possamos executar uma
* verificação de autorização para ela.
*/
const post = await Post.findOrFail(params.id)
/**
* Use a capacidade de ver se o usuário logado
* tem permissão para executar a ação.
*/
if (await bouncer.allows(editPost, post)) {
return 'Você pode editar a postagem'
}
return response.forbidden('You cannot edit the post')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
O oposto do método bouncer.allows
é o método bouncer.denies
. Você pode preferir este método em vez de escrever uma declaração if not
.
if (await bouncer.denies(editPost, post)) {
response.abort('Your cannot edit the post', 403)
}
2
3
Permitindo usuários convidados
Por padrão, o Bouncer nega verificações de autorização para usuários não logados sem invocar o callback de habilidade.
No entanto, você pode querer definir certas habilidades que podem funcionar com um usuário convidado. Por exemplo, permitir que convidados visualizem postagens publicadas, mas permitir que o criador da postagem visualize rascunhos também.
Você pode definir uma habilidade que permita usuários convidados usando a opção allowGuest
. Neste caso, as opções serão definidas como o primeiro parâmetro, e o callback será o segundo parâmetro.
export const viewPost = Bouncer.ability(
{ allowGuest: true },
(user: User | null, post: Post) => {
/**
* Permitir que todos acessem postagens publicadas
*/
if (post.isPublished) {
return true
}
/**
* O convidado não pode visualizar postagens não publicadas
*/
if (!user) {
return false
}
/**
* O criador da postagem também pode visualizar postagens não publicadas.
*/
return user.id === post.userId
}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Autorizando usuários diferentes do usuário logado
Se você quiser autorizar um usuário diferente do usuário logado, você pode usar o construtor Bouncer
para criar uma nova instância do bouncer para um determinado usuário.
import User from '#models/user'
import { Bouncer } from '@adonisjs/bouncer'
const user = await User.findOrFail(1)
const bouncer = new Bouncer(user)
if (await bouncer.allows(editPost, post)) {
}
2
3
4
5
6
7
8
Definindo políticas
As políticas oferecem uma camada de abstração para organizar as verificações de autorização como classes. É recomendado criar uma política por recurso. Por exemplo, se seu aplicativo tiver um modelo Post, você deve criar uma classe PostPolicy
para autorizar ações como criar ou atualizar postagens.
As políticas são armazenadas dentro do diretório ./app/policies
, e cada arquivo representa uma única política. Você pode criar uma nova política executando o seguinte comando.
Veja também: Comando Make policy
node ace make:policy post
A classe policy estende a classe BasePolicy e você pode implementar métodos para as verificações de autorização que deseja executar. No exemplo a seguir, definimos verificações de autorização para criar
, editar
e excluir
uma postagem.
// app/policies/post_policy.ts
import User from '#models/user'
import Post from '#models/post'
import { BasePolicy } from '@adonisjs/bouncer'
import { AuthorizerResponse } from '@adonisjs/bouncer/types'
export default class PostPolicy extends BasePolicy {
/**
* Cada usuário logado pode criar uma postagem
*/
create(user: User): AuthorizerResponse {
return true
}
/**
* Somente o criador da postagem pode editar a postagem
*/
edit(user: User, post: Post): AuthorizerResponse {
return user.id === post.userId
}
/**
* Somente o criador da postagem pode excluir a postagem
*/
delete(user: User, post: Post): AuthorizerResponse {
return user.id === post.userId
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Executando autorização
Depois de criar uma política, você pode usar o método bouncer.with
para especificar a política que deseja usar para autorização e, em seguida, encadear os métodos bouncer.allows
ou bouncer.denies
para executar a verificação de autorização.
NOTA
Os métodos allows
e denies
encadeados após os métodos bouncer.with
são seguros para o tipo e mostrarão uma lista de conclusões com base nos métodos que você definiu na classe de política.
import Post from '#models/post'
import PostPolicy from '#policies/post_policy'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async create({ bouncer, response }: HttpContext) {
if (await bouncer.with(PostPolicy).denies('create')) {
return response.forbidden('Cannot create a post')
}
// Continue com a lógica do controlador
}
async edit({ bouncer, params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
if (await bouncer.with(PostPolicy).denies('edit', post)) {
return response.forbidden('Cannot edit the post')
}
// Continue com a lógica do controlador
}
async delete({ bouncer, params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
if (await bouncer.with(PostPolicy).denies('delete', post)) {
return response.forbidden('Cannot delete the post')
}
// Continue com a lógica do controlador
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Permitindo usuários convidados
Semelhante a capabilities, as políticas também podem definir verificações de autorização para usuários convidados usando o decorador @allowGuest
. Por exemplo:
import User from '#models/user'
import Post from '#models/post'
import { BasePolicy, allowGuest } from '@adonisjs/bouncer'
import type { AuthorizerResponse } from '@adonisjs/bouncer/types'
export default class PostPolicy extends BasePolicy {
@allowGuest()
view(user: User | null, post: Post): AuthorizerResponse {
/**
* Permitir que todos acessem postagens publicadas
*/
if (post.isPublished) {
return true
}
/**
* O convidado não pode visualizar postagens não publicadas
*/
if (!user) {
return false
}
/**
* O criador da postagem também pode visualizar postagens não publicadas.
*/
return user.id === post.userId
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Ganchos de política
Você pode definir os métodos de modelo before
e after
em uma classe de política para executar ações em torno de uma verificação de autorização. Um caso de uso comum é sempre permitir ou negar acesso a um determinado usuário.
NOTA
Os métodos before
e after
são sempre invocados, independentemente de um usuário conectado. Portanto, certifique-se de lidar com o caso em que o valor de user
será null
.
A resposta de before
é interpretada da seguinte forma.
- O valor
true
será considerado autorização bem-sucedida, e o método de ação não será chamado. - O valor
false
será considerado acesso negado, e o método de ação não será chamado. - Com um valor de retorno
undefined
, o bouncer executará o método de ação para realizar a verificação de autorização.
export default class PostPolicy extends BasePolicy {
async before(user: User | null, action: string, ...params: any[]) {
/**
* Sempre permitir um usuário administrador sem realizar nenhuma verificação
*/
if (user && user.isAdmin) {
return true
}
}
}
2
3
4
5
6
7
8
9
10
O método after
recebe a resposta bruta do método de ação e pode substituir a resposta anterior retornando um novo valor. A resposta de after
é interpretada da seguinte forma.
- O valor
true
será considerado autorização bem-sucedida, e a resposta antiga será descartada. - O valor
false
será considerado acesso negado, e a resposta antiga será descartada. - Com um valor de retorno
undefined
, o bouncer continuará a usar a resposta antiga.
import { AuthorizerResponse } from '@adonisjs/bouncer/types'
export default class PostPolicy extends BasePolicy {
async after(
user: User | null,
action: string,
response: AuthorizerResponse,
...params: any[]
) {
if (user && user.isAdmin) {
return true
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Injeção de dependência
As classes de política são criadas usando o contêiner IoC; portanto, você pode dar uma dica de tipo e injetar dependências dentro do construtor de política usando o decorador @inject
.
import { inject } from '@adonisjs/core'
import { PermissionsResolver } from '#services/permissions_resolver'
@inject()
export class PostPolicy extends BasePolicy {
constructor(
protected permissionsResolver: PermissionsResolver
) {
super()
}
}
2
3
4
5
6
7
8
9
10
11
Se uma classe de política for criada durante uma solicitação HTTP, você também pode injetar uma instância de HttpContext dentro dela.
import { HttpContext } from '@adonisjs/core/http'
import { PermissionsResolver } from '#services/permissions_resolver'
@inject()
export class PostPolicy extends BasePolicy {
constructor(protected ctx: HttpContext) {
super()
}
}
2
3
4
5
6
7
8
9
Lançando AuthorizationException
Juntamente com os métodos allows
e denies
, você pode usar o método bouncer.authorize
para executar a verificação de autorização. Este método lançará a AuthorizationException quando a verificação falhar.
router.put('posts/:id', async ({ bouncer, params }) => {
const post = await Post.findOrFail(post)
await bouncer.authorize(editPost, post)
/**
* Se nenhuma exceção foi levantada, você pode considerar que o usuário
* tem permissão para editar a postagem.
*/
})
2
3
4
5
6
7
8
9
O AdonisJS converterá a AuthorizationException
em uma resposta HTTP 403 - Forbidden
usando as seguintes regras de negociação de conteúdo.
As solicitações HTTP com o cabeçalho
Accept=application/json
receberão uma matriz de mensagens de erro. Cada elemento da matriz será um objeto com a propriedademessage
.JSON API spec.
Páginas de status para mostrar uma página de erro personalizada para erros de autorização.
Você também pode automanipular erros AuthorizationException
dentro do manipulador de exceção global.
import { errors } from '@adonisjs/bouncer'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
protected debug = !app.inProduction
protected renderStatusPages = app.inProduction
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof errors.E_AUTHORIZATION_FAILURE) {
return ctx
.response
.status(error.status)
.send(error.getResponseMessage(ctx))
}
return super.handle(error, ctx)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Personalizando a resposta de autorização
Em vez de retornar um valor booleano de habilidades e políticas, você pode construir uma resposta de erro usando a classe AuthorizationResponse.
A classe AuthorizationResponse
fornece controle refinado para definir um código de status HTTP personalizado e a mensagem de erro.
import User from '#models/user'
import Post from '#models/post'
import { Bouncer, AuthorizationResponse } from '@adonisjs/bouncer'
export const editPost = Bouncer.ability((user: User, post: Post) => {
if (user.id === post.userId) {
return true
}
return AuthorizationResponse.deny('Post not found', 404)
})
2
3
4
5
6
7
8
9
10
11
Se você estiver usando o pacote @adonisjs/i18n, você pode retornar uma resposta localizada usando o método .t
. A mensagem de tradução será usada sobre a mensagem padrão durante uma solicitação HTTP com base no idioma do usuário.
export const editPost = Bouncer.ability((user: User, post: Post) => {
if (user.id === post.userId) {
return true
}
return AuthorizationResponse
.deny('Post not found', 404) // mensagem padrão
.t('errors.not_found') // identificador de tradução
})
2
3
4
5
6
7
8
9
Usando um construtor de resposta personalizado
A flexibilidade para definir mensagens de erro personalizadas para verificações de autorização individuais é ótima. No entanto, se você sempre quiser retornar a mesma resposta, pode ser complicado repetir o mesmo código todas as vezes.
Portanto, você pode substituir o construtor de resposta padrão para o Bouncer da seguinte forma.
import { Bouncer, AuthorizationResponse } from '@adonisjs/bouncer'
Bouncer.responseBuilder = (response: boolean | AuthorizationResponse) => {
if (response instanceof AuthorizationResponse) {
return response
}
if (response === true) {
return AuthorizationResponse.allow()
}
return AuthorizationResponse
.deny('Resource not found', 404)
.t('errors.not_found')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Pré-registrando habilidades e políticas
Até agora, neste guia, importamos explicitamente uma habilidade ou uma política sempre que queremos usá-la. No entanto, depois de pré-registrá-los, você pode referenciar uma habilidade ou uma política pelo seu nome como uma string.
Pré-registrar habilidades e políticas pode ser menos útil dentro da sua base de código TypeScript do que apenas limpar as importações. No entanto, eles oferecem DX muito melhor dentro dos modelos Edge.
Veja os seguintes exemplos de código de modelos Edge com e sem pré-registro de uma política.
Sem pré-registro. Não, não é super limpo
{{-- Primeiro importe a habilidade --}}
@let(editPost = (await import('#abilities/main')).editPost)
@can(editPost, post)
{{-- Pode editar postagem --}}
@end
2
3
4
5
6
Com pré-registro
{{-- Nome da habilidade de referência como uma string --}}
@can('editPost', post)
{{-- Pode editar a postagem --}}
@end
2
3
4
Se você abrir o arquivo initialize_bouncer_middleware.ts
, você nos verá já importando e pré-registrando habilidades e políticas ao criar a instância do Bouncer.
import * as abilities from '#abilities/main'
import { policies } from '#policies/main'
export default InitializeBouncerMiddleware {
async handle(ctx, next) {
ctx.bouncer = new Bouncer(
() => ctx.auth.user,
abilities,
policies
)
}
}
2
3
4
5
6
7
8
9
10
11
12
Pontos a serem observados
Se você decidir definir habilidades em outras partes da sua base de código, certifique-se de importá-las e pré-registrá-las dentro do middleware.
No caso de políticas, toda vez que você executar o comando
make:policy
, certifique-se de aceitar o prompt para registrar a política dentro da coleção de políticas. A coleção de políticas é definida dentro do arquivo./app/policies/main.ts
.
// app/policies/main.ts
export const policies = {
PostPolicy: () => import('#policies/post_policy'),
CommentPolicy: () => import('#policies/comment_policy')
}
2
3
4
5
6
Referenciando habilidades e políticas pré-registradas
No exemplo a seguir, nos livramos das importações e referenciamos habilidades e políticas por seus nomes. Observe que a API baseada em string também é segura para tipos, mas o recurso "Ir para definição" do seu editor de código pode não funcionar.
// Exemplo de uso de habilidade
import { editPost } from '#abilities/main'
router.put('posts/:id', async ({ bouncer, params, response }) => {
const post = await Post.findOrFail(params.id)
if (await bouncer.allows(editPost, post)) {
if (await bouncer.allows('editPost', post)) {
return 'You can edit the post'
}
})
2
3
4
5
6
7
8
9
10
11
12
// Exemplo de uso de política
import PostPolicy from '#policies/post_policy'
export default class PostsController {
async create({ bouncer, response }: HttpContext) {
if (await bouncer.with(PostPolicy).denies('create')) {
if (await bouncer.with('PostPolicy').denies('create')) {
return response.forbidden('Cannot create a post')
}
// Continue com a lógica do controlador
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Verificações de autorização dentro de modelos Edge
Antes de poder executar verificações de autorização dentro de modelos Edge, certifique-se de pré-registrar habilidades e políticas. Uma vez feito isso, você pode usar as tags @can
e @cannot
para executar as verificações de autorização.
Essas tags aceitam o nome ability
ou o nome policy.method
como o primeiro parâmetro, seguido pelo restante dos parâmetros aceitos pela habilidade ou uma política.
// Uso com habilidade
@can('editPost', post)
{{-- Pode editar postagem --}}
@end
@cannot('editPost', post)
{{-- Não pode editar postagem --}}
@end
2
3
4
5
6
7
8
9
// Uso com política
@can('PostPolicy.edit', post)
{{-- Pode editar postagem --}}
@end
@cannot('PostPolicy.edit', post)
{{-- Não pode editar postagem --}}
@end
2
3
4
5
6
7
8
9
Eventos
Consulte o guia de referência de eventos para visualizar a lista de eventos despachados pelo pacote @adonisjs/bouncer
.