-
Notifications
You must be signed in to change notification settings - Fork 177
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 inconsistent auth/identity creation #735
Conversation
8ec2052
to
9978a54
Compare
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.
I think this mostly makes sense to me! I left some questions and confusions.
Also I have no idea if there's any automated test coverage we could/should be updating or anything 🤷
(I also left some notes for other reviewers based on my read-through; I'm not looking for a response from you on those! 🙂)
crates/client-api/src/auth.rs
Outdated
#[derive(Clone, Deserialize)] | ||
pub struct SpacetimeCreds { | ||
token: String, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, sanity-check - is this type serialized+deserialized anywhere besides via the Credentials
trait? are we breaking compatibility with anything on some disk / over the wire / etc?
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.
Yeah, nowhere else - it's just for Credentials.
crates/standalone/src/routes/mod.rs
Outdated
database::control_routes(ctx.clone()).merge(database::worker_routes(ctx.clone())), | ||
) | ||
.nest("/identity", identity::router(ctx.clone())) | ||
.nest("/energy", energy::router(ctx.clone())) |
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.
probably a dumb question... why do only some of these routes get the ctx
? Since the function is generic, I'd kind of expect it to just pass along ctx
to everything?
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.
Limitations of axum, sorta - the middleware needs access to ctx, but axum doesn't let middleware access the state from the router, so you have to double up on passing the state.
TypedHeader(SpacetimeIdentity(auth.identity)), | ||
TypedHeader(SpacetimeIdentityToken(auth.creds)), | ||
res, | ||
)) |
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.
note for review: this got moved into the auth_middleware
Err(StatusCode::BAD_REQUEST.into()) | ||
} | ||
} else { | ||
Err(StatusCode::UNAUTHORIZED.into()) |
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.
note for review: this outer if
got eaten by the change to SpacetimeAuthRequired
Ok(( | ||
[(CONTENT_TYPE, "application/pem-certificate-chain")], | ||
ctx.public_key_bytes().to_owned(), | ||
)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, this one's confusing me - why did this change / is it the same behavior?
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.
Same behavior, yeah, just more idiomatic.
crates/client-api/src/auth.rs
Outdated
Jwt(JwtErrorKind), | ||
Header(TypedHeaderRejection), | ||
MalformedTokenQueryString, | ||
CantDecodeAuthorizationToken, |
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.
where did CantDecodeAuthorizationToken
go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That wasn't an error that could ever actually occur; we basically encoded the token to base64 and then decoded it from base64 and if the decoding failed we returned that error. It could've just been an unwrap() instead of a variant.
crates/client-api/src/auth.rs
Outdated
let Some(creds) = SpacetimeCreds::from_request_parts(parts)? else { | ||
return Ok(Self { auth: None }); | ||
}; | ||
let claims = creds.decode_token(state.public_key())?; |
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.
again, a dumb question - we used to do e.into_kind()
and wrap it in a specific return - does this work fine if we just return the "bare" Err
value here?
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.
Yeah, the error handling before was unnecessarily complicated, this is equivalent.
crates/client-api/src/auth.rs
Outdated
if let Ok(Query(creds)) = Query::<Self>::try_from_uri(&parts.uri) { | ||
return Ok(Some(creds)); | ||
} | ||
res.map(|()| None) |
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.
I might be missing something simple.. why do we return Ok(())
into res
, only to .map(|()| None)
here? Couldn't we just => Ok(None)
above?
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.
Yeah, I guess, but then it would be a None that would never be Some. it doesn't much matter I suppose, it could be Ok(None)
, the logic is just kinda convoluted and I was trying to preserve it while making it idiomatic.
Summary of changes, I think:
|
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.
Okay, I think this looks good to me - thank you for explaining everything!
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.
I did another review pass, and had a few more comments
crates/client-api/src/auth.rs
Outdated
@@ -260,3 +243,15 @@ impl headers::Header for SpacetimeExecutionDurationMicros { | |||
values.extend([(self.0.as_micros() as u64).into()]) | |||
} | |||
} | |||
|
|||
pub async fn auth_middleware<S: ControlStateDelegate + NodeDelegate>( |
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.
what do you think about renaming this to something more descriptive? e.g. anonymous_auth_middleware
?
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.
Not fully opposed, but I feel that this is descriptive enough as is - it's a feature of our auth system that you don't actually need to be auth'd, that we'll generate an identity for you. So this is the full auth middleware; it does do standard authentication (verify you are who you say you are), not just anonymous auth.
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.
The only places we don't generate it for you is where it doesn't make any sense to - but there's no urgent reason to disallow e.g. set_email on an anonymous identity, just that it doesn't make much sense and is likely a mistake.
9978a54
to
8a3cd56
Compare
Our reviewers are a bit bottlenecked, and I don't think we should necessarily trust me as solo-reviewer yet 😅 - I moved just the |
8a3cd56
to
4dcb97e
Compare
4dcb97e
to
bd11314
Compare
@@ -199,39 +197,39 @@ pub struct ValidateTokenParams { | |||
|
|||
pub async fn validate_token( | |||
Path(ValidateTokenParams { identity }): Path<ValidateTokenParams>, | |||
auth: SpacetimeAuthHeader, | |||
SpacetimeAuthRequired(auth): SpacetimeAuthRequired, |
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.
nit: this one ends up doing a auth.identity != identity
check anyway, so would anonymous auth be fine like the others?
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.
I think given we're validating the token - it makes sense to require a token to validate against. If you don't pass a token to validate_token it was probably a mistake.
timer += Duration::from_secs(seconds); | ||
// SAFETY: duration_since will panic if an argument is later than the time | ||
// used for the duration calculation. In case of UNIX_EPOCH it can't be the case | ||
timer.duration_since(UNIX_EPOCH).unwrap().as_secs() |
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.
why did this get to be removed?
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.
Tidying up auth stuff, I guess. serde_with can do this conversion for us.
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.
Ah I see the new serde_with
line. Seems reasonable.
@@ -148,8 +146,6 @@ pub async fn call<S: ControlStateDelegate + NodeDelegate>( | |||
let (status, body) = reducer_outcome_response(&identity, &reducer, result.outcome); | |||
Ok(( | |||
status, | |||
TypedHeader(SpacetimeIdentity(caller_identity)), | |||
TypedHeader(SpacetimeIdentityToken(caller_identity_token)), |
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.
to double-check: are these always added by a different layer of the stack right now? even for things that don't use the auth_middleware
?
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.
No, only for anon_auth_middleware
routes. However, the only routes that added the headers before now have anon_auth_middleware
, so it's fine.
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.
ah sorry I didn't look at where this was 😅 thank you!
// while everywhere else we return `BAD_REQUEST`. | ||
// Is this special in some way? Should this change? | ||
// Should all the others change? | ||
let auth = auth_or_unauth(auth)?; |
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.
(review note: I think this used to fail if auth wasn't present, but it's fine because we check for a specific identity below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do check. What's happening here is that instead of returning an error if you are unauthenticated (not unauthorized), we will create an anon authenticated user for you (which will presumably not be authorized).
) -> axum::response::Result<impl IntoResponse> { | ||
let auth = auth_or_unauth(auth)?; |
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.
(review note: I think this used to fail if auth wasn't present, but it's fine because we check for a specific identity below)
) -> axum::response::Result<impl IntoResponse> { | ||
let auth = auth_or_unauth(auth)?; |
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.
(review note: I think this used to fail if auth wasn't present, but it's fine because delete_database
checks the identity)
@@ -798,7 +780,6 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>( | |||
|
|||
// You should not be able to publish to a database that you do not own | |||
// so, unless you are the owner, this will fail. | |||
let auth = auth_or_unauth(auth)?; |
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.
(review note: create_dns_record
and/or delete_database
and/or publish_database
will fail if you're not the right identity anyway)
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.
On a high level this looks good to me. Your review comments are very helpful @bfops
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.
PR is already approved, but adding my own approval notes - I will vouch for the fact that all the individual route changes (e.g. which ones are now SpacetimeAuthRequired
etc.) are faithful translations of the previous logic.
I left review notes where the logic changed a bit, but is effectively the same.
I did not have the capacity to verify the small refactorings, because I'm unfamiliar with the underlying libs.
Description of Changes
Now, in most cases, it'll create an identity if the user doesn't pass one.
Testing