-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add description field * add AccessTokenCreateView * token reveal workflow * do not allow API access with token of inactive user * add password revokation view * squash migration * add user token tests * expand documentation on API tokens * expand documentation on API tokens * self-review improvements * some obvious review fixes * seperate personal API tokens from OAuth grants.
- Loading branch information
1 parent
b9d4aec
commit 86dc616
Showing
30 changed files
with
809 additions
and
193 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication | ||
|
||
from ephios.api.models import AccessToken | ||
from ephios.core.models import UserProfile | ||
|
||
|
||
class CustomOAuth2Authentication(OAuth2Authentication): | ||
""" | ||
Overwrites the default OAuth2Authentication to not allow inactive users to authenticate. | ||
""" | ||
|
||
def authenticate(self, request): | ||
oauth_result = super().authenticate(request) | ||
if oauth_result is None: | ||
return None | ||
user, token = oauth_result | ||
if not user.is_active: | ||
return None | ||
return user, token | ||
|
||
|
||
def revoke_all_access_tokens(user: UserProfile): | ||
for access_token in AccessToken.objects.filter(user=user): | ||
access_token.revoke_related() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from oauth2_provider.urls import base_urlpatterns | ||
|
||
app_name = "oauth2_provider" | ||
|
||
management_urlpatterns = [ | ||
# views to manage oauth2 applications are part of the API django app url config under the "API" namespace | ||
] | ||
|
||
urlpatterns = base_urlpatterns + management_urlpatterns |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,124 @@ | ||
from django import forms | ||
from django.contrib import messages | ||
from django.core.exceptions import PermissionDenied | ||
from django.shortcuts import get_object_or_404, redirect | ||
from django.urls import reverse_lazy | ||
from django.utils import timezone | ||
from django.utils.crypto import get_random_string | ||
from django.utils.safestring import mark_safe | ||
from django.utils.translation import gettext_lazy as _ | ||
from django.views import View | ||
from django.views.generic import CreateView, TemplateView | ||
from guardian.mixins import LoginRequiredMixin | ||
from oauth2_provider import views as oauth2_views | ||
from oauth2_provider.models import get_application_model | ||
from oauth2_provider.views import ApplicationList | ||
from oauth2_provider.scopes import get_scopes_backend | ||
|
||
from ephios.api.models import AccessToken | ||
from ephios.extra.mixins import StaffRequiredMixin | ||
from ephios.extra.widgets import CustomSplitDateTimeWidget | ||
|
||
|
||
class AllUserApplicationList(StaffRequiredMixin, ApplicationList): | ||
class AllUserApplicationList(StaffRequiredMixin, oauth2_views.ApplicationList): | ||
def get_queryset(self): | ||
return get_application_model().objects.all() | ||
|
||
|
||
class ApplicationDelete(oauth2_views.ApplicationDelete): | ||
success_url = reverse_lazy("api:settings-oauth-app-list") | ||
|
||
|
||
class AccessTokensListView(oauth2_views.AuthorizedTokensListView): | ||
template_name = "api/access_token_list.html" | ||
|
||
context_object_name = "personal_access_tokens" | ||
|
||
def get_queryset(self): | ||
# personal API tokens | ||
qs = super().get_queryset().filter(user=self.request.user, application__id=None) | ||
return qs.order_by("-created") | ||
|
||
def get_context_data(self, *, object_list=None, **kwargs): | ||
context = super().get_context_data(object_list=object_list, **kwargs) | ||
context["oauth_access_tokens"] = AccessToken.objects.filter( | ||
user=self.request.user, | ||
application_id__isnull=False, | ||
) | ||
return context | ||
|
||
|
||
class TokenScopesChoiceField(forms.MultipleChoiceField): | ||
def clean(self, value): | ||
scopes_list = super().clean(value) | ||
return " ".join(scopes_list) | ||
|
||
def to_python(self, value): | ||
# this should be the corresponding method to clean, | ||
# but it is not called/tested | ||
if isinstance(value, str): | ||
return value.split(" ") | ||
return value | ||
|
||
|
||
class AccessTokenForm(forms.ModelForm): | ||
description = forms.CharField( | ||
widget=forms.TextInput(attrs={"placeholder": _("What is this token for?")}), | ||
required=True, | ||
) | ||
scope = TokenScopesChoiceField( | ||
choices=[ | ||
(scope, mark_safe(f"<code>{scope}</code>: {description}")) | ||
for scope, description in get_scopes_backend().get_all_scopes().items() | ||
], | ||
widget=forms.CheckboxSelectMultiple, | ||
help_text=_("For security reasons, only select the scopes that are actually needed."), | ||
) | ||
expires = forms.SplitDateTimeField( | ||
widget=CustomSplitDateTimeWidget, | ||
required=False, | ||
) | ||
|
||
class Meta: | ||
model = AccessToken | ||
fields = ["description", "scope", "expires"] | ||
|
||
|
||
def generate_key(): | ||
return get_random_string(50) | ||
|
||
|
||
class AccessTokenCreateView(LoginRequiredMixin, CreateView): | ||
model = AccessToken | ||
form_class = AccessTokenForm | ||
template_name = "api/access_token_form.html" | ||
|
||
def form_valid(self, form): | ||
"""If the form is valid, save the associated model.""" | ||
token = form.save(commit=False) | ||
token.user = self.request.user | ||
token.token = generate_key() | ||
token.save() | ||
return redirect("api:settings-access-token-reveal", pk=token.pk) | ||
|
||
|
||
class AccessTokenRevealView(LoginRequiredMixin, TemplateView): | ||
template_name = "api/access_token_reveal.html" | ||
|
||
def get(self, request, *args, **kwargs): | ||
try: | ||
token = AccessToken.objects.get(pk=self.kwargs["pk"], user=self.request.user) | ||
except AccessToken.DoesNotExist: | ||
raise PermissionDenied | ||
if token.created < timezone.now() - timezone.timedelta(seconds=30): | ||
messages.error(request, _("Token is too old to be revealed.")) | ||
return redirect("api:settings-access-token-list") | ||
context = self.get_context_data(token=token, **kwargs) | ||
return self.render_to_response(context) | ||
|
||
|
||
class AccessTokenRevokeView(LoginRequiredMixin, View): | ||
def post(self, request, *args, **kwargs): | ||
# user can only revoke own tokens | ||
get_object_or_404(AccessToken, pk=request.POST["pk"], user=request.user).revoke_related() | ||
messages.success(request, _("Token was revoked.")) | ||
return redirect("api:settings-access-token-list") |
27 changes: 27 additions & 0 deletions
27
ephios/api/migrations/0002_accesstoken_description_accesstoken_revoked_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Generated by Django 4.1.7 on 2023-05-28 18:11 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("api", "0001_initial"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="accesstoken", | ||
name="description", | ||
field=models.CharField(blank=True, max_length=1000, verbose_name="Description"), | ||
), | ||
migrations.AddField( | ||
model_name="accesstoken", | ||
name="revoked", | ||
field=models.BooleanField(null=True), | ||
), | ||
migrations.AlterField( | ||
model_name="accesstoken", | ||
name="expires", | ||
field=models.DateTimeField(null=True), | ||
), | ||
] |
Oops, something went wrong.