Entendeno Serializers no Django: A Ponte Entre Seus Modelos e a API Moderna Parte 4
- Published on
- Published on
- /8 mins read/---
Tutorial 4: Autenticação e Permissões - Django REST Framework
Atualmente, nossa API não tem restrições sobre quem pode editar ou excluir trechos de código. Gostaríamos de ter um comportamento mais avançado para garantir que:
- Os trechos de código estejam sempre associados a um criador.
- Somente usuários autenticados possam criar snippets.
- Somente o criador de um snippet possa atualizá-lo ou excluí-lo.
- Requisições não autenticadas devam ter acesso completo somente leitura.
Adicionando Informações ao Nosso Modelo
Faremos algumas alterações em nossa classe de modelo Snippet
. Primeiro, vamos adicionar alguns campos. Um desses campos será usado para representar o usuário que criou o trecho de código. O outro campo será usado para armazenar a representação HTML destacada do código.
Adicione os dois campos a seguir ao modelo Snippet
em models.py
.
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
Também precisamos garantir que, quando o modelo for salvo, preenchamos o campo highlighted
, usando a biblioteca de destaque de código pygments
.
Precisaremos de algumas importações extras:
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
E agora podemos adicionar um método .save()
à nossa classe de modelo:
def save(self, *args, **kwargs):
"""
Usa a biblioteca `pygments` para criar uma representação HTML
destacada do trecho de código.
"""
lexer = get_lexer_by_name(self.language)
linenos = 'table' if self.linenos else False
options = {'title': self.title} if self.title else {}
formatter = HtmlFormatter(style=self.style, linenos=linenos,
full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super().save(*args, **kwargs)
Quando tudo isso estiver pronto, precisaremos atualizar nossas tabelas do banco de dados. Normalmente, criaríamos uma migração de banco de dados para fazer isso, mas, para os fins deste tutorial, vamos apenas excluir o banco de dados e começar de novo.
rm -f db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate
Você também pode querer criar alguns usuários diferentes, para usar para testar a API. A maneira mais rápida de fazer isso é com o comando createsuperuser
.
python manage.py createsuperuser
Adicionando Endpoints para Nossos Modelos de Usuário
Agora que temos alguns usuários para trabalhar, é melhor adicionarmos representações desses usuários à nossa API. Criar um novo serializador é fácil. Em serializers.py
, adicione:
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'snippets']
Como 'snippets' é um relacionamento reverso no modelo User
, ele não será incluído por padrão ao usar a classe ModelSerializer
, então precisamos adicionar um campo explícito para ele.
Também adicionaremos algumas views a views.py
. Gostaríamos de usar apenas views de leitura para as representações do usuário, então usaremos as views genéricas baseadas em classes ListAPIView
e RetrieveAPIView
.
from django.contrib.auth.models import User
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
Certifique-se de importar também a classe UserSerializer
:
from snippets.serializers import UserSerializer
Finalmente, precisamos adicionar essas views à API, referenciando-as a partir do URL conf. Adicione o seguinte aos padrões em snippets/urls.py
.
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
Associando Snippets a Usuários
No momento, se criássemos um trecho de código, não haveria como associar o usuário que criou o snippet à instância do snippet. O usuário não é enviado como parte da representação serializada, mas é uma propriedade da requisição de entrada.
A maneira como lidamos com isso é substituindo um método .perform_create()
em nossas views de snippet, que nos permite modificar como o salvamento da instância é gerenciado e lidar com qualquer informação que esteja implícita na requisição de entrada ou URL solicitada.
Na classe de view SnippetList
, adicione o seguinte método:
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
O método create()
do nosso serializador agora receberá um campo 'owner' adicional, junto com os dados validados da requisição.
Atualizando Nosso Serializador
Agora que os snippets estão associados ao usuário que os criou, vamos atualizar nosso SnippetSerializer
para refletir isso. Adicione o seguinte campo à definição do serializador em serializers.py
:
owner = serializers.ReadOnlyField(source='owner.username')
Nota: Certifique-se de adicionar também 'owner'
à lista de campos na classe Meta
interna.
Este campo está fazendo algo bastante interessante. O argumento source
controla qual atributo é usado para preencher um campo e pode apontar para qualquer atributo na instância serializada. Ele também pode usar a notação com ponto mostrada acima, caso em que percorrerá os atributos fornecidos, de forma semelhante à usada com a linguagem de template do Django.
O campo que adicionamos é a classe ReadOnlyField
não tipada, em contraste com os outros campos tipados, como CharField
, BooleanField
etc... O ReadOnlyField
não tipado é sempre somente leitura e será usado para representações serializadas, mas não será usado para atualizar instâncias de modelo quando forem desserializadas. Também poderíamos ter usado CharField(read_only=True)
aqui.
Adicionando Permissões Necessárias às Views
Agora que os trechos de código estão associados aos usuários, queremos garantir que apenas usuários autenticados possam criar, atualizar e excluir trechos de código.
O REST framework inclui várias classes de permissão que podemos usar para restringir quem pode acessar uma determinada view. Neste caso, o que estamos procurando é IsAuthenticatedOrReadOnly
, que garantirá que as requisições autenticadas obtenham acesso de leitura e gravação e as requisições não autenticadas obtenham acesso somente leitura.
Primeiro, adicione a seguinte importação no módulo de views:
from rest_framework import permissions
Em seguida, adicione a seguinte propriedade às classes de view SnippetList
e SnippetDetail
.
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
Adicionando Login à API Navegável
Se você abrir um navegador e navegar para a API navegável no momento, descobrirá que não poderá mais criar novos trechos de código. Para fazer isso, precisaríamos fazer login como um usuário.
Podemos adicionar uma view de login para uso com a API navegável, editando o URLconf em nosso arquivo urls.py
no nível do projeto.
Adicione a seguinte importação na parte superior do arquivo:
from django.urls import path, include
E, no final do arquivo, adicione um padrão para incluir as views de login e logout para a API navegável.
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
A parte 'api-auth/'
do padrão pode ser qualquer URL que você queira usar.
Agora, se você abrir o navegador novamente e atualizar a página, verá um link 'Login' no canto superior direito da página. Se você fizer login como um dos usuários que criou anteriormente, poderá criar trechos de código novamente.
Depois de criar alguns trechos de código, navegue até o endpoint '/users/' e observe que a representação inclui uma lista dos IDs de snippet associados a cada usuário, no campo 'snippets' de cada usuário.
Permissões em Nível de Objeto
Na verdade, gostaríamos que todos os trechos de código fossem visíveis para qualquer pessoa, mas também garantir que apenas o usuário que criou um trecho de código possa atualizá-lo ou excluí-lo.
Para fazer isso, precisaremos criar uma permissão personalizada.
No aplicativo snippets, crie um novo arquivo, permissions.py
.
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Permissão personalizada para permitir apenas que os proprietários de um
objeto o editem.
"""
def has_object_permission(self, request, view, obj):
# As permissões de leitura são permitidas para qualquer requisição,
# então sempre permitiremos requisições GET, HEAD ou OPTIONS.
if request.method in permissions.SAFE_METHODS:
return True
# As permissões de escrita são permitidas apenas para o proprietário
# do snippet.
return obj.owner == request.user
Agora podemos adicionar essa permissão personalizada ao nosso endpoint de instância de snippet, editando a propriedade permission_classes
na classe de view SnippetDetail
:
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]
Certifique-se de importar também a classe IsOwnerOrReadOnly
.
from snippets.permissions import IsOwnerOrReadOnly
Agora, se você abrir um navegador novamente, descobrirá que as ações 'DELETE' e 'PUT' só aparecem em um endpoint de instância de snippet se você estiver logado como o mesmo usuário que criou o trecho de código.
Autenticando com a API
Como agora temos um conjunto de permissões na API, precisamos autenticar nossas solicitações a ela se quisermos editar qualquer snippet. Não configuramos nenhuma classe de autenticação, então os padrões são aplicados no momento, que são SessionAuthentication
e BasicAuthentication
.
Quando interagimos com a API por meio do navegador da web, podemos fazer login, e a sessão do navegador fornecerá a autenticação necessária para as solicitações. Se estivermos interagindo com a API programaticamente, precisamos fornecer explicitamente as credenciais de autenticação em cada solicitação. Se tentarmos criar um snippet sem autenticar, receberemos um erro:
http POST http://127.0.0.1:8000/snippets/ code="print(123)"
{
"detail": "Authentication credentials were not provided."
}
Podemos fazer uma solicitação bem-sucedida incluindo o nome de usuário e a senha de um dos usuários que criamos anteriormente.
http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"
{
"id": 1,
"owner": "admin",
"title": "foo",
"code": "print(789)",
"linenos": false,
"language": "python",
"style": "friendly"
}
Resumo
Agora temos um conjunto bastante granular de permissões em nossa API Web e endpoints para usuários do sistema e para os trechos de código que eles criaram.
Na parte 5 (substitua pelo link real) do tutorial, veremos como podemos unir tudo criando um endpoint HTML para nossos snippets destacados e melhorar a coesão de nossa API usando hiperlinks para os relacionamentos dentro do sistema.