-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
fix(user token): Stop leaking API token #61941
Conversation
49664b6
to
93023e3
Compare
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## master #61941 +/- ##
==========================================
- Coverage 81.22% 81.21% -0.01%
==========================================
Files 5185 5185
Lines 228404 228533 +129
Branches 38328 38371 +43
==========================================
+ Hits 185511 185613 +102
- Misses 37246 37265 +19
- Partials 5647 5655 +8
|
93023e3
to
0031657
Compare
@@ -27,6 +27,13 @@ def serialize(self, obj, attrs, user): | |||
"state": attrs.get("state"), | |||
} | |||
if not attrs["application"]: | |||
data["token"] = obj.token | |||
include_token = kwargs.get("include_token", 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.
This should default to false in the future, but currently is true because other endpoints and services are also utilizing this serializer
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.
rather than passing include_token
in the kwargs, can we be explicit in the args, and just default it to False if that's what we expect?
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 related to the comment above, but in relation to the False
matter:
Unfortunately we cannot default to False
, as other API endpoints are referencing this serializer for their contract, and we would need to change more domain code than necessary for this change. This is the result of not using repository patterns and proper contracts/DTOs :/
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.
Here's another service/endpoint that uses the same serliazier:
sentry/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/index.py
Line 69 in cd40441
token = ApiTokenSerializer().serialize(api_token, attrs, request.user) |
0031657
to
1e0a9bc
Compare
Could you write a test in |
Update API contract to stop sending API token and send new field of lastTokenCharacters instead add comments and update logic default to true for other endpoints get last 4 elements instead of first 4 add test file and cases
1e0a9bc
to
8829bbd
Compare
Yes of course! 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.
quick comment
@@ -4,7 +4,7 @@ | |||
|
|||
@register(ApiToken) | |||
class ApiTokenSerializer(Serializer): | |||
def get_attrs(self, item_list, user): | |||
def get_attrs(self, item_list, user, **kwargs): |
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.
Do we need to capture the kwargs here?
Would rather be explicit in the api args, rather than allowing any kwargs to be passed
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.
Unfortunately no, we cannot make a change the would break the contract of the base class function https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
Other incompatible signature changes in method overrides, such as adding an extra required parameter, or removing an optional parameter, will also generate errors.
This fails on our linter as well, as I tried that initially https://github.com/getsentry/sentry/actions/runs/7227889927/job/19696388755
@@ -27,6 +27,13 @@ def serialize(self, obj, attrs, user): | |||
"state": attrs.get("state"), | |||
} | |||
if not attrs["application"]: | |||
data["token"] = obj.token | |||
include_token = kwargs.get("include_token", 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.
rather than passing include_token
in the kwargs, can we be explicit in the args, and just default it to False if that's what we expect?
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.
small nits but lgtm otherwise 👍
|
||
class TestApiTokenSerializer(TestCase): | ||
def setUp(self) -> None: | ||
self._user = self.create_user() |
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.
self.user
is automatically set in TestCase
so you can just use that directly in your tests which is convenient.
Same thing for self.project
, self.team
, and self.organization
. The only difference is I think those are lazy so they only get created after you call it in the test.
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 good to know, thank you!
self._scopes = ["test_scope"] | ||
self._token = self.create_user_auth_token(user=self._user, scope_list=self._scopes) |
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.
Sorry I realize I should've defaulted to empty scopes in create_user_auth_token
if it wasn't relevant to the test
self._scopes = ["test_scope"] | |
self._token = self.create_user_auth_token(user=self._user, scope_list=self._scopes) | |
self._token = self.create_user_auth_token(user=self._user, scope_list=[]) |
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's okay, perfectly fine to be explicit about having empty scopes. Is that even a feasible token? Does that equate to unbounded (i.e. all permissions)?
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.
We should probably add more tests here that make sure scopes are also returned correctly, so it's okay to keep a not empty list. This area in general needs more coverage, but something to follow up on!
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.
We do allow empty scopes, the token would just be useless.
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.
Is that something we should change?
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.
hmm that's a good question. The only use case I can think of for having empty scopes is we allow users to create internal integrations without scopes. They're not required when you follow the creation flow.
I'm not super familiar with how people use integrations, or what you can do without a token/no scopes token. There might be a use case there
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.
For user tokens, we don't allow it in the UI and you can't create user tokens through other tokens (which is a recent-ish change) so it should be fine.
It's not great though that it's not immediately obvious this is the case for user tokens and is implicit/localized knowledge
Oh whoops, didnt realize auto merge was on lol |
Update API contract to stop sending API token and send new field of lastTokenCharacters instead. Currently the API token in user settings can always be seen, even after creation. This experience also deviates from the org auth tokens. To keep both experiences consistent, and increase security around tokens from leaking, we are updating the API token contract and UI. The current PR focuses on BE, and updating the contract to send a field
lastTokenCharacters
This PR should go after #61939
Resolves https://github.com/getsentry/team-enterprise/issues/1#issue-2003077670