Browse Source

Adiciona autenticação via Token (#3151)

* Adiciona autenticação via Token

* Adiciona token nos usuários existentes

* Adiciona token automaticamente nos novos usuários

* Adiciona campo para somente leitura com o token do usuário na edição do usuário

* Adiciona função para renovar token do usuário autenticado

* Adiciona botão para renovar token

* Corrige com mudanças solicitadas

* Padroniza nome do html

* Cria página para visualização de perfil

* Redireciona para perfil do usuário quando é criado

* Altera url para página de detalhe do usuário

* Adiciona botões para pesquisa e edição de usuário

* Corrige model de Pesquisa Usuário

* Redireciona pra tela de detalhe

* Altera forma para criar objeto ou estender dicionário

* Corrige ajax para post e id do usuário dono do token

* Adiciona roles in rows

* Adiciona botao de cancelar em editar usuario

* Conserta localizacao de templates HTML

Co-authored-by: eribeiro <edwardr@senado.leg.br>
Co-authored-by: Vinícius Cantuária <cantuariavc@gmail.com>
pull/3008/head
Edward 5 years ago
committed by GitHub
parent
commit
8975f33abb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      docs/token-auth.rst
  2. 26
      sapl/api/migrations/0001_initial.py
  3. 0
      sapl/api/migrations/__init__.py
  4. 5
      sapl/api/urls.py
  5. 39
      sapl/api/views.py
  6. 19
      sapl/base/forms.py
  7. 3
      sapl/base/urls.py
  8. 73
      sapl/base/views.py
  9. 18
      sapl/settings.py
  10. 2
      sapl/templates/auth/user_filter.html
  11. 57
      sapl/templates/base/usuario_detail.html
  12. 20
      sapl/templates/base/usuario_edit.html

17
docs/token-auth.rst

@ -0,0 +1,17 @@
1. Realizar o migrate
./manage.py migrate
2. Criar um API Token para usuário e anotar a API Key gerada.
python3 manage.py drf_create_token admin
3. Testar endpoint
curl http://localhost:8000/api/version -H 'Authorization: Token <API Key>'
4. Exemplo de POST
curl -d '{"nome_completo”:”Gozer The Gozerian“, "nome_parlamentar": “Gozer”, "sexo":"M"}' -X POST http://localhost:8000/api/parlamentares/parlamentar/ -H 'Authorization: Token <API Key>' -H 'Content-Type: application/json'
Note: If you use TokenAuthentication in production you must ensure that your API is only available over https.
References: https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication

26
sapl/api/migrations/0001_initial.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-04-27 17:40
from __future__ import unicode_literals
from django.db import migrations
from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework.authtoken.models import Token
def adiciona_token_de_usuarios(apps, schema_editor):
for user in get_user_model().objects.all():
Token.objects.get_or_create(user=user)
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RunPython(adiciona_token_de_usuarios)
]

0
sapl/api/migrations/__init__.py

5
sapl/api/urls.py

