diff --git a/docs/token-auth.rst b/docs/token-auth.rst new file mode 100644 index 000000000..76bc1d5a9 --- /dev/null +++ b/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 ' + +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 ' -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 diff --git a/sapl/api/migrations/0001_initial.py b/sapl/api/migrations/0001_initial.py new file mode 100644 index 000000000..9ab35362d --- /dev/null +++ b/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) + ] diff --git a/sapl/api/migrations/__init__.py b/sapl/api/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sapl/api/urls.py b/sapl/api/urls.py index 4fc853e08..17fd432ab 100644 --- a/sapl/api/urls.py +++ b/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\d*)$', recria_token, name="recria_token"), # implementar caminho para autenticação # https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/ diff --git a/sapl/api/views.py b/sapl/api/views.py index f07710215..77db0e05f 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -3,8 +3,11 @@ import logging from django import apps from django.conf import settings from django.contrib.contenttypes.models import ContentType +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 _ @@ -16,10 +19,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 @@ -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 +@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")) @@ -587,3 +609,18 @@ class _NormaJuridicaViewset: def destaques(self, request, *args, **kwargs): self.queryset = self.get_queryset().filter(norma_de_destaque=True) 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) + diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 659914d6a..a20cb8b2d 100644 --- a/sapl/base/forms.py +++ b/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("Cancelar")], + label='Salvar Alterações')) def clean(self): super().clean() diff --git a/sapl/base/urls.py b/sapl/base/urls.py index 19a72c901..84bd2d0e1 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -8,7 +8,7 @@ from django.contrib.auth.views import (password_reset, password_reset_complete, password_reset_done) 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 .apps import AppConfig @@ -45,6 +45,7 @@ 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\d+)$', DetailUsuarioView.as_view(), name='user_detail'), url(r'^sistema/usuario/(?P\d+)/edit$', EditUsuarioView.as_view(), name='user_edit'), url(r'^sistema/usuario/(?P\d+)/delete$', DeleteUsuarioView.as_view(), name='user_delete') ] diff --git a/sapl/base/views.py b/sapl/base/views.py index 2b0d1a680..edb32e336 100644 --- a/sapl/base/views.py +++ b/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.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 @@ -79,6 +78,8 @@ from .forms import (AlterarSenhaForm, CasaLegislativaForm, RelatorioNormasPorAutorFilterSet) from .models import AppConfig, CasaLegislativa +from rest_framework.authtoken.models import Token + def get_casalegislativa(): return CasaLegislativa.objects.first() @@ -1749,7 +1750,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 @@ -1768,18 +1769,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 @@ -1806,6 +1805,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 @@ -1813,21 +1834,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']) @@ -1835,7 +1856,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) @@ -1876,11 +1897,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() @@ -1888,10 +1910,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 diff --git a/sapl/settings.py b/sapl/settings.py index 01db9c336..cea3f2b4a 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -86,6 +86,7 @@ INSTALLED_APPS = ( 'drf_yasg', #'rest_framework_swagger', 'rest_framework', + 'rest_framework.authtoken', 'django_filters', 'easy_thumbnails', @@ -147,14 +148,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": ( @@ -167,6 +160,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", @@ -175,6 +169,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' diff --git a/sapl/templates/auth/user_filter.html b/sapl/templates/auth/user_filter.html index 1a953a663..5426cab0b 100644 --- a/sapl/templates/auth/user_filter.html +++ b/sapl/templates/auth/user_filter.html @@ -32,7 +32,7 @@ {% for usuario in page_obj %} - {{ usuario.username }} + {{ usuario.username }} {{ usuario.first_name }} {{ usuario.last_name }} {{ usuario.email }} diff --git a/sapl/templates/base/usuario_detail.html b/sapl/templates/base/usuario_detail.html new file mode 100644 index 000000000..d4d1863fc --- /dev/null +++ b/sapl/templates/base/usuario_detail.html @@ -0,0 +1,57 @@ +{% extends "crud/detail.html" %} +{% load i18n %} +{% load crispy_forms_tags cropping %} + +{% block base_content %} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Usuário{{ user.username }}
Token{{ token }}
Nome{% firstof user.first_name "-" %}
Sobrenome{% firstof user.last_name "-" %}
Endereço de e-mail{% firstof user.email "-" %}
Usuário ativo?{% if user.is_active %} Sim {% else %} Não {% endif %}
Último acesso{{ user.last_login }}
Roles
    + {% for r in roles %} +
  • {{r.group }}
  • + {% endfor %} +
+
+{% endblock base_content %} diff --git a/sapl/templates/base/usuario_edit.html b/sapl/templates/base/usuario_edit.html new file mode 100644 index 000000000..d922e68d4 --- /dev/null +++ b/sapl/templates/base/usuario_edit.html @@ -0,0 +1,20 @@ +{% extends "crud/form.html" %} +{% load i18n %} + +{% block extra_js %} + + +{% endblock %} \ No newline at end of file