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

feat: improved user auth tokens #68148

Merged
merged 17 commits into from
Apr 17, 2024
Merged

feat: improved user auth tokens #68148

merged 17 commits into from
Apr 17, 2024

Conversation

mdtro
Copy link
Member

@mdtro mdtro commented Apr 3, 2024

Supports getsentry/rfcs#32.

  • Newly created user auth tokens will be prefixed with sntryu_.
  • Introduce a custom model manager for ApiToken to handle the unique creation logic where we need to hash the token values and store them.
  • Use the token_type (currently optional) parameter/field on ApiToken when creating user auth tokens to let the model do the heavy lifting on generating and hashing the values. This will keep the logic out of views and simplify calls to just new_token = ApiToken.objects.create(user=user, token_type=AuthTokenType.USER).
  • I've changed some behavior where we only return the refreshToken on applicable token types.

I introduce a "read once" pattern in this PR for the token secrets to prevent leaking of them in logs, exceptions, etc. It works like this when creating a new ApiToken:

  1. The model manager sets temporary fields __plaintext_token and __plaintext_refresh_token that store the respective plaintext values for temporary reading.
  2. When reading the value through the _plaintext_token property on ApiToken (notice the single prepended underscore) the string value is returned and __plaintext_token is immediately set to None.
  3. If you attempt to read the _plaintext_token property again, an exception will be raised, PlaintextSecretAlreadyRead.
    • I opted to raise an exception here rather than just returning None as I feel it's more explicit and will prevent unexpected behavior when we accidentally expect a value to be there.

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
Copy link

codecov bot commented Apr 3, 2024

Codecov Report

Attention: Patch coverage is 97.93814% with 2 lines in your changes are missing coverage. Please review.

Project coverage is 79.68%. Comparing base (7a2578b) to head (42bb344).
Report is 8 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #68148      +/-   ##
==========================================
+ Coverage   78.75%   79.68%   +0.93%     
==========================================
  Files        6428     6429       +1     
  Lines      285278   285379     +101     
  Branches    49136    49158      +22     
==========================================
+ Hits       224681   227415    +2734     
+ Misses      60229    57596    -2633     
  Partials      368      368              
Files Coverage Δ
src/sentry/api/endpoints/api_tokens.py 100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py 100.00% <100.00%> (ø)
src/sentry/testutils/factories.py 96.00% <100.00%> (+0.34%) ⬆️
src/sentry/testutils/helpers/backups.py 99.70% <100.00%> (+<0.01%) ⬆️
src/sentry/web/frontend/setup_wizard.py 97.00% <100.00%> (+0.03%) ⬆️
src/sentry/models/apitoken.py 98.96% <97.75%> (-1.04%) ⬇️

... and 234 files with indirect coverage changes

Copy link

codecov bot commented Apr 3, 2024

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle name Size Change
sentry-webpack-bundle-array-push 26.28MB 12.94kB ⬆️

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.

hmmm this looks mostly sane to me?

I feel like i would need to significantly refresh my context here to be fully comfortable to sign off on it 😅

src/sentry/models/apitoken.py Outdated Show resolved Hide resolved
@azaslavsky azaslavsky self-requested a review April 4, 2024 23:50
Copy link
Contributor

@azaslavsky azaslavsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, LGTM from the relocation perspective!

@@ -28,10 +29,98 @@ def default_expiration():
return timezone.now() + DEFAULT_EXPIRATION


def generate_token():
def generate_token(token_type: AuthTokenType | str | None = AuthTokenType.__empty__) -> str:
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 want to support these many types? Also it looks like we default to this empty type, would we need the other types and None?

It might be helpful to add some doc strings to this function so people know how to use it and what the intention/difference is based on the input params

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was running in to some issues with mypy on this when I originally just had the type as AuthTokenType.
The particular field on the class is a CharField so it's expecting a str.

Is there a way around this?

token_type = models.CharField(max_length=7, choices=AuthTokenType, null=True)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just depends what you want to support in the method, and the caller has to make sure to appease the contract. What is the expected flow or contract we want to have?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, as we improve all of the various token types, I would like to require AuthTokenType. All newly generated tokens would be classified with this and we wouldn't allow a None. Right now, the None is there for backwards compatibility.

Since we've set choices on the CharField, then I suspect we will end up with a validation error if someone provides a string that doesn't match one of the choices. 🤔 I'll test that out as we progress through the token types.

src/sentry/models/apitoken.py Outdated Show resolved Hide resolved
src/sentry/models/apitoken.py Show resolved Hide resolved
src/sentry/models/apitoken.py Show resolved Hide resolved
src/sentry/models/apitoken.py Outdated Show resolved Hide resolved
src/sentry/models/apitoken.py Outdated Show resolved Hide resolved
mdtro added 17 commits April 17, 2024 16:05
- Setting the plaintext values on the manager class is wrong and would
cause issues when creating multiple instances of ApiToken.
  - it results in the plaintext value always being the latest instance
    of ApiToken that was created
  - moving this to the model fixes the issue
- introduce setter functions for the plaintext token values
- add docstrings
- remove leading `_` on property functions that return the the plaintext
token values
- after reading, set the token to string value stored in `TOKEN_REDACTED` so
it can still be printed and we can search for the string in log data
where accidental leaks may happen
  - we still throw `PlaintextSecretAlreadyRead` when attempting to read
    the value more than once
- update tests to access correct property
- ignore typing error
Copy link
Contributor

@ykamo001 ykamo001 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making all the changes!! Excited about the new flow!

@mdtro mdtro merged commit c1d6984 into master Apr 17, 2024
49 checks passed
@mdtro mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
HazAT added a commit that referenced this pull request Apr 22, 2024
In the wizard endpoint, we’d reuse existing user auth tokens of the
authenticated user if:
1. the user was part of multiple orgs (==> we can't create an org-based
token)
2. AND we found one that satisfied the necessary permissions for
sourcemap upload.
 
With #68148 being merged, we
cannot do this anymore. Plain user auth token values are only gonna be
available directly after the token was created.

For the fix, this PR makes a change to the wizard endpoint to always
create a new user API token. This now works just like when we create an
org token for single-org users.

Closes: #69381

---------

Co-authored-by: Daniel Griesser <[email protected]>
MichaelSun48 pushed a commit that referenced this pull request Apr 25, 2024
In the wizard endpoint, we’d reuse existing user auth tokens of the
authenticated user if:
1. the user was part of multiple orgs (==> we can't create an org-based
token)
2. AND we found one that satisfied the necessary permissions for
sourcemap upload.
 
With #68148 being merged, we
cannot do this anymore. Plain user auth token values are only gonna be
available directly after the token was created.

For the fix, this PR makes a change to the wizard endpoint to always
create a new user API token. This now works just like when we create an
org token for single-org users.

Closes: #69381

---------

Co-authored-by: Daniel Griesser <[email protected]>
@github-actions github-actions bot locked and limited conversation to collaborators May 3, 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