@ -6,7 +6,7 @@ from rest_framework.routers import DefaultRouter
from sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet,\ from sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet,\
AutoresProvaveisListView, AutoresPossiveisListView, AutorListView,\ AutoresProvaveisListView, AutoresPossiveisListView, AutorListView,\
ModelChoiceView ModelChoiceView
from sapl.api.views import SaplApiViewSetConstrutor from sapl.api.views import SaplApiViewSetConstrutor, AppVersionView, recria_token
from .apps import AppConfig from .apps import AppConfig
@ -70,7 +70,8 @@ urlpatterns = [
url(r'^api/', include(deprecated_urlpatterns_api)), url(r'^api/', include(deprecated_urlpatterns_api)),
url(r'^api/', include(urlpatterns_api_doc)), url(r'^api/', include(urlpatterns_api_doc)),
url(r'^api/', include(urlpatterns_router)), url(r'^api/', include(urlpatterns_router)),
url(r'^api/version', AppVersionView.as_view()),
url(r'^api/recriar-token/(?P<pk>\d*)$', recria_token, name="recria_token"),
# implementar caminho para autenticação # implementar caminho para autenticação
# https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/ # https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/

39
sapl/api/views.py

@ -3,8 +3,11 @@ import logging
from django import apps from django import apps
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse_lazy
from django.db.models import Q from django.db.models import Q
from django.db.models.fields.files import FileField from django.db.models.fields.files import FileField
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -16,10 +19,14 @@ from django_filters.utils import resolve_field
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers as rest_serializers from rest_framework import serializers as rest_serializers
from rest_framework.decorators import action from rest_framework.authtoken.models import Token
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.views import APIView
from sapl.api.forms import SaplFilterSetMixin from sapl.api.forms import SaplFilterSetMixin
from sapl.api.permissions import SaplModelPermissions from sapl.api.permissions import SaplModelPermissions
@ -36,6 +43,21 @@ from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria
from sapl.parlamentares.models import Mandato, Parlamentar, Legislatura from sapl.parlamentares.models import Mandato, Parlamentar, Legislatura
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
@api_view(['POST'])
@permission_classes([IsAdminUser])
def recria_token(request, pk):
Token.objects.get(user_id=pk).delete()
token = Token.objects.create(user_id=pk)
return Response({"message": "Token recriado com sucesso!", "token": token.key})
class BusinessRulesNotImplementedMixin: class BusinessRulesNotImplementedMixin:
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
raise Exception(_("POST Create não implementado")) raise Exception(_("POST Create não implementado"))
@ -587,3 +609,18 @@ class _NormaJuridicaViewset:
def destaques(self, request, *args, **kwargs): def destaques(self, request, *args, **kwargs):
self.queryset = self.get_queryset().filter(norma_de_destaque=True) self.queryset = self.get_queryset().filter(norma_de_destaque=True)
return self.list(request, *args, **kwargs) return self.list(request, *args, **kwargs)
class AppVersionView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request):
content = {
'name': 'SAPL',
'description': 'Sistema de Apoio ao Processo Legislativo',
'version': settings.SAPL_VERSION,
'user': request.user.username,
'is_authenticated': request.user.is_authenticated(),
}
return Response(content)

19
sapl/base/forms.py

