diff --git a/authserver/authserver/api/customroutes.py b/authserver/authserver/api/customroutes.py new file mode 100644 index 0000000..8ad1564 --- /dev/null +++ b/authserver/authserver/api/customroutes.py @@ -0,0 +1,179 @@ +import base64 +import json +import requests +import struct +import time + +from django.contrib.auth.models import User +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, get_list_or_404 + +from rest_framework import mixins, permissions, status, viewsets +from rest_framework.authtoken.models import Token +from rest_framework.decorators import api_view, permission_classes +from rest_framework.response import Response + +from . import models, serializers, views +from authserver.settings import PROTOSPACE_LOGIN_PAGE, FIRMWARE_VERSION_MAGIC + +LOG_DIRECTORY = '/var/log/pslockout' +VALID_TIME = 1000000000 + +EVENTS = [ + 'LOG_BOOT_UP - =========== Booted up, version: ', + 'LOG_INIT_COMPLETE - Initialization completed', + 'LOG_WIFI_CONNECTED - Wifi connected', + 'LOG_WIFI_DISCONNECTED - Wifi disconnected', + 'LOG_COMM_LOCK_ARM - Received arm request over web', + 'LOG_COMM_LOCK_DISARM - Received disarm request over web', + 'LOG_COMM_LOCK_FAIL - Lock status communication failed, code: ', + 'LOG_COMM_CARD_FAIL - Card list communication failed, code: ', + 'LOG_COMM_INFO_FAIL - Info log communication failed, code: ', + 'LOG_LOCK_OFF - Lock turned off', + 'LOG_LOCK_ARMED - Lock armed', + 'LOG_LOCK_TIMEOUT - Lock arming timed out', + 'LOG_LOCK_ON - Lock turned on', + 'LOG_LOCK_DISARMED - Lock disarmed', + 'LOG_LOCK_ERROR - Button held while arming lock', + 'LOG_CARD_GOOD_READ - Successful read from card: ', + 'LOG_CARD_ACCEPTED - Accepted card: ', + 'LOG_CARD_DENIED - Denied card: ', + 'LOG_UPDATE_FAILED - Firmware update failed, code: ', +] + +@api_view(['POST']) +def login(request): + username = request.data.get('username').lower() + password = request.data.get('password') + if username is None or password is None: + return Response({'error': 'Please provide both username and password'}, + status=status.HTTP_400_BAD_REQUEST) + + post_data = {'user_name': username, 'web_pw': password, 'SubmitButton': 'Login'} + res = requests.post(PROTOSPACE_LOGIN_PAGE, post_data, allow_redirects=False) + if res.status_code == requests.codes.ok: + return Response({'error': 'Invalid Credentials'}, status=status.HTTP_404_NOT_FOUND) + + lockout_username = username.replace('.', '') + + user, created = User.objects.get_or_create(username=lockout_username) + user.set_password(password) # not validated + user.save() + + if created: + models.Profile.objects.create(user=user) + + token, _ = Token.objects.get_or_create(user=user) + + return Response({'token': token.key}, status=status.HTTP_200_OK) + +@api_view(['GET']) +def cards(request, mac): + tool = get_object_or_404(models.Tool, mac=mac) + cards = models.Card.objects.all().filter(profile__courses__tools=tool) + card_numbers = [card.number for card in cards] + + return Response(','.join(card_numbers), status=status.HTTP_200_OK) + +@api_view(['PUT']) +@permission_classes((views.IsLockoutAdmin,)) +def update_cards(request): + data = request.data + updated_count = 0 + + if not data: + return Response({'error': 'Please provide card data in the form username=cardnumber,cardnumber,cardnumber'}, + status=status.HTTP_400_BAD_REQUEST) + + for username, card_numbers in data.items(): + try: + lockout_username = username.replace('.', '') + profile = models.Profile.objects.get(user__username=lockout_username) + except models.Profile.DoesNotExist: + continue + + for card_number in card_numbers.split(','): + card, _ = models.Card.objects.get_or_create( + profile=profile, + number=card_number + ) + if card: updated_count += 1 + + return Response({'updated': updated_count}, status=status.HTTP_200_OK) + +@api_view(['POST']) +def infolog(request, mac): + entries_processed = 0 + oldest_valid_log_time = time.time() + + tool = get_object_or_404(models.Tool, mac=mac) + + encoded_log = request.data.get('log') + if encoded_log: + decoded_log = base64.b64decode(encoded_log) + unpacked_log = list(struct.iter_unpack(' VALID_TIME: + oldest_valid_log_time = entry[0] + + with open(LOG_DIRECTORY + '/devices/' + mac + '.log', 'a') as log_file: + for entry in unpacked_log: + # if time is obviously wrong, just use oldest valid log time or now + entry_time = entry[0] if entry[0] > VALID_TIME else oldest_valid_log_time + entry_time_string = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(entry_time)) + + entry_event = EVENTS[entry[1]] + entry_data = entry[2].decode('utf-8').strip('\0') + + if entry_data: + user = models.User.objects.filter(profile__cards__number=entry_data) + if user.count(): + entry_data += ' (' + str(user.first()) + ')' + + entry_string = '{} - {} - {}{}'.format(entry_time_string, mac, entry_event, entry_data) + + entries_processed += 1 + log_file.write(entry_string + '\n') + + version = str(get_object_or_404(models.Firmware, tools=tool)) + version_string = '{} {} {}'.format(FIRMWARE_VERSION_MAGIC, version, FIRMWARE_VERSION_MAGIC) + + response_object = { + 'processed': entries_processed, + 'unixTime': int(time.time()), + 'version': version_string, + } + + return Response(response_object, status=status.HTTP_200_OK) + +@api_view(['GET']) +def update(request, mac): + tool = get_object_or_404(models.Tool, mac=mac) + firmware = get_object_or_404(models.Firmware, tools=tool) + + response = HttpResponse(firmware.binary, content_type='text/plain') + response['Content-Disposition'] = 'attachment; filename=firmware_{}.bin'.format(firmware.version) + return response + +@api_view(['PUT']) +@permission_classes((permissions.IsAuthenticated,)) +def select_courses(request): + if 'courses' not in request.data: + return Response({'error': 'Please provide a list of course slugs'}, + status=status.HTTP_400_BAD_REQUEST) + courses = request.data.get('courses') + + profile = get_object_or_404(models.Profile, user=request.user) + + if profile.courses.count() or profile.selected_courses: + return Response({'error': 'Already selected courses'}, + status=status.HTTP_400_BAD_REQUEST) + + if len(courses): + course_objects = get_list_or_404(models.Course, slug__in=courses) + profile.courses.set(course_objects) + profile.selected_courses = True + profile.save() + + return Response({'updated': len(courses)}, status=status.HTTP_200_OK) diff --git a/authserver/authserver/api/models.py b/authserver/authserver/api/models.py index 815624f..e1f7066 100644 --- a/authserver/authserver/api/models.py +++ b/authserver/authserver/api/models.py @@ -6,14 +6,12 @@ class Category(models.Model): slug = models.CharField(max_length=32, unique=True) info = models.TextField(max_length=1024, blank=True) photo = models.ImageField(blank=True) - def __str__(self): return self.name class Firmware(models.Model): version = models.CharField(unique=True, max_length=4) binary = models.FileField() - def __str__(self): return self.version @@ -26,7 +24,7 @@ class Tool(models.Model): photo = models.ImageField(blank=True) mac = models.CharField(max_length=12) firmware = models.ForeignKey(Firmware, blank=True, null=True, related_name='tools', on_delete=models.SET_NULL) - + upgrade_group = models.CharField(max_length=32) def __str__(self): return self.name @@ -34,7 +32,6 @@ class Course(models.Model): name = models.CharField(max_length=64) slug = models.CharField(max_length=32, unique=True) tools = models.ManyToManyField(Tool, blank=True) - def __str__(self): return self.name @@ -43,13 +40,11 @@ class Profile(models.Model): lockout_admin = models.BooleanField(default=False) courses = models.ManyToManyField(Course, blank=True) selected_courses = models.BooleanField(default=False) - def __str__(self): return self.user.username class Card(models.Model): profile = models.ForeignKey(Profile, related_name='cards', on_delete=models.CASCADE) number = models.CharField(max_length=10) - def __str__(self): return self.number diff --git a/authserver/authserver/api/serializers.py b/authserver/authserver/api/serializers.py index af520a8..b4afe7a 100644 --- a/authserver/authserver/api/serializers.py +++ b/authserver/authserver/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from . import models -from authserver.settings import FIRMWARE_VERSION_MAGIC +from authserver.settings import FIRMWARE_VERSION_MAGIC, UPGRADE_GROUPS class CategorySerializer(serializers.HyperlinkedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='category-detail', lookup_field='slug') @@ -37,6 +37,7 @@ class ToolSerializer(serializers.HyperlinkedModelSerializer): slug_field='version', queryset=models.Firmware.objects.all().order_by('-version') ) + upgrade_group = serializers.ChoiceField(choices=UPGRADE_GROUPS) class Meta: model = models.Tool @@ -79,6 +80,7 @@ class FirmwareSerializer(serializers.HyperlinkedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='firmware-detail', lookup_field='version') version = serializers.CharField(read_only=True) tools = serializers.StringRelatedField(read_only=True, many=True) + deploy_to_group = serializers.ChoiceField(write_only=True, choices=['None', 'All'] + UPGRADE_GROUPS) class Meta: model = models.Firmware @@ -101,4 +103,18 @@ class FirmwareSerializer(serializers.HyperlinkedModelSerializer): raise serializers.ValidationError('Firmware version already exists.') validated_data['version'] = version - return serializers.ModelSerializer.create(self, validated_data) + group = validated_data.pop('deploy_to_group') + firmware = serializers.ModelSerializer.create(self, validated_data) + + if group == 'None': + tools = [] + elif group == 'All': + tools = models.Tool.objects.all() + else: + tools = models.Tool.objects.filter(upgrade_group=group) + + for tool in tools: + tool.firmware = firmware + tool.save() + + return firmware diff --git a/authserver/authserver/api/views.py b/authserver/authserver/api/views.py index 2908e8c..fca18cf 100644 --- a/authserver/authserver/api/views.py +++ b/authserver/authserver/api/views.py @@ -16,9 +16,6 @@ from rest_framework.response import Response from . import models, serializers from authserver.settings import PROTOSPACE_LOGIN_PAGE, FIRMWARE_VERSION_MAGIC -LOG_DIRECTORY = '/var/log/pslockout' -VALID_TIME = 1000000000 - class IsLockoutAdmin(permissions.BasePermission): def has_permission(self, request, view): try: @@ -74,162 +71,3 @@ class FirmwareViewSet(viewsets.ModelViewSet): permission_classes = (IsLockoutAdmin,) lookup_field='version' http_method_names = ['get', 'post', 'head', 'delete', 'options'] - -@api_view(['POST']) -def login(request): - username = request.data.get('username').lower() - password = request.data.get('password') - if username is None or password is None: - return Response({'error': 'Please provide both username and password'}, - status=status.HTTP_400_BAD_REQUEST) - - post_data = {'user_name': username, 'web_pw': password, 'SubmitButton': 'Login'} - res = requests.post(PROTOSPACE_LOGIN_PAGE, post_data, allow_redirects=False) - if res.status_code == requests.codes.ok: - return Response({'error': 'Invalid Credentials'}, status=status.HTTP_404_NOT_FOUND) - - lockout_username = username.replace('.', '') - - user, created = User.objects.get_or_create(username=lockout_username) - user.set_password(password) # not validated - user.save() - - if created: - models.Profile.objects.create(user=user) - - token, _ = Token.objects.get_or_create(user=user) - - return Response({'token': token.key}, status=status.HTTP_200_OK) - -@api_view(['GET']) -def cards(request, mac): - tool = get_object_or_404(models.Tool, mac=mac) - cards = models.Card.objects.all().filter(profile__courses__tools=tool) - card_numbers = [card.number for card in cards] - - return Response(','.join(card_numbers), status=status.HTTP_200_OK) - -@api_view(['PUT']) -@permission_classes((IsLockoutAdmin,)) -def update_cards(request): - data = request.data - updated_count = 0 - - if not data: - return Response({'error': 'Please provide card data in the form username=cardnumber,cardnumber,cardnumber'}, - status=status.HTTP_400_BAD_REQUEST) - - for username, card_numbers in data.items(): - try: - lockout_username = username.replace('.', '') - profile = models.Profile.objects.get(user__username=lockout_username) - except models.Profile.DoesNotExist: - continue - - for card_number in card_numbers.split(','): - card, _ = models.Card.objects.get_or_create( - profile=profile, - number=card_number - ) - if card: updated_count += 1 - - return Response({'updated': updated_count}, status=status.HTTP_200_OK) - -EVENTS = [ - 'LOG_BOOT_UP - =========== Booted up, version: ', - 'LOG_INIT_COMPLETE - Initialization completed', - 'LOG_WIFI_CONNECTED - Wifi connected', - 'LOG_WIFI_DISCONNECTED - Wifi disconnected', - 'LOG_COMM_LOCK_ARM - Received arm request over web', - 'LOG_COMM_LOCK_DISARM - Received disarm request over web', - 'LOG_COMM_LOCK_FAIL - Lock status communication failed, code: ', - 'LOG_COMM_CARD_FAIL - Card list communication failed, code: ', - 'LOG_COMM_INFO_FAIL - Info log communication failed, code: ', - 'LOG_LOCK_OFF - Lock turned off', - 'LOG_LOCK_ARMED - Lock armed', - 'LOG_LOCK_TIMEOUT - Lock arming timed out', - 'LOG_LOCK_ON - Lock turned on', - 'LOG_LOCK_DISARMED - Lock disarmed', - 'LOG_LOCK_ERROR - Button held while arming lock', - 'LOG_CARD_GOOD_READ - Successful read from card: ', - 'LOG_CARD_ACCEPTED - Accepted card: ', - 'LOG_CARD_DENIED - Denied card: ', - 'LOG_UPDATE_FAILED - Firmware update failed, code: ', -] - - -@api_view(['POST']) -def infolog(request, mac): - entries_processed = 0 - oldest_valid_log_time = time.time() - - tool = get_object_or_404(models.Tool, mac=mac) - - encoded_log = request.data.get('log') - if encoded_log: - decoded_log = base64.b64decode(encoded_log) - unpacked_log = list(struct.iter_unpack(' VALID_TIME: - oldest_valid_log_time = entry[0] - - with open(LOG_DIRECTORY + '/devices/' + mac + '.log', 'a') as log_file: - for entry in unpacked_log: - # if time is obviously wrong, just use oldest valid log time or now - entry_time = entry[0] if entry[0] > VALID_TIME else oldest_valid_log_time - entry_time_string = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(entry_time)) - - entry_event = EVENTS[entry[1]] - entry_data = entry[2].decode('utf-8').strip('\0') - - if entry_data: - user = models.User.objects.filter(profile__cards__number=entry_data) - if user.count(): - entry_data += ' (' + str(user.first()) + ')' - - entry_string = '{} - {} - {}{}'.format(entry_time_string, mac, entry_event, entry_data) - - entries_processed += 1 - log_file.write(entry_string + '\n') - - version = str(get_object_or_404(models.Firmware, tools=tool)) - version_string = '{} {} {}'.format(FIRMWARE_VERSION_MAGIC, version, FIRMWARE_VERSION_MAGIC) - - response_object = { - 'processed': entries_processed, - 'unixTime': int(time.time()), - 'version': version_string, - } - - return Response(response_object, status=status.HTTP_200_OK) - -@api_view(['GET']) -def update(request, mac): - tool = get_object_or_404(models.Tool, mac=mac) - firmware = get_object_or_404(models.Firmware, tools=tool) - - response = HttpResponse(firmware.binary, content_type='text/plain') - response['Content-Disposition'] = 'attachment; filename=firmware_{}.bin'.format(firmware.version) - return response - -@api_view(['PUT']) -@permission_classes((permissions.IsAuthenticated,)) -def select_courses(request): - courses = request.data.get('courses') - if courses is None: - return Response({'error': 'Please provide a list of course slugs'}, - status=status.HTTP_400_BAD_REQUEST) - - profile = get_object_or_404(models.Profile, user=request.user) - - if profile.courses.count() or profile.selected_courses: - return Response({'error': 'Please provide a list of course slugs'}, - status=status.HTTP_400_BAD_REQUEST) - - course_objects = get_list_or_404(models.Course, slug__in=courses) - profile.courses.set(course_objects) - profile.selected_courses = True - profile.save() - - return Response({'updated': len(courses)}, status=status.HTTP_200_OK) diff --git a/authserver/authserver/settings.py b/authserver/authserver/settings.py index eb8c463..a14b0de 100644 --- a/authserver/authserver/settings.py +++ b/authserver/authserver/settings.py @@ -130,3 +130,5 @@ MEDIA_URL = '/media/' PROTOSPACE_LOGIN_PAGE = 'https://my.protospace.ca/login' FIRMWARE_VERSION_MAGIC = 'MRWIZARD' + +UPGRADE_GROUPS = ['Critical', 'Testing', 'Development'] diff --git a/authserver/authserver/urls.py b/authserver/authserver/urls.py index 5644268..466d2b8 100644 --- a/authserver/authserver/urls.py +++ b/authserver/authserver/urls.py @@ -20,7 +20,7 @@ from django.urls import path from django.conf.urls import url, include from rest_framework import routers -from .api import views +from .api import views, customroutes router = routers.DefaultRouter() router.register(r'tool', views.ToolViewSet) @@ -35,12 +35,12 @@ urlpatterns = [ url(r'^', include(router.urls)), url(r'^admin/', admin.site.urls), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - url(r'^login/', views.login), - url(r'^cards/(?P.*)/', views.cards), - url(r'^update-cards/', views.update_cards), - url(r'^infolog/(?P.*)/', views.infolog), - url(r'^update/(?P.*)/', views.update), - url(r'^select-courses/', views.select_courses), + url(r'^login/', customroutes.login), + url(r'^cards/(?P.*)/', customroutes.cards), + url(r'^update-cards/', customroutes.update_cards), + url(r'^infolog/(?P.*)/', customroutes.infolog), + url(r'^update/(?P.*)/', customroutes.update), + url(r'^select-courses/', customroutes.select_courses), ] if settings.DEBUG is True: