From c0abaa535247aee031e72cf32411e772f18b53a9 Mon Sep 17 00:00:00 2001 From: NEZRI Ygal Date: Fri, 19 Jul 2024 16:22:42 +0200 Subject: [PATCH] API Key Creation & Management Added functionality for superusers and users to create and manage API keys, with Knox integration for secure key hashing. --- Watcher/Watcher/accounts/admin.py | 222 +++++++++++++++++----------- Watcher/Watcher/accounts/api.py | 24 ++- Watcher/Watcher/accounts/models.py | 15 +- Watcher/Watcher/watcher/settings.py | 8 +- 4 files changed, 160 insertions(+), 109 deletions(-) diff --git a/Watcher/Watcher/accounts/admin.py b/Watcher/Watcher/accounts/admin.py index 49a8dbb..af4b990 100644 --- a/Watcher/Watcher/accounts/admin.py +++ b/Watcher/Watcher/accounts/admin.py @@ -1,18 +1,18 @@ from django.contrib import admin + +# Import for Log Entries Snippet from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION from django.utils.html import escape from django.urls import reverse, NoReverseMatch from django.contrib.auth.models import User from django.utils.safestring import mark_safe +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from .models import APIKey from .api import generate_api_key from django.contrib import messages from django import forms from django.utils import timezone from datetime import timedelta -from knox.models import AuthToken -from django.db.models.signals import post_delete -from django.dispatch import receiver """ Log Entries Snippet @@ -82,6 +82,7 @@ class LogEntryAdmin(admin.ModelAdmin): UserFilter, ActionFilter, 'content_type', + # 'user', ] search_fields = [ @@ -131,82 +132,130 @@ def action_description(self, obj): action_description.short_description = 'Action' + admin.site.register(LogEntry, LogEntryAdmin) +class UserAdmin(BaseUserAdmin): + actions = ['generate_api_key'] + + def generate_api_key(self, request, queryset): + for user in queryset: + raw_key, hashed_key = generate_api_key(user) + if raw_key: + self.message_user(request, f"API Key generated for {user.username}: {raw_key[:10]}...") + else: + self.message_user(request, f"Failed to generate API Key for {user.username}", level='ERROR') + + generate_api_key.short_description = "Generate API Key" + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) + + +class ReadOnlyTextInput(forms.TextInput): + def render(self, name, value, attrs=None, renderer=None): + if value: + truncated_value = value[:5] + '*' * 59 + return f'{truncated_value}' + return super().render(name, value, attrs, renderer) + + class APIKeyForm(forms.ModelForm): EXPIRATION_CHOICES = ( - (1, '1 day'), (7, '7 days'), (30, '30 days'), (60, '60 days'), (90, '90 days'), (365, '1 year'), (730, '2 years'), + (1, '1 day'), + (7, '7 days'), + (30, '30 days'), + (60, '60 days'), + (90, '90 days'), + (365, '1 year'), + (730, '2 years'), ) expiration = forms.ChoiceField(choices=EXPIRATION_CHOICES, label='Expiration', required=True) - user = forms.ModelChoiceField(queryset=User.objects.all(), label='User', required=True) - + class Meta: - fields = ['user', 'expiration'] + model = APIKey + fields = ['user', 'key', 'expiration', 'expiry_at'] def __init__(self, *args, **kwargs): self.request = kwargs.pop('request', None) super().__init__(*args, **kwargs) - - if not self.instance or not self.instance.pk: - self.fields['expiration'].initial = 30 - - else: + instance = kwargs.get('instance') + if instance and instance.pk: + if 'key' in self.fields: + self.fields['key'].widget.attrs['readonly'] = True + self.fields['key'].widget = ReadOnlyTextInput() if 'user' in self.fields: - self.fields['user'].widget = forms.HiddenInput() + self.fields['user'].widget.attrs['readonly'] = True + if 'expiry_at' in self.fields: + self.fields['expiry_at'].widget.attrs['readonly'] = True if 'expiration' in self.fields: - self.fields['expiration'].widget = forms.HiddenInput() - - if self.request and not self.request.user.is_superuser: - self.fields['user'].queryset = User.objects.filter(id=self.request.user.id) - self.fields['user'].initial = self.request.user + if not self.request.user.is_superuser: + self.fields['expiration'].widget.attrs['disabled'] = True + else: + self.fields['expiration'].widget.attrs['readonly'] = True else: - self.fields['user'].queryset = User.objects.all() + if 'key' in self.fields: + self.fields['key'].widget = forms.HiddenInput() + if 'expiry_at' in self.fields: + self.fields['expiry_at'].widget = forms.HiddenInput() + + if self.request and not self.request.user.is_superuser: + self.fields['user'].queryset = User.objects.filter(id=self.request.user.id) + self.fields['user'].initial = self.request.user + else: + self.fields['user'].queryset = User.objects.all() + + def clean_key(self): + instance = getattr(self, 'instance', None) + if instance and instance.pk: + return instance.key + return self.cleaned_data.get('key', '') + + def clean_expiration(self): + expiration = self.cleaned_data.get('expiration') + if expiration: + try: + expiration = int(expiration) + if expiration not in [choice[0] for choice in self.EXPIRATION_CHOICES]: + raise forms.ValidationError('Invalid expiration value.') + except ValueError: + raise forms.ValidationError('Invalid expiration value.') + return expiration def save(self, commit=True): instance = super().save(commit=False) - expiration_days = int(self.cleaned_data['expiration']) - instance.get_expiry = timezone.now() + timezone.timedelta(days=expiration_days) + expiration = self.cleaned_data.get('expiration') + + if expiration: + instance.expiry_at = timezone.now() + timedelta(days=int(expiration)) if commit: instance.save() + return instance + class APIKeyAdmin(admin.ModelAdmin): - list_display = ('get_user', 'get_digest', 'get_created', 'get_expiry') + list_display = ('user', 'shortened_key', 'created_at', 'expiry_at_display') form = APIKeyForm readonly_fields = ('key_details',) - def get_user(self, obj): - return obj.auth_token.user if obj.auth_token else None - - def get_digest(self, obj): - return obj.auth_token.digest if obj.auth_token else None - - def get_created(self, obj): - return obj.auth_token.created.strftime("%b %d, %Y, %-I:%M %p").replace('AM', 'a.m.').replace('PM', 'p.m.') if obj.auth_token else None - - def get_expiry(self, obj): - return obj.auth_token.expiry.strftime("%b %d, %Y, %-I:%M %p").replace('AM', 'a.m.').replace('PM', 'p.m.') if obj.auth_token else None - - get_user.short_description = 'User' - get_digest.short_description = 'Digest' - get_created.short_description = 'Created' - get_expiry.short_description = 'Expiry' + def get_queryset(self, request): + if request.user.is_superuser: + return APIKey.objects.all() + else: + return APIKey.objects.filter(user=request.user) def has_add_permission(self, request): return True - def get_queryset(self, request): - qs = super().get_queryset(request) - if not request.user.is_superuser: - qs = qs.filter(auth_token__user=request.user) - return qs - def get_form(self, request, obj=None, **kwargs): kwargs['form'] = self.form form = super().get_form(request, obj, **kwargs) - + if 'key' in form.base_fields: + form.base_fields['key'].widget = ReadOnlyTextInput() + class CustomAPIKeyForm(form): def __init__(self, *args, **kwargs): kwargs['request'] = request @@ -215,12 +264,20 @@ def __init__(self, *args, **kwargs): return CustomAPIKeyForm def save_model(self, request, obj, form, change): - if not obj.pk: - user = form.cleaned_data['user'] - expiration = form.cleaned_data['expiration'] - raw_key, auth_token = generate_api_key(user, int(expiration)) - obj.auth_token = auth_token - obj.save() + if not obj.key: + user = request.user + expiration_days = int(form.cleaned_data.get('expiration', 30)) + raw_key, hashed_key = generate_api_key(user, expiration_days) + obj.key = hashed_key + obj.expiry_at = timezone.now() + timedelta(days=expiration_days) + hash_parts = hashed_key.split('$') + obj.key_details = ( + f"algorithm: pbkdf2_sha256 \n " + f"iterations: {hash_parts[1]}\n " + f"salt: {hash_parts[2][:8]}{'*' * (len(hash_parts[2]) - 8)}\n " + f"hash: {hash_parts[3][:8]}{'*' * (len(hash_parts[3]) - 8)}\n\n" + f"Raw API keys are not stored, so there is no way to see this user’s API key." + ) copy_button = f'''