@ -177,6 +177,11 @@ class UsuarioEditForm(ModelForm):
# ROLES = [(g.id, g.name) for g in Group.objects.all().order_by('name')] # ROLES = [(g.id, g.name) for g in Group.objects.all().order_by('name')]
ROLES = [] ROLES = []
token = forms.CharField(
required=False,
label="Token",
max_length=40,
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
first_name = forms.CharField( first_name = forms.CharField(
required=False, required=False,
label="Nome", label="Nome",
@ -206,6 +211,7 @@ class UsuarioEditForm(ModelForm):
model = get_user_model() model = get_user_model()
fields = [ fields = [
get_user_model().USERNAME_FIELD, get_user_model().USERNAME_FIELD,
"token",
"first_name", "first_name",
"last_name", "last_name",
'password1', 'password1',
@ -220,19 +226,24 @@ class UsuarioEditForm(ModelForm):
super(UsuarioEditForm, self).__init__(*args, **kwargs) super(UsuarioEditForm, self).__init__(*args, **kwargs)
rows = to_row(( rows = to_row((
('username', 12),
('first_name', 6), ('first_name', 6),
('last_name', 6), ('last_name', 6),
('email', 6), ('email', 6),
('user_active', 6), ('user_active', 6),
('password1', 6), ('password1', 6),
('password2', 6))) ('password2', 6),
('roles', 12)))
self.helper = SaplFormHelper() self.helper = SaplFormHelper()
self.helper.layout = Layout( self.helper.layout = Layout(
'username',
FieldWithButtons('token', StrictButton('Renovar', id="renovar-token", css_class="btn-outline-primary")),
rows, rows,
'roles', form_actions(
form_actions(label='Salvar Alterações')) more=[
HTML("<a href='{% url 'sapl.base:user_detail' object.pk %}' "
"class='btn btn-dark'>Cancelar</a>")],
label='Salvar Alterações'))
def clean(self): def clean(self):
super().clean() super().clean()

3
sapl/base/urls.py

@ -8,7 +8,7 @@ from django.contrib.auth.views import (password_reset, password_reset_complete,
password_reset_done) password_reset_done)
from django.views.generic.base import RedirectView, TemplateView from django.views.generic.base import RedirectView, TemplateView
from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud, get_estatistica from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud, get_estatistica, DetailUsuarioView
from sapl.settings import EMAIL_SEND_USER, MEDIA_URL from sapl.settings import EMAIL_SEND_USER, MEDIA_URL
from .apps import AppConfig from .apps import AppConfig
@ -45,6 +45,7 @@ app_name = AppConfig.name
admin_user = [ admin_user = [
url(r'^sistema/usuario/$', PesquisarUsuarioView.as_view(), name='usuario'), url(r'^sistema/usuario/$', PesquisarUsuarioView.as_view(), name='usuario'),
url(r'^sistema/usuario/create$', CreateUsuarioView.as_view(), name='user_create'), url(r'^sistema/usuario/create$', CreateUsuarioView.as_view(), name='user_create'),
url(r'^sistema/usuario/(?P<pk>\d+)$', DetailUsuarioView.as_view(), name='user_detail'),
url(r'^sistema/usuario/(?P<pk>\d+)/edit$', EditUsuarioView.as_view(), name='user_edit'), url(r'^sistema/usuario/(?P<pk>\d+)/edit$', EditUsuarioView.as_view(), name='user_edit'),
url(r'^sistema/usuario/(?P<pk>\d+)/delete$', DeleteUsuarioView.as_view(), name='user_delete') url(r'^sistema/usuario/(?P<pk>\d+)/delete$', DeleteUsuarioView.as_view(), name='user_delete')
] ]

73
sapl/base/views.py

@ -24,8 +24,7 @@ from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import string_concat from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import (CreateView, DeleteView, FormView, ListView, from django.views.generic import (CreateView, DetailView, DeleteView, FormView, ListView, UpdateView)
UpdateView)
from django.views.generic.base import RedirectView, TemplateView from django.views.generic.base import RedirectView, TemplateView
from django_filters.views import FilterView from django_filters.views import FilterView
from haystack.views import SearchView from haystack.views import SearchView
@ -79,6 +78,8 @@ from .forms import (AlterarSenhaForm, CasaLegislativaForm,
RelatorioNormasPorAutorFilterSet) RelatorioNormasPorAutorFilterSet)
from .models import AppConfig, CasaLegislativa from .models import AppConfig, CasaLegislativa
from rest_framework.authtoken.models import Token
def get_casalegislativa(): def get_casalegislativa():
return CasaLegislativa.objects.first() return CasaLegislativa.objects.first()
@ -1749,7 +1750,7 @@ class ListarProtocolosDuplicadosView(PermissionRequiredMixin, ListView):
class PesquisarUsuarioView(PermissionRequiredMixin, FilterView): class PesquisarUsuarioView(PermissionRequiredMixin, FilterView):
model = User model = get_user_model()
filterset_class = UsuarioFilterSet filterset_class = UsuarioFilterSet
permission_required = ('base.list_appconfig',) permission_required = ('base.list_appconfig',)
paginate_by = 10 paginate_by = 10
@ -1768,18 +1769,16 @@ class PesquisarUsuarioView(PermissionRequiredMixin, FilterView):
return kwargs return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PesquisarUsuarioView, context = super(PesquisarUsuarioView, self).get_context_data(**kwargs)
self).get_context_data(**kwargs)
paginator = context['paginator'] paginator = context['paginator']
page_obj = context['page_obj'] page_obj = context['page_obj']
context['page_range'] = make_pagination( context.update({
page_obj.number, paginator.num_pages) "page_range": make_pagination(page_obj.number, paginator.num_pages),
"NO_ENTRIES_MSG": "Nenhum usuário encontrado!",
context['NO_ENTRIES_MSG'] = 'Nenhum usuário encontrado!' "title": _("Usuários")
})
context['title'] = _('Usuários')
return context return context
@ -1806,6 +1805,28 @@ class PesquisarUsuarioView(PermissionRequiredMixin, FilterView):
return self.render_to_response(context) return self.render_to_response(context)
class DetailUsuarioView(PermissionRequiredMixin, DetailView):
model = get_user_model()
template_name = "base/usuario_detail.html"
permission_required = ('base.detail_appconfig',)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = get_user_model().objects.get(id=self.kwargs['pk'])
context.update({
"user": user,
"token": Token.objects.filter(user=user)[0],
"roles": [
{
"checked": "checked" if g in user.groups.all() else "unchecked",
"group": g.name
} for g in Group.objects.all().order_by("name")]
})
return context
class CreateUsuarioView(PermissionRequiredMixin, CreateView): class CreateUsuarioView(PermissionRequiredMixin, CreateView):
model = get_user_model() model = get_user_model()
form_class = UsuarioCreateForm form_class = UsuarioCreateForm
@ -1813,21 +1834,21 @@ class CreateUsuarioView(PermissionRequiredMixin, CreateView):
fail_message = 'Usuário não criado!' fail_message = 'Usuário não criado!'
permission_required = ('base.add_appconfig',) permission_required = ('base.add_appconfig',)
def get_success_url(self): def get_success_url(self, pk):
return reverse('sapl.base:usuario') return reverse('sapl.base:user_detail', kwargs={"pk": pk})
def form_valid(self, form): def form_valid(self, form):
data = form.cleaned_data data = form.cleaned_data
new_user = get_user_model().objects.create( new_user = get_user_model().objects.create(
username=data['username'], username=data['username'],
email=data['email'] email=data['email'],
first_name=data['firstname'],
last_name=data['lastname'],
is_superuser=False,
is_staff=False
) )
new_user.first_name = data['firstname']
new_user.last_name = data['lastname']
new_user.set_password(data['password1']) new_user.set_password(data['password1'])
new_user.is_superuser = False
new_user.is_staff = False
new_user.save() new_user.save()
groups = Group.objects.filter(id__in=data['roles']) groups = Group.objects.filter(id__in=data['roles'])
@ -1835,7 +1856,7 @@ class CreateUsuarioView(PermissionRequiredMixin, CreateView):
g.user_set.add(new_user) g.user_set.add(new_user)
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url(new_user.pk))
def form_invalid(self, form): def form_invalid(self, form):
messages.error(self.request, self.fail_message) messages.error(self.request, self.fail_message)
@ -1876,11 +1897,12 @@ class DeleteUsuarioView(PermissionRequiredMixin, DeleteView):
class EditUsuarioView(PermissionRequiredMixin, UpdateView): class EditUsuarioView(PermissionRequiredMixin, UpdateView):
model = get_user_model() model = get_user_model()
form_class = UsuarioEditForm form_class = UsuarioEditForm
template_name = "base/usuario_edit.html"
success_message = 'Usuário editado com sucesso!' success_message = 'Usuário editado com sucesso!'
permission_required = ('base.change_appconfig',) permission_required = ('base.change_appconfig',)
def get_success_url(self): def get_success_url(self):
return reverse('sapl.base:usuario') return reverse('sapl.base:user_detail', kwargs={"pk": self.kwargs['pk']})
def get_initial(self): def get_initial(self):
initial = super().get_initial() initial = super().get_initial()
@ -1888,10 +1910,13 @@ class EditUsuarioView(PermissionRequiredMixin, UpdateView):
user = get_user_model().objects.get(id=self.kwargs['pk']) user = get_user_model().objects.get(id=self.kwargs['pk'])
roles = [str(g.id) for g in user.groups.all()] roles = [str(g.id) for g in user.groups.all()]
initial['first_name'] = user.first_name initial.update({
initial['last_name'] = user.last_name "token": Token.objects.filter(user=user)[0],
initial['roles'] = roles "first_name": user.first_name,
initial['user_active'] = user.is_active "last_name": user.last_name,
"roles": roles,
"user_active": user.is_active
})
return initial return initial

18
sapl/settings.py

@ -86,6 +86,7 @@ INSTALLED_APPS = (
'drf_yasg', 'drf_yasg',
#'rest_framework_swagger', #'rest_framework_swagger',
'rest_framework', 'rest_framework',
'rest_framework.authtoken',
'django_filters', 'django_filters',
'easy_thumbnails', 'easy_thumbnails',
@ -147,14 +148,6 @@ if DEBUG:
SITE_URL = config('SITE_URL', cast=str, default='') SITE_URL = config('SITE_URL', cast=str, default='')
CACHES = {
'default': {
'BACKEND': 'speedinfo.backends.proxy_cache',
'CACHE_BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',
}
}
REST_FRAMEWORK = { REST_FRAMEWORK = {
"UNICODE_JSON": False, "UNICODE_JSON": False,
"DEFAULT_PARSER_CLASSES": ( "DEFAULT_PARSER_CLASSES": (
@ -167,6 +160,7 @@ REST_FRAMEWORK = {
"sapl.api.permissions.SaplModelPermissions", "sapl.api.permissions.SaplModelPermissions",
), ),
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
'rest_framework.authentication.TokenAuthentication',
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
), ),
"DEFAULT_PAGINATION_CLASS": "sapl.api.pagination.StandardPagination", "DEFAULT_PAGINATION_CLASS": "sapl.api.pagination.StandardPagination",
@ -175,6 +169,14 @@ REST_FRAMEWORK = {
'django_filters.rest_framework.DjangoFilterBackend', 'django_filters.rest_framework.DjangoFilterBackend',
), ),
} }
CACHES = {
'default': {
'BACKEND': 'speedinfo.backends.proxy_cache',
'CACHE_BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',
}
}
ROOT_URLCONF = 'sapl.urls' ROOT_URLCONF = 'sapl.urls'

