Skip to content
Snippets Groups Projects
Commit a061f1be authored by Jonathan Séguin's avatar Jonathan Séguin
Browse files

Merge branch...

Merge branch '98-implement-pubkey-a-la-gitlab-github-ou-simple-token-pour-permettre-un-acces-sans-fournir-son-mot' into 'master'

Resolve "Implement pubkey à la gitlab/github (ou simple token), pour permettre un accès sans fournir son mot de passe"

Closes #98

See merge request !68
parents 7bfedd4d f8b1a5e1
No related branches found
No related tags found
1 merge request!68Resolve "Implement pubkey à la gitlab/github (ou simple token), pour permettre un accès sans fournir son mot de passe"
......@@ -141,6 +141,7 @@ AUTHENTICATION_BACKENDS = [
'social_core.backends.azuread_tenant.AzureADTenantOAuth2',
'django.contrib.auth.backends.ModelBackend',
'django_auth_ldap.backend.LDAPBackend',
'portal.auth_backends.TokenAuthBackend',
]
SOCIAL_AUTH_PIPELINE = (
......
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class TokenAuthBackend(ModelBackend):
def authenticate(self, request, token=None):
auth = request.headers.get('Authorization')
if not token and auth and auth.startswith('Token '):
token = auth[6:]
try:
return User.objects.get(profile__api_token=token)
except User.DoesNotExist:
return None
# Generated by Django 2.0.13 on 2021-01-29 16:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0024_auto_20200227_0948'),
]
operations = [
migrations.AddField(
model_name='profile',
name='api_token',
field=models.CharField(default=None, max_length=64, null=True, unique=True),
),
]
import os
import binascii
import hashlib
from django.conf import settings
......@@ -13,15 +14,16 @@ from private_storage.fields import PrivateFileField
from portal.storage import HashStorage
class ProfileManager(models.Manager):
def exclude_profile(self, profile):
return super().get_queryset().exclude(id=profile.id)
class Profile(models.Model):
"""example Documentation:
https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone
"""
class Meta:
ordering = ['user__last_name', 'user__first_name']
verbose_name = _("user")
......@@ -32,8 +34,16 @@ class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
labs = models.ManyToManyField('Lab', related_name='members')
api_token = models.CharField(max_length=64, null=True, unique=True, blank=False, default=None)
objects = ProfileManager()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.api_token:
self.api_token = self.generate_token()
self.save()
def __str__(self):
return '{0}, {1} <{2}>'.format(self.user.last_name, self.user.first_name, self.user.email)
......@@ -53,6 +63,10 @@ class Profile(models.Model):
def is_pi_or_data_manager(self):
return Lab.objects.filter(Q(pi=self) | Q(data_managers=self)).exists()
@staticmethod
def generate_token():
return binascii.hexlify(os.urandom(32)).decode()
class Institution(models.Model):
class Meta:
......
{% extends './base.html' %}
{% load i18n %}
{% block main_content %}
<div class="row mb-5">
<div class="col">
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<h4 class="alert-heading">{% trans 'Warning' %}!</h4>
<p>{% trans 'This token enables authentication to IRIC Data. Do not share this token!' %}</p>
</div>
<p>{% trans 'API Token' %} :</p>
<div class="col-8 alert alert-dark">
<pre class='mb-0'><code><h5 class='mb-0'>{{user.profile.api_token}}</h5></code></pre>
</div>
<p>
{% trans 'Use this token to authenticate your API calls by inserting it in the "Authorization" request header with the key "Token".' %}
Ex.
</p>
<div class="col-10 alert alert-secondary">
<pre class='mb-0'><code>import requests
url = 'http://localhost:8000/api/v1/my-datasets/list/json/'
headers = {'Authorization': 'Token {{user.profile.api_token}}'}
r = requests.get(url, headers=headers)</code></pre>
</div>
</div>
</div>
{% endblock %}
......@@ -29,7 +29,7 @@
</span>
</a>
<div class="collapse subnav {% if active_page == 'datafiles' or active_page == 'datasets' or active_page == 'shared-datafiles' or active_page == 'sharegroups' or active_page == 'dashboard' %}show{% endif %}" id="file-subnav">
<div class="collapse subnav {% if active_page == 'datafiles' or active_page == 'datasets' or active_page == 'shared-datafiles' or active_page == 'sharegroups' or active_page == 'dashboard' or active_page == 'api' %}show{% endif %}" id="file-subnav">
<a class="nav-link {% if active_page == 'datafiles' %}active{% endif %}" href="{% url 'user.datafiles' %}">
<i class="fas fa-file-alt fa-fw fa-lg"></i>
{% trans 'My Files' %}
......@@ -46,5 +46,9 @@
<i class="fas fa-handshake fa-fw fa-lg"></i>
{% trans 'ShareGroups' %}
</a>
<a class="nav-link {% if active_page == 'api' %}active{% endif %}" href="{% url 'user.api' %}">
<i class="fas fa-terminal fa-fw fa-lg"></i>
{% trans 'API' %}
</a>
</div>
{% endblock %}
......@@ -53,6 +53,7 @@ from .views.secure.user_profile import (AdminUserView, LDAPUserCreateView,
ProfileUpdateView, UserCreateView,
UserDeleteView, UserJSONListView,
UserUpdateView)
from .views.secure.api import APIView
urlpatterns = [
# Landing Pages
......@@ -130,6 +131,9 @@ urlpatterns = [
path('secure/dataset/download/<int:pk>', DataSetDownload.as_view(), name='user.dataset-download'),
path('secure/dataset/download/<slug:iric_data_id>', DataSetDownload.as_view(), name='user.dataset-download-iric-data-id'),
# API
path('secure/api/', APIView.as_view(), name='user.api'),
# ShareGroups
path('secure/sharegroups/list/json/', ShareGroupJSONListView.as_view(), name='secure.sharegroup-json-list'),
path('secure/my-sharegroups/', ShareGroupView.as_view(), name='user.sharegroups'),
......
......@@ -9,10 +9,23 @@ from django.views.generic import ListView
from django.views.generic.base import ContextMixin
from django.views.generic.detail import BaseDetailView, SingleObjectMixin
from django.views.generic.edit import ModelFormMixin
from django.contrib.auth import login, authenticate
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import (AppSettings, DataFile, DataSet, Lab, Log, Profile,
ShareGroup)
class TokenLoginMixin(LoginRequiredMixin):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
user = authenticate(request)
if user:
login(request, user)
else:
return JsonResponse({'error': _('Malformed authorization header or invalid token')}, status=401)
return super().dispatch(request, *args, **kwargs)
class StaffViewMixin(UserPassesTestMixin):
def test_func(self):
......
......@@ -4,15 +4,26 @@ import logging
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.views.generic import TemplateView
from django.utils.translation import ugettext_lazy as _
from portal.views import TokenLoginMixin
from ...models import DataFile, DataSet, Profile
from ...views import JSONListView, JSONView, StaffViewMixin
from ...views import JSONListView, JSONView, StaffViewMixin, ActivePageViewMixin
logger = logging.getLogger('debug')
class APIView(LoginRequiredMixin, ActivePageViewMixin, TemplateView):
template_name = 'portal/secure/user/api.html'
active_page = 'api'
page_title = _('API Details')
# DataFile API Views #######
class DataFileLookupJSONListView(LoginRequiredMixin, JSONListView):
class DataFileLookupJSONListView(TokenLoginMixin, JSONListView):
model = DataFile
def get_queryset(self):
......@@ -51,7 +62,7 @@ class DataFileLookupJSONListView(LoginRequiredMixin, JSONListView):
return rows
class DataFileAnnotationJSONView(LoginRequiredMixin, JSONView):
class DataFileAnnotationJSONView(TokenLoginMixin, JSONView):
model = DataFile
def get_data(self):
......@@ -59,7 +70,7 @@ class DataFileAnnotationJSONView(LoginRequiredMixin, JSONView):
return o.annotations if o.annotations else {}
class DataFileMetadataJSONView(LoginRequiredMixin, JSONView):
class DataFileMetadataJSONView(TokenLoginMixin, JSONView):
model = DataFile
def get_data(self):
......@@ -79,7 +90,7 @@ class DataFileMetadataJSONView(LoginRequiredMixin, JSONView):
# DataSet API Views #######
class AdminDataSetJSONListView(StaffViewMixin, JSONListView):
class AdminDataSetJSONListView(TokenLoginMixin, JSONListView):
model = DataSet
def get_queryset(self):
......@@ -103,7 +114,7 @@ class AdminDataSetJSONListView(StaffViewMixin, JSONListView):
return rows
class UserDataSetJSONListView(LoginRequiredMixin, JSONListView):
class UserDataSetJSONListView(TokenLoginMixin, JSONListView):
model = DataSet
def get_queryset(self):
......
......@@ -4,7 +4,7 @@ cffi==1.14.3
chardet==3.0.4
cryptography==3.1.1
defusedxml==0.7.0rc1
Django==2.0.13
Django==2.2.17
django-auth-ldap==1.7.0
django-chartjs==1.3
django-crispy-forms==1.8.1
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment