Skip to content
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

Merged
merged 1 commit into from
Dec 18, 2023
Merged

fix(user token): Stop leaking API token #61941

merged 1 commit into from
Dec 18, 2023

Conversation

ykamo001
Copy link
Contributor

@ykamo001 ykamo001 commented Dec 18, 2023

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

@ykamo001 ykamo001 self-assigned this Dec 18, 2023
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Dec 18, 2023
Copy link

codecov bot commented Dec 18, 2023

Codecov Report

Merging #61941 (0031657) into master (b545add) will decrease coverage by 0.01%.
Report is 41 commits behind head on master.
The diff coverage is 83.33%.

❗ Current head 0031657 differs from pull request most recent head 1e0a9bc. Consider uploading reports for the commit 1e0a9bc to get more accurate results

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     
Files Coverage Δ
src/sentry/api/endpoints/api_tokens.py 96.49% <ø> (ø)
...atic/app/utils/analytics/replayAnalyticsEvents.tsx 100.00% <ø> (ø)
static/app/utils/replays/replayReader.tsx 50.00% <ø> (ø)
static/app/views/monitors/details.tsx 0.00% <ø> (ø)
src/sentry/api/serializers/models/apitoken.py 89.47% <83.33%> (-4.28%) ⬇️

... and 30 files with indirect coverage changes

@@ -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)
Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

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 :/

Copy link
Contributor Author

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:

token = ApiTokenSerializer().serialize(api_token, attrs, request.user)

@schew2381
Copy link
Contributor

schew2381 commented Dec 18, 2023

Could you write a test in test_apitoken.py that ensures the new serializer checks work as expected?

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
@ykamo001
Copy link
Contributor Author

Could you write a test in test_apitoken.py that ensures the new serializer checks work as expected?

Yes of course! Added!

Copy link
Contributor

@nhsiehgit nhsiehgit left a 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):
Copy link
Contributor

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

Copy link
Contributor Author

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

Screenshot 2023-12-18 at 3 01 44 PM

@@ -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)
Copy link
Contributor

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?

@ykamo001 ykamo001 enabled auto-merge (squash) December 18, 2023 23:11
Copy link
Contributor

@schew2381 schew2381 left a 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()
Copy link
Contributor

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.

Copy link
Contributor Author

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!

Comment on lines +8 to +9
self._scopes = ["test_scope"]
self._token = self.create_user_auth_token(user=self._user, scope_list=self._scopes)
Copy link
Contributor

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

Suggested change
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=[])

Copy link
Contributor Author

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)?

Copy link
Contributor Author

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!

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@schew2381 schew2381 Dec 18, 2023

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

Copy link
Contributor

@schew2381 schew2381 Dec 18, 2023

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

@ykamo001 ykamo001 merged commit 633debb into master Dec 18, 2023
49 checks passed
@ykamo001 ykamo001 deleted the enterprise-1-be branch December 18, 2023 23:20
@schew2381
Copy link
Contributor

small nits but lgtm otherwise 👍

Oh whoops, didnt realize auto merge was on lol

@github-actions github-actions bot locked and limited conversation to collaborators Jan 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Scope: Backend Automatically applied to PRs that change backend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants