-
-
Notifications
You must be signed in to change notification settings - Fork 272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix installation token cache issue #442
Merged
XAMPPRocky
merged 7 commits into
XAMPPRocky:main
from
matthewgapp:matt/fix/installation-cached-token-expired-after-awhile
Aug 24, 2023
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
591ea04
implemented logic to determine if token is expired
matthewgapp f85df26
debug
matthewgapp aafbf3e
added unit tests
matthewgapp 96bd7e3
wip
matthewgapp 1168e88
remove unused import
matthewgapp 03a6873
clean up naming
matthewgapp 95ba338
remove unused method
matthewgapp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -190,6 +190,7 @@ pub mod params; | |
pub mod service; | ||
use crate::service::body::BodyStreamExt; | ||
|
||
use chrono::{DateTime, Utc}; | ||
use http::{HeaderMap, HeaderValue, Method, Uri}; | ||
use std::convert::{Infallible, TryInto}; | ||
use std::fmt; | ||
|
@@ -760,18 +761,54 @@ impl DefaultOctocrabBuilderConfig { | |
|
||
pub type DynBody = dyn http_body::Body<Data = Bytes, Error = BoxError> + Send + Unpin; | ||
|
||
#[derive(Debug, Clone)] | ||
struct CachedTokenInner { | ||
expiration: Option<DateTime<Utc>>, | ||
secret: SecretString, | ||
} | ||
|
||
impl CachedTokenInner { | ||
fn new(secret: SecretString, expiration: Option<DateTime<Utc>>) -> Self { | ||
Self { secret, expiration } | ||
} | ||
|
||
fn expose_secret(&self) -> &str { | ||
self.secret.expose_secret() | ||
} | ||
} | ||
|
||
/// A cached API access token (which may be None) | ||
pub struct CachedToken(RwLock<Option<SecretString>>); | ||
pub struct CachedToken(RwLock<Option<CachedTokenInner>>); | ||
|
||
impl CachedToken { | ||
fn clear(&self) { | ||
*self.0.write().unwrap() = None; | ||
} | ||
fn get(&self) -> Option<SecretString> { | ||
self.0.read().unwrap().clone() | ||
|
||
/// Returns a valid token if it exists and is not expired or if there is no expiration date. | ||
fn valid_token_with_buffer(&self, buffer: chrono::Duration) -> Option<SecretString> { | ||
let inner = self.0.read().unwrap(); | ||
|
||
if let Some(token) = inner.as_ref() { | ||
if let Some(exp) = token.expiration { | ||
if exp - Utc::now() > buffer { | ||
return Some(token.secret.clone()); | ||
} | ||
} else { | ||
return Some(token.secret.clone()); | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
fn valid_token(&self) -> Option<SecretString> { | ||
self.valid_token_with_buffer(chrono::Duration::seconds(30)) | ||
} | ||
fn set(&self, value: String) { | ||
*self.0.write().unwrap() = Some(SecretString::new(value)); | ||
|
||
fn set(&self, token: String, expiration: Option<DateTime<Utc>>) { | ||
*self.0.write().unwrap() = | ||
Some(CachedTokenInner::new(SecretString::new(token), expiration)); | ||
} | ||
} | ||
|
||
|
@@ -1349,7 +1386,21 @@ impl Octocrab { | |
|
||
let token_object = | ||
InstallationToken::from_response(crate::map_github_error(response).await?).await?; | ||
token.set(token_object.token.clone()); | ||
|
||
let expiration = token_object | ||
.expires_at | ||
.map(|time| { | ||
DateTime::<Utc>::from_str(&time).map_err(|e| error::Error::Other { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Github doesn't document the timezone but I assumed it was UTC. Also checked a couple responses manually and verified that it was UTC. |
||
source: Box::new(e), | ||
backtrace: snafu::Backtrace::generate(), | ||
}) | ||
}) | ||
.transpose()?; | ||
|
||
tracing::debug!("Token expires at: {:?}", expiration); | ||
|
||
token.set(token_object.token.clone(), expiration); | ||
|
||
Ok(SecretString::new(token_object.token)) | ||
} | ||
|
||
|
@@ -1407,7 +1458,7 @@ impl Octocrab { | |
Some(HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue")) | ||
} | ||
AuthState::Installation { ref token, .. } => { | ||
let token = if let Some(token) = token.get() { | ||
let token = if let Some(token) = token.valid_token() { | ||
token | ||
} else { | ||
self.request_installation_auth_token().await? | ||
|
@@ -1523,4 +1574,57 @@ mod tests { | |
.await | ||
.unwrap(); | ||
} | ||
|
||
use super::*; | ||
use chrono::Duration; | ||
|
||
#[test] | ||
fn clear_token() { | ||
let cache = CachedToken(RwLock::new(None)); | ||
cache.set("secret".to_string(), None); | ||
cache.clear(); | ||
|
||
assert!(cache.valid_token().is_none(), "Token was not cleared."); | ||
} | ||
|
||
#[test] | ||
fn no_token_when_expired() { | ||
let cache = CachedToken(RwLock::new(None)); | ||
let expiration = Utc::now() + Duration::seconds(9); | ||
cache.set("secret".to_string(), Some(expiration)); | ||
|
||
assert!( | ||
cache | ||
.valid_token_with_buffer(Duration::seconds(10)) | ||
.is_none(), | ||
"Token should be considered expired due to buffer." | ||
); | ||
} | ||
|
||
#[test] | ||
fn get_valid_token_outside_buffer() { | ||
let cache = CachedToken(RwLock::new(None)); | ||
let expiration = Utc::now() + Duration::seconds(12); | ||
cache.set("secret".to_string(), Some(expiration)); | ||
|
||
assert!( | ||
cache | ||
.valid_token_with_buffer(Duration::seconds(10)) | ||
.is_some(), | ||
"Token should still be valid outside of buffer." | ||
); | ||
} | ||
|
||
#[test] | ||
fn get_valid_token_without_expiration() { | ||
let cache = CachedToken(RwLock::new(None)); | ||
cache.set("secret".to_string(), None); | ||
|
||
assert!( | ||
cache | ||
.valid_token_with_buffer(Duration::seconds(10)) | ||
.is_some(), | ||
"Token with no expiration should always be considered valid." | ||
); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider a token that expires within 30 seconds to be expired by default.
From what I can tell, the expiration is usually set about 1 hour from token mint.