feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: improved user auth tokens - #68148

Merged
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu
Apr 17, 2024
Merged

feat: improved user auth tokens#68148
mdtro merged 17 commits into
masterfrom
mdtro/apitoken-sntryu

Conversation

@mdtro

@mdtromdtro commented Apr 3, 2024

Copy link
Copy Markdown
Contributor

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-actionsgithub-actionsBot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2024
@mdtro
mdtroforce-pushed the mdtro/apitoken-sntryu branch from 2ea65f7 to b165afeCompareApril 3, 2024 04:12
@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

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 
FilesCoverage Δ
src/sentry/api/endpoints/api_tokens.py100.00% <100.00%> (ø)
src/sentry/api/serializers/models/apitoken.py100.00% <100.00%> (ø)
src/sentry/testutils/factories.py96.00% <100.00%> (+0.34%)⬆️
src/sentry/testutils/helpers/backups.py99.70% <100.00%> (+<0.01%)⬆️
src/sentry/web/frontend/setup_wizard.py97.00% <100.00%> (+0.03%)⬆️
src/sentry/models/apitoken.py98.96% <97.75%> (-1.04%)⬇️

... and 234 files with indirect coverage changes

@codecov

codecovBot commented Apr 3, 2024

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.94kB ⬆️

Bundle nameSizeChange
sentry-webpack-bundle-array-push26.28MB12.94kB ⬆️

@nhsiehgitnhsiehgit left a comment

Copy link
Copy Markdown
Contributor

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 😅

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadtests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
@azaslavsky
azaslavsky self-requested a review April 4, 2024 23:50

@azaslavskyazaslavsky left a comment

Copy link
Copy Markdown
Contributor

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!

Comment threadsrc/sentry/models/apitoken.py Outdated

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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?

https://github.com/getsentry/sentry/blob/3a86fa61bf4293c0eed9837f6b0529bda15dfbc4/src/sentry/models/apitoken.py#L135

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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.

Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
Comment threadsrc/sentry/models/apitoken.py Outdated
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

@ykamo001ykamo001 left a comment

Copy link
Copy Markdown
Contributor

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 masterApr 17, 2024
@mdtro
mdtro deleted the mdtro/apitoken-sntryu branch April 17, 2024 22:26
@HazATHazAT mentioned this pull request Apr 22, 2024
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 <daniel.griesser.86@gmail.com>
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 <daniel.griesser.86@gmail.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 3, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: BackendAutomatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mdtro@azaslavsky@ykamo001@nhsiehgit