Skip to content
This repository has been archived by the owner on Sep 16, 2022. It is now read-only.

Commit

Permalink
Merge pull request #301 from GreatFruitOmsk/issue_277
Browse files Browse the repository at this point in the history
Add support for API keys in the dashboard
  • Loading branch information
vpetersson authored Jul 3, 2019
2 parents 6f326b5 + 44bf359 commit 629264a
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 47 deletions.
2 changes: 2 additions & 0 deletions backend/backend/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def check_ip_range(ipr):
'django.contrib.staticfiles',
'django_json_widget',
'rest_framework',
'rest_framework.authtoken',
'tagulous',
'device_registry.apps.DeviceRegistryConfig',
'profile_page.apps.ProfilePageConfig',
Expand Down Expand Up @@ -127,6 +128,7 @@ def check_ip_range(ipr):
LOGOUT_REDIRECT_URL = 'auth_login'

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication'],
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer'],
'DEFAULT_PARSER_CLASSES': ['rest_framework.parsers.JSONParser'],
Expand Down
2 changes: 2 additions & 0 deletions backend/device_registry/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.generics import ListAPIView, DestroyAPIView, CreateAPIView, UpdateAPIView
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
from rest_framework.permissions import AllowAny
from netaddr import IPAddress

Expand Down Expand Up @@ -214,6 +215,7 @@ class DeviceListView(ListAPIView):
List all of the users devices.
"""
serializer_class = DeviceInfoSerializer
authentication_classes = [SessionAuthentication, TokenAuthentication]

def get_queryset(self):
return DeviceInfo.objects.filter(device__owner=self.request.user)
Expand Down
8 changes: 6 additions & 2 deletions backend/device_registry/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@ def __repr__(self):
class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
fields = '__all__'
fields = ['id', 'device_id', 'owner', 'created', 'last_ping', 'certificate_expires', 'comment', 'name',
'agent_version', 'tags']


class DeviceInfoSerializer(serializers.ModelSerializer):
device = DeviceSerializer(read_only=True)

class Meta:
model = DeviceInfo
fields = '__all__'
fields = ['device', 'device_manufacturer', 'device_model', 'device_architecture', 'device_operating_system',
'device_operating_system_version', 'distr_id', 'distr_release', 'trust_score', 'fqdn', 'ipv4_address',
'selinux_state', 'app_armor_enabled', 'logins', 'default_password', 'detected_mirai',
'device_metadata']


class TagsSerializer(serializers.ModelSerializer):
Expand Down
6 changes: 1 addition & 5 deletions backend/device_registry/tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,11 +733,7 @@ def setUp(self):
}

def test_get(self):
response = self.client.get(
self.url,
**self.headers,
format='json'
)
response = self.client.get(self.url, **self.headers, format='json')
self.assertEqual(response.status_code, 200)
self.assertListEqual(response.json(),
[{'name': 'name1', 'key': 'key1', 'value': 'as9dfyaoiufhoasdfjh', 'linux_user': 'nobody',
Expand Down
73 changes: 45 additions & 28 deletions backend/device_registry/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from rest_framework import status
from rest_framework import serializers
from rest_framework.exceptions import ErrorDetail
from rest_framework.authtoken.models import Token

from device_registry.models import Credential, Device, DeviceInfo, Tag

Expand Down Expand Up @@ -173,44 +174,60 @@ def setUp(self):
logins={'pi': {'failed': 1, 'success': 1}},
device_metadata={'test-key': 'test-value'}
)
self.client.login(username='test', password='123')

def test_get(self):
self.client.login(username='test', password='123')
response = self.client.get(self.url)
self.client.logout()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.data, [OrderedDict([('id', self.device_info.id),
('device', OrderedDict(
[('id', self.device.id),
('device_id', self.device.device_id),
('created', datetime_to_str(self.device.created)),
('last_ping', None), ('certificate', None),
('certificate_csr', None),
('certificate_expires', None),
('comment', None), ('claim_token', ''),
('fallback_token', ''), ('name', ''),
('agent_version', None),
('owner', self.user.id),
('tags', list(self.device.tags.values_list('pk',
flat=True)))
]
)),
(
'device_manufacturer',
self.device_info.device_manufacturer),
self.assertListEqual(response.data, [OrderedDict([('device', OrderedDict(
[('id', self.device.id), ('device_id', self.device.device_id), ('owner', self.user.id),
('created', datetime_to_str(self.device.created)), ('last_ping', None), ('certificate_expires', None),
('comment', None), ('name', ''), ('agent_version', None),
('tags', list(self.device.tags.values_list('pk', flat=True)))])),
('device_manufacturer', self.device_info.device_manufacturer),
('device_model', self.device_info.device_model),
('device_architecture', None),
('device_operating_system', None),
('device_operating_system_version', None),
('distr_id', None), ('distr_release', None),
('trust_score', None), ('fqdn', None),
('ipv4_address', None),
('device_operating_system_version', None), ('distr_id', None),
('distr_release', None), ('trust_score', None),
('fqdn', None), ('ipv4_address', None),
('selinux_state', {'mode': 'enforcing', 'enabled': True}),
('app_armor_enabled', True),
('logins', {'pi': {'failed': 1, 'success': 1}}),
('default_password', None),
('detected_mirai', False),
('device_metadata', {'test-key': 'test-value'})
])])
('default_password', None), ('detected_mirai', False),
('device_metadata', {'test-key': 'test-value'})])])

def test_get_token_auth_success(self):
token = Token.objects.create(user=self.user)
self.client.credentials(HTTP_AUTHORIZATION='Token %s' % token.key)
response = self.client.get(self.url)
self.client.credentials() # Reset previously set HTTP headers.
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.data, [OrderedDict([('device', OrderedDict(
[('id', self.device.id), ('device_id', self.device.device_id), ('owner', self.user.id),
('created', datetime_to_str(self.device.created)), ('last_ping', None), ('certificate_expires', None),
('comment', None), ('name', ''), ('agent_version', None),
('tags', list(self.device.tags.values_list('pk', flat=True)))])),
('device_manufacturer', self.device_info.device_manufacturer),
('device_model', self.device_info.device_model),
('device_architecture', None),
('device_operating_system', None),
('device_operating_system_version', None), ('distr_id', None),
('distr_release', None), ('trust_score', None),
('fqdn', None), ('ipv4_address', None),
('selinux_state', {'mode': 'enforcing', 'enabled': True}),
('app_armor_enabled', True),
('logins', {'pi': {'failed': 1, 'success': 1}}),
('default_password', None), ('detected_mirai', False),
('device_metadata', {'test-key': 'test-value'})])])

def test_get_token_auth_fail(self):
Token.objects.create(user=self.user)
self.client.credentials(HTTP_AUTHORIZATION='Token random_string')
response = self.client.get(self.url)
self.client.credentials() # Reset previously set HTTP headers.
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class CredentialsViewTest(APITestCase):
Expand Down
55 changes: 45 additions & 10 deletions backend/profile_page/templates/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ <h1 class="h3 mb-3">Settings</h1>
<h5 class="card-title mb-0">Profile Settings</h5>
</div>

<div class="list-group list-group-flush" role="tablist">
<div class="list-group list-group-flush" role="tablist" id="tablist">
<a class="list-group-item list-group-item-action active" data-toggle="list" href="#account" role="tab">
Account
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#password" role="tab">
Password
</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#token" role="tab">
API token
</a>
</div>
</div>
</div>
Expand Down Expand Up @@ -66,22 +69,17 @@ <h5 class="card-title mb-0">Public info</h5>
<input class="form-control form-control-lg" type="text" name="company"
value="{% if user.profile.company_name is not None %}{{user.profile.company_name}}{% endif %}"/>
</div>
<div class="form-group">
<div class="text-center mt-3">
<input type="submit" class="btn btn-lg btn-primary" value="Save changes"/>
</div>
</div>
<button type="submit" class="btn btn-primary">Save changes</button>
</form>

</div>
</div>

</div>
<div class="tab-pane fade" id="password" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Password</h5>
</div>
<div class="card-body">
<h5 class="card-title">Password</h5>

<form>
{% csrf_token %}
<div class="form-group">
Expand All @@ -101,8 +99,45 @@ <h5 class="card-title">Password</h5>
</div>
</div>
</div>
<div class="tab-pane fade" id="token" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">API token</h5>
</div>
<div class="card-body">
{% if user.auth_token %}
<div class="form-group"><b><samp id="token-code">{{ user.auth_token }}</samp></b></div>
<a href="{% url 'revoke_api_token' %}" class="btn btn-danger">Revoke token</a>
{% else %}
<a href="{% url 'generate_api_token' %}" class="btn btn-primary">Generate token</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

{% block scripts %}
<script>
let url = location.href;

if (location.hash) {
const hash = url.split("#");
$('#tablist a[href="#' + hash[1] + '"]').tab("show");
}

$('a[data-toggle="list"]').on("click", function () {
let newUrl;
const hash = $(this).attr("href");
if (hash == "#account") {
newUrl = url.split("#")[0];
} else {
newUrl = url.split("#")[0] + hash;
}
history.replaceState(null, null, newUrl);
});
</script>
{% endblock %}
4 changes: 3 additions & 1 deletion backend/profile_page/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django.urls import path

from .views import profile_view
from .views import profile_view, GenerateAPITokenView, RevokeAPITokenView

urlpatterns = [
path('profile/', profile_view, name='profile'),
path('generate-api-token/', GenerateAPITokenView.as_view(), name='generate_api_token'),
path('revoke-api-token/', RevokeAPITokenView.as_view(), name='revoke_api_token')
]
21 changes: 20 additions & 1 deletion backend/profile_page/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
from django.contrib.auth import logout
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.http import HttpResponseRedirect
from django.contrib import messages
from django.views.generic import View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.urls import reverse

from rest_framework.authtoken.models import Token

from .forms import ProfileForm
from .models import Profile
Expand Down Expand Up @@ -43,3 +48,17 @@ def dispatch(self, request, *args, **kwargs):
# Redirect to this page until the session has been cleared.
return HttpResponseRedirect(next_page)
return super().dispatch(request, *args, **kwargs)


class GenerateAPITokenView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
if not hasattr(request.user, 'auth_token'):
Token.objects.create(user=request.user)
return HttpResponseRedirect(reverse('profile') + '#token')


class RevokeAPITokenView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
if hasattr(request.user, 'auth_token'):
Token.objects.filter(user=request.user).delete()
return HttpResponseRedirect(reverse('profile') + '#token')

0 comments on commit 629264a

Please sign in to comment.