-
Notifications
You must be signed in to change notification settings - Fork 96
Add new API endpoint to allow for create/edit/delete of tokens by user #648
Conversation
1404955
to
1e56cf5
Compare
69d7234
to
b4fd095
Compare
Api view needs to return expired tokens. I believe these are filtered out. |
I got the tests working just a few minutes before I have to leave today so I will polish and commit them when I am in the office on Wednesday! |
Was talking with Tharon, no need to have these tokens expire. |
3fb1ad5
to
ecd34d2
Compare
Removed token expireTime and finished the tests 👍 |
Creation of the d-c-a Token should pass an "issuer" |
core/models/access_token.py
Outdated
@@ -16,7 +16,7 @@ class Meta: | |||
app_label = "core" | |||
|
|||
def create_access_token(user, token_name=None, token_expire=None, remote_ip=None, issuer=None): | |||
token = Token(user=user) | |||
token = Token(user=user, issue=issuer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue -> issuer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WHOOPS!
f2906a4
to
ecbcd31
Compare
api/v2/views/access_token.py
Outdated
return qs.filter(only_current_access_tokens()) | ||
|
||
def create(self, request): | ||
issuer_backend = request.session.get('_auth_user_backend', '').split('.')[-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious, why are you looking this up in the session?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issuer may just as well be "Personal-Access-Token"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the only way I knew how to get the issuer by copying what is done in auth.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case you're the issuer. Do some searching for where we pass values for issuer in d-c-a, which should show its purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The session is a key value store which is used in web-apps to track a user across multiple stateless requests. It's a bag of properties. If a user submits requests and passes their session as a cookie, we'll know which session they're using. _auth_user_backend
is probably some relic of long ago
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I will look into that
A user shouldn't be allowed to create access tokens with the same name |
I was able to whip up some changes to disallow creating tokens with the same name. I just need to improve the error message, make some tests, and make the query only look for tokens with the same name AND same user (I didn't implement the user part yet). The change you mentioned in Tharon's PR about having the NotificationController show the error from the API will show the error message I am creating here. Unfortunately I can't finish this up until Friday. Class is so inconvenient 😢 |
I was thinking about this some more. There's nothing technically wrong with a user creating the same token from the api's perspective. Each token has a unique id, and the api response includes said ids. We don't really care if users do this for projects etc. |
Yeah that was my reasoning behind not enforcing it. But it also makes sense to enforce it because users can get confused if tokens have the same name (but they caused the confusion themselves). So it's a tricky decision to make. However, with projects, the users have other distinguishing information such as the creation date, description, and resources inside the project so it might make sense to enforce unique names with the tokens. |
In response to "Api view needs to return expired tokens. I believe these are filtered out.": Should I still do this now that the tokens do not expire? |
I created the commit for enforcing unique names because I had it mostly finished anyways so now we can look at that when deciding what to do. However, users are still able to rename tokens to have identical names because, to be honest, I am not sure how the editing works on the API side of this. Maybe we need to enforce that on the Troposphere side (in collections or stores)? Edit: I just did some searching and it looks like the edit is handled by the |
When you're enforcing some guarantee you want to enforce at the lowest level possible, a db constraint. If you can't enforce there, then you enforce at the api level, and hope your logic covers the edge cases (ex. you may have forgotten to replace update logic you mentioned above). The last place to add a constraint is in the ui, which is the weakest place, because no one has to use the frontend and they could just as easily change it to have different behavior. A db constraint can only exist on a given table. You would have to add a user field to your token, and then you could add unique constraint between (user, name). This would imply that those two columns MUST uniquely identify a record, the db wouldn't allow any inserts/updates/changes which would break that guarantee. This isn't great because the token model has a user. So your access_token.user and access_token.token.user would be redundant and would open up potential for bugs. One path here would be to remove the user field from token, a quick glance showed that it was unnecessary. Or you could just add it in the api, but none of our other apis do this checking. In each of those scenarios if the user is confused by making a duplicate, they can feel pretty responsible for doing something like that and not blame the application (so the weight isn't that important). A middle ground solution would be to allow duplicate names, but to show the creation date of the token. With this information users could likely figure out which duplicate was which. |
I don't think we need to worry about this issue. I still have some feedback on your pr i havn't mentioned, and functionally its good for MVP. |
agh accidentally closed |
Re: #648 (comment) |
Another reason to enforce constraints at the database level. The python api code runs in parallel. So the code you added says something like "If the token exists already with this name, then error". However if two requests to create a token with the same name happened in parallel, they would both be created with the same name. The database constraint would prevent this. |
core/models/access_token.py
Outdated
|
||
class AccessToken(models.Model): | ||
""" | ||
Extend the django_cyverse_auth Token to add a name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is literally describing what the model is, may explain how this is used in the context of the api.
core/models/access_token.py
Outdated
def create_access_token(user, token_name=None, token_expire=None, remote_ip=None, issuer=None): | ||
if AccessToken.objects.filter(name=token_name, token__user_id=user.id): | ||
return None | ||
token = Token(user=user, issuer=issuer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line and one below can be replaced by a single token = Token.objects.create(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this still check if a token with that name already exists? Or does it rely on the database enforcing that constraint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm referring to line 21, 22 here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh okay. To make the name enforced by the database, I can probably add unique=True
to the CharField and then I don't need 19 and 20 either
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would mean make the name unique across all tokens, so two users couldn't have token 'foobar'
core/models/access_token.py
Outdated
app_label = "core" | ||
|
||
def create_access_token(user, token_name=None, token_expire=None, remote_ip=None, issuer=None): | ||
if AccessToken.objects.filter(name=token_name, token__user_id=user.id): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AccessToken.objects.filter(name=token_name, token__user=user)
core/models/access_token.py
Outdated
return None | ||
token = Token(user=user, issuer=issuer) | ||
token.save() | ||
access_token, created = AccessToken.objects.update_or_create(token=token, name=token_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may have been a recent change, but you don't want update_or_create
. You just want create, a new AccessToken should always be returned, since a new Token is always created.
ad95e00
to
989ff50
Compare
# RunPython operations to refer to the local versions: | ||
# core.migrations.0088_set_project_leaders_and_authors | ||
# core.migrations.0089_projects_to_project__alter_created_by | ||
# core.migrations.0092_set_unknown_size_instance_status_history |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you replace this comment with a short summary of why this migration exists.
One last thing to look at, try using curl, and submitting gobbledigook:
I believe you're not using your serializer in your create method. You should have a line like |
6514f37
to
9d42edf
Compare
I created a |
Since the validate method returns the valid data, maybe I should use |
api/v2/views/access_token.py
Outdated
return AccessToken.objects.filter(token__user=self.request.user) | ||
|
||
def create(self, request): | ||
AccessTokenSerializer().validate(request.data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try replacing this line with:
++ serializer = self.get_serializer(data=request.data)
++ serializer.is_valid(raise_exception=True)
Your class is implementing an AuthModelViewSet, which is based off of rest_framework's ModelViewSet, which provides a default implementation for def create
. If you're going to override it, you just need to know what all its supposed to do. It basically takes serializer_class
above does some shit and returns a json repsonse from the serializers data. You don't have to implement the serializers validate method. That method is based on the serializers fields (which in turn are based on the fields defined on your model).
In a manage.py shell run:
from api.v2.serializers.details import AccessTokenSerializer
AccessTokenSerializer()
You'll see the serializer fields which are automatically populated and used in the serializers default validate method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A helpful trick when you're replacing a method:
def create(self, request):
import ipdb; ipdb.set_trace()
super(self.__class__, self).create(request)
That will call the superclass implementation and you can step through and see what it does.
api/v2/views/access_token.py
Outdated
@@ -22,7 +22,8 @@ def get_queryset(self): | |||
return AccessToken.objects.filter(token__user=self.request.user) | |||
|
|||
def create(self, request): | |||
AccessTokenSerializer().validate(request.data) | |||
serializer = self.get_serializer(data=request.data) | |||
serializer.is_valid(raise_exception=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 May seem a tad overkill for so simple an api, but this future proofs as more fields get added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to do it right the first time for sure. I appreciate all the feedback, I am learning a lot about APIs!
Once this is approved, I would like a chance to rebase and squash some of these commits before merge |
Troposphere expects error responses to be valid json. For example this is what rest_framework returns when i send gobbledygook:
A raw string is not technically a json document, and tropo expects to see one. Fixed in a42fee9 |
Oh yeah, there is still one problem: token names can be changed to match existing names without error. Any suggestions to fix this? |
api/tests/v2/test_access_tokens.py
Outdated
@@ -80,7 +80,7 @@ def test_create_same_name(self): | |||
force_authenticate(self.create_request, user=self.user) | |||
response = self.create_view(self.create_request) | |||
self.assertEquals(response.status_code, 400) | |||
self.assertEquals(response.data, "Token with name \"Test Token Creation\" exists.") | |||
self.assertEquals(response.data, {'detail': u'Token with name "Test Token Creation" exists.'}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
It would be a little more work, but we could give up on checking if the names are unique, and instead just include a creation date (the serializer would pull this field off the Token) |
The ui could list each token with its creation date, and the issue would no longer exist |
I sort of like that route, because in the absence of an expiration date, it would be nice to show how old a token is |
Another route would be to perform the validation in the update method |
I'll wip this up real quick |
45d1457
to
b68d84a
Compare
@calvinmclean feel free to rebase, squash, make sure you refetch this branch, its history has been altered |
a753d7a
to
2a4b0cc
Compare
Rebase complete for beautiful git history 👍 |
2a4b0cc
to
5a3597d
Compare
Created a new model, serializer, and APIv2 view for AccessTokens. These changes are intended to be compatible with a new Troposphere feature that allows users to manage tokens from the settings page that allow them to interact with the Atmosphere API.
Description
This PR adds an API endpoint that allows users to create/edit/delete named tokens that they can use to access Atmosphere from something like the Atmosphere CLI.
The tokens are a new model that extends the existing cyverse-django-auth tokens and adds a name.
The tokens have a 5 year expiration date.
I still need to create the migration for this!Related Troposphere PR
Checklist before merging Pull Requests
roles/sanitary-sql-access/templates/sanitize-dump.sh.j2
access_token
table existed but was empty/unused