2
sapl/templates/auth/user_filter.html

@ -32,7 +32,7 @@
{% for usuario in page_obj %} {% for usuario in page_obj %}
<tr> <tr>
<td> <td>
<a href="{% url 'sapl.base:user_edit' usuario.pk %}">{{ usuario.username }}</a> <a href="{% url 'sapl.base:user_detail' usuario.pk %}">{{ usuario.username }}</a>
</td> </td>
<td>{{ usuario.first_name }} {{ usuario.last_name }}</td> <td>{{ usuario.first_name }} {{ usuario.last_name }}</td>
<td>{{ usuario.email }}</td> <td>{{ usuario.email }}</td>

57
sapl/templates/base/usuario_detail.html

@ -0,0 +1,57 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% load crispy_forms_tags cropping %}
{% block base_content %}
<div class="actions btn-group float-right " role="group" style="margin: 0px 0px 20px">
<a href="{% url 'sapl.base:usuario' %}" class="btn btn-outline-primary">
{% blocktrans with verbose_name=view.verbose_name %} Fazer nova pesquisa {% endblocktrans %}
</a>
<a href="{% url 'sapl.base:user_edit' user.pk %}" class="btn btn-outline-primary">
{% blocktrans with verbose_name=view.verbose_name %} Editar usuário {% endblocktrans %}
</a>
</div>
<div>
<table class="table table-striped">
<tbody>
<tr>
<th scope="row">Usuário</th>
<td>{{ user.username }}</td>
</tr>
<tr>
<th scope="row">Token</th>
<td>{{ token }}</td>
</tr>
<tr>
<th scope="row">Nome</th>
<td>{% firstof user.first_name "-" %}</td>
</tr>
<tr>
<th scope="row">Sobrenome</th>
<td>{% firstof user.last_name "-" %}</td>
</tr>
<tr>
<th scope="row">Endereço de e-mail</th>
<td>{% firstof user.email "-" %}</td>
</tr>
<tr>
<th scope="row">Usuário ativo?</th>
<td>{% if user.is_active %} Sim {% else %} Não {% endif %}</td>
</tr>
<tr>
<th scope="row">Último acesso</th>
<td>{{ user.last_login }}</td>
</tr>
<tr>
<th scope="row">Roles</th>
<td><ul style="list-style-type:none">
{% for r in roles %}
<li><input type="checkbox" {{ r.checked }} disabled> {{r.group }}</li>
{% endfor %}
</ul></td>
</tr>
</tbody>
</table>
</div>
{% endblock base_content %}

20
sapl/templates/base/usuario_edit.html

@ -0,0 +1,20 @@
{% extends "crud/form.html" %}
{% load i18n %}
{% block extra_js %}
<script type="text/javascript">
$(() => {
var $crf_token = $('[name="csrfmiddlewaretoken"]').attr('value');
$("#renovar-token").click(() => {
$.ajax({
url: "{% url 'sapl.api:recria_token' user.id %}",
type: "POST",
headers: { "X-CSRFToken": $crf_token },
dataType: "json",
success: (res) => $("#id_token").val(res.token)
});
});
});
</script>
{% endblock %}
Loading…
Cancel
Save