-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Identifying owner of APIKey #20
Comments
It is not necessary so. You can create custom endpoint for key generation. For my project I created endpoint that allows unauthorized access and generates and returns the key to the caller. In the background this endpoint launches a process that grants access to the user to certain records in the database. Here is the example of filtering for the version from rest_framework_api_key.settings import TOKEN_HEADER
class MyView(ListAPIView):
def get_queryset(self):
token = self.request.META[TOKEN_HEADER]
queryset = MyModel.objects.filter(clients__token=token) # `clients` is M2M relation here
return queryset |
Hmm. Maybe this is a poor choice of words on my end. If you think about it, API keys don't have a built-in permission system (like OAuth does with scopes): they're simply an identifier that allows to access a resource based on the hypothesis that the person who sent the API key with the request is the same than the person who was given that API key in the first place. But it's not authentication either because we can't relate a particular user to the API key — precisely because the system that uses the API key does not have a Django In this sense it's probably more an identification system than anything else. Do you think this term could help avoid ambiguity?
I believe this should do the trick! I'm curious about the actual use case you have here — you're welcome to share more details if you can. :-) |
@florimondmanca yep, no problem. I understand where you're coming from. We are getting into some partner integrations that will connect to our platform to read data. However, these partners will only be able to read a subset of our endpoints, and within those, a subset of the resources. This is based on service tiers and also which of our users have selected the partners for integration. For example, I'd like to have X partner have access to It sounds like, a. "authentication" will happen when I generate the key/secret and give it to them. And then "authorization" will happen based on how I manage the ApiKey relationship -- like, attach ApiKeys to a Does that sound like a not-bad idea? :) |
@ssrebelious cool, I think I'm on the same page 👍 |
^ Oops! @camflan So, for example, user A says "I want to enable integration with Partner X", which means Partner X will access resources related to user A (and that of all other users who, like user A, have enabled the integration), and they'll do that using their (Partner X's) API key? I tried to convert that to code: from django.db import models
from django.contrib.auth.models import AbstractUser
from rest_framework.generics import ListAPIView
from rest_framework_api_key.models import APIKey
class Partner(models.Model):
api_key = models.OneToOneField(APIKey, on_delete=models.PROTECT)
...
class User(AbstractUser):
# Enabled by the user
integrations = models.ManyToManyField(Partner)
...
class Resource(models.Model):
owner = models.ForeignKey(User)
...
class ResourceListView(ListAPIView):
# Endpoint accessed by a partner
permission_classes = [HasAPIKey]
def get_queryset(self):
token = self.request.META["Api-Token"]
return Resource.objects.filter(owner__integrations__api_key__token=token) That I think you could simplify the query by having partners send their |
@florimondmanca Yep, that's really close to what I was thinking, schema/pseudocode wise. There are some ways to shortcut it like storing a bitfield in redis of userIds that are enabled per ApiKey or Partner, etc. Then you can just say, thoughts? |
Yes, that’s a real no-no in retrospect! As a middle ground you could simply retrieve the Partner that corresponds to the given API token (provided there is a 1-1 match between the two), and the filter query would basically be the same as my last suggestion. You’d need an extra database query to get the Partner but that would do the trick IMO. I’m not familiar with Redis bitfields but that does sound like a very performant alternative indeed. |
Ah, yeah. Duh - instead of a big join that’d be way easier. Good call 😎 I’m going to give this a shot tonight 👍🏻 |
@camflan Did you manage to make this work? I'm wondering if we should document this pattern somewhere — your experience could be useful in that regard. :-) |
I haven’t had a chance yet. I’ll definitely report back once I get it going and then we can add an example 👌🏼 |
@florimondmanca so, this does work but I'm not super in love with it. I'm going to keep going with it and see about refactoring/improving it later on or if I have an epiphany while finishing Here's an example, models, class IntegrationPartner(models.Model):
api_key = models.OneToOneField(APIKey, on_delete=models.PROTECT)
name = models.CharField(max_length=255,)
class Organization(models.Model):
integrations = models.ManyToManyField(Partner)
name = models.CharField(max_length=255,) views, class PartnerAPIView(APIView):
permission_classes = [permissions.IsAuthenticated | HasAPIKey]
renderer_classes = (JSONRenderer,)
def get_partner(self):
api_key = self.request.META.get("HTTP_X_API_KEY", None)
partner = None
if api_key:
try:
partner = IntegrationPartner.objects.get_by_api_key(api_key)
except IntegrationPartner.DoesNotExist:
pass
return (api_key is not None, partner)
class OrganizationView(PartnerAPIView):
def get(self, request):
user = request.user
qs = Organization.objects.exclude(is_active=False)
if user.is_authenticated:
return qs.filter(users=user)
(_, partner) = self.get_partner()
return qs.filter(integrations=partner)
class OrgUser(PartnerAPIView):
def get(self, request):
user = request.user
qs = OrgUser.objects.exclude(organization__is_active=False)
if user.is_authenticated:
return qs.filter(organization__users=user)
(_, partner) = self.get_partner()
return qs.filter(organization__integrations=partner) I also experimented with extending |
@camflan Thanks a ton for getting back to this! It’s good to see this solution is working out. Why are you not very happy about it? Does it feel hacky, or does it induce boilerplate? I guess if we tried to implement this over several models we would start to see a pattern emerging. For example, maybe we could add some model (or manager) mixin to aid with traversing relationships via an API key. This way we could encapsulate retrieving the API key fro headers and the like. |
Hi, I'm closing this for housekeeping purposes. I think the upcoming features in v1.3 (the abstract API key model and base permissions in particular) should help with this kind of use case. :-) See #42. Thanks! |
This looks like a great lib, though I have a few questions.
It appears as though this lib is positioned as an authorization lib, not authentication. As such, I assume that the authentication should happen before ApiKey creation? And because of that, I can assume it's authenticated when used to make a call to the API?
What's the best way to identify the owner of the ApiKey so I can manage which resources a particular request has access to? Is it as simple as using a FK to link this ApiKey to a user/organization/etc and then use that for filtering the querysets of the resources?
The text was updated successfully, but these errors were encountered: