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/3367/head
Edward 5 years ago
committed by eribeiro
parent
commit
729417e35d
  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. 38
      sapl/api/views.py
  6. 19
      sapl/base/forms.py
  7. 13
      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,\
AutoresProvaveisListView, AutoresPossiveisListView, AutorListView,\
ModelChoiceView
from sapl.api.views import SaplApiViewSetConstrutor
from sapl.api.views import SaplApiViewSetConstrutor, AppVersionView, recria_token
from .apps import AppConfig
@ -70,7 +70,8 @@ urlpatterns = [
url(r'^api/', include(deprecated_urlpatterns_api)),
url(r'^api/', include(urlpatterns_api_doc)),
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
# https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/

38
sapl/api/views.py

@ -4,8 +4,11 @@ from django import apps
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.core.urlresolvers import reverse_lazy
from django.db.models import Q
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.text import capfirst
from django.utils.translation import ugettext_lazy as _
@ -17,10 +20,14 @@ from django_filters.utils import resolve_field
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist
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.response import Response
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.permissions import SaplModelPermissions
@ -38,6 +45,21 @@ from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria
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:
def create(self, request, *args, **kwargs):
raise Exception(_("POST Create não implementado"))
@ -617,3 +639,17 @@ class _CronometroViewSet:
except FieldError as e:
pass
return qs
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 = []
token = forms.CharField(
required=False,
label="Token",
max_length=40,
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
first_name = forms.CharField(
required=False,
label="Nome",
@ -206,6 +211,7 @@ class UsuarioEditForm(ModelForm):
model = get_user_model()
fields = [
get_user_model().USERNAME_FIELD,
"token",
"first_name",
"last_name",
'password1',
@ -220,19 +226,24 @@ class UsuarioEditForm(ModelForm):
super(UsuarioEditForm, self).__init__(*args, **kwargs)
rows = to_row((
('username', 12),
('first_name', 6),
('last_name', 6),
('email', 6),
('user_active', 6),
('password1', 6),
('password2', 6)))
('password2', 6),
('roles', 12)))
self.helper = SaplFormHelper()
self.helper.layout = Layout(
'username',
FieldWithButtons('token', StrictButton('Renovar', id="renovar-token", css_class="btn-outline-primary")),
rows,
'roles',
form_actions(label='Salvar Alterações'))
form_actions(
more=[
HTML("<a href='{% url 'sapl.base:user_detail' object.pk %}' "
"class='btn btn-dark'>Cancelar</a>")],
label='Salvar Alterações'))
def clean(self):
super().clean()

13
sapl/base/urls.py

@ -9,8 +9,7 @@ from django.contrib.auth.views import (password_reset, password_reset_complete,
from django.views.generic.base import RedirectView, TemplateView
from sapl import base
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 .apps import AppConfig
@ -47,12 +46,10 @@ app_name = AppConfig.name
admin_user = [
url(r'^sistema/usuario/$', PesquisarUsuarioView.as_view(), name='usuario'),
url(r'^sistema/usuario/create$',
CreateUsuarioView.as_view(), name='user_create'),
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/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+)/delete$', DeleteUsuarioView.as_view(), name='user_delete')
]
alterar_senha = [

73
sapl/base/views.py

@ -26,8 +26,7 @@ from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.safestring import mark_safe
from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
from django.views.generic import (CreateView, DeleteView, FormView, ListView,
UpdateView)
from django.views.generic import (CreateView, DetailView, DeleteView, FormView, ListView, UpdateView)
from django.views.generic.base import RedirectView, TemplateView
from django_filters.views import FilterView
from haystack.views import SearchView
@ -113,6 +112,8 @@ def filtra_url_materias_em_tramitacao(qr, qs, campo_url, local_ou_status):
return qs.filter(em_tramitacao=True, id__in=id_materias)
from rest_framework.authtoken.models import Token
def get_casalegislativa():
return CasaLegislativa.objects.first()
@ -1822,7 +1823,7 @@ class ListarProtocolosDuplicadosView(PermissionRequiredMixin, ListView):
class PesquisarUsuarioView(PermissionRequiredMixin, FilterView):
model = User
model = get_user_model()
filterset_class = UsuarioFilterSet
permission_required = ('base.list_appconfig',)
paginate_by = 10
@ -1841,18 +1842,16 @@ class PesquisarUsuarioView(PermissionRequiredMixin, FilterView):
return kwargs
def get_context_data(self, **kwargs):
context = super(PesquisarUsuarioView,
self).get_context_data(**kwargs)
context = super(PesquisarUsuarioView, self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhum usuário encontrado!'
context['title'] = _('Usuários')
context.update({
"page_range": make_pagination(page_obj.number, paginator.num_pages),
"NO_ENTRIES_MSG": "Nenhum usuário encontrado!",
"title": _("Usuários")
})
return context
@ -1879,6 +1878,28 @@ class PesquisarUsuarioView(PermissionRequiredMixin, FilterView):
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):
model = get_user_model()
form_class = UsuarioCreateForm
@ -1886,21 +1907,21 @@ class CreateUsuarioView(PermissionRequiredMixin, CreateView):
fail_message = 'Usuário não criado!'
permission_required = ('base.add_appconfig',)
def get_success_url(self):
return reverse('sapl.base:usuario')
def get_success_url(self, pk):
return reverse('sapl.base:user_detail', kwargs={"pk": pk})
def form_valid(self, form):
data = form.cleaned_data
new_user = get_user_model().objects.create(
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.is_superuser = False
new_user.is_staff = False
new_user.save()
groups = Group.objects.filter(id__in=data['roles'])
@ -1908,7 +1929,7 @@ class CreateUsuarioView(PermissionRequiredMixin, CreateView):
g.user_set.add(new_user)
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):
messages.error(self.request, self.fail_message)
@ -1950,11 +1971,12 @@ class DeleteUsuarioView(PermissionRequiredMixin, DeleteView):
class EditUsuarioView(PermissionRequiredMixin, UpdateView):
model = get_user_model()
form_class = UsuarioEditForm
template_name = "base/usuario_edit.html"
success_message = 'Usuário editado com sucesso!'
permission_required = ('base.change_appconfig',)
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):
initial = super().get_initial()
@ -1962,10 +1984,13 @@ class EditUsuarioView(PermissionRequiredMixin, UpdateView):
user = get_user_model().objects.get(id=self.kwargs['pk'])
roles = [str(g.id) for g in user.groups.all()]
initial['first_name'] = user.first_name
initial['last_name'] = user.last_name
initial['roles'] = roles
initial['user_active'] = user.is_active
initial.update({
"token": Token.objects.filter(user=user)[0],
"first_name": user.first_name,
"last_name": user.last_name,
"roles": roles,
"user_active": user.is_active
})
return initial

18
sapl/settings.py

@ -86,6 +86,7 @@ INSTALLED_APPS = (
'drf_yasg',
#'rest_framework_swagger',
'rest_framework',
'rest_framework.authtoken',
'django_filters',
'easy_thumbnails',
@ -158,14 +159,6 @@ if DEBUG:
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 = {
"UNICODE_JSON": False,
"DEFAULT_PARSER_CLASSES": (
@ -178,6 +171,7 @@ REST_FRAMEWORK = {
"sapl.api.permissions.SaplModelPermissions",
),
"DEFAULT_AUTHENTICATION_CLASSES": (
'rest_framework.authentication.TokenAuthentication',
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_PAGINATION_CLASS": "sapl.api.pagination.StandardPagination",
@ -186,6 +180,14 @@ REST_FRAMEWORK = {
'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'

2
sapl/templates/auth/user_filter.html

@ -32,7 +32,7 @@
{% for usuario in page_obj %}
<tr>
<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>{{ usuario.first_name }} {{ usuario.last_name }}</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