Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inconsistent auth/identity creation #735

Merged
merged 6 commits into from
Jun 25, 2024
Merged

Fix inconsistent auth/identity creation #735

merged 6 commits into from
Jun 25, 2024

Conversation

coolreader18
Copy link
Collaborator

@coolreader18 coolreader18 commented Jan 19, 2024

Description of Changes

Now, in most cases, it'll create an identity if the user doesn't pass one.

Testing

spacetime init my_new_project --lang rust
cd my_new_project
spacetime publish my_module

# Make sure we can call a reducer with no identity
curl -X POST http://localhost:3000/database/my_module/say_hello

Copy link
Collaborator

@bfops bfops left a 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! 🙂)

#[derive(Clone, Deserialize)]
pub struct SpacetimeCreds {
token: String,
}
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

database::control_routes(ctx.clone()).merge(database::worker_routes(ctx.clone())),
)
.nest("/identity", identity::router(ctx.clone()))
.nest("/energy", energy::router(ctx.clone()))
Copy link
Collaborator

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?

Copy link
Collaborator Author

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,
))
Copy link
Collaborator

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())
Copy link
Collaborator

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(),
))
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Jwt(JwtErrorKind),
Header(TypedHeaderRejection),
MalformedTokenQueryString,
CantDecodeAuthorizationToken,
Copy link
Collaborator

Choose a reason for hiding this comment

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

where did CantDecodeAuthorizationToken go?

Copy link
Collaborator Author

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.

let Some(creds) = SpacetimeCreds::from_request_parts(parts)? else {
return Ok(Self { auth: None });
};
let claims = creds.decode_token(state.public_key())?;
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

if let Ok(Query(creds)) = Query::<Self>::try_from_uri(&parts.uri) {
return Ok(Some(creds));
}
res.map(|()| None)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

@bfops
Copy link
Collaborator

bfops commented Jan 31, 2024

Summary of changes, I think:

  • Restructuring SpacetimeCreds datatype
  • Factoring out helper functions, especially in SpacetimeCreds + SpacetimeAuthHeader
  • Add SpacetimeAuthRequired
  • Plumb a ctx parameter through our routes
  • Add auth_middleware to create identities "on the fly" (and return them if appropriate)
  • Wrap a bunch of routes in auth_middleware

Copy link
Collaborator

@bfops bfops left a 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!

bfops
bfops previously requested changes Feb 6, 2024
Copy link
Collaborator

@bfops bfops left a 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

@@ -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>(
Copy link
Collaborator

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?

Copy link
Collaborator Author

@coolreader18 coolreader18 Feb 8, 2024

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.

Copy link
Collaborator Author

@coolreader18 coolreader18 Feb 8, 2024

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.

@bfops bfops added bugfix Fixes something that was expected to work differently breaks library compatibility This change breaks the SpacetimeDB library interface release-any To be landed in any release window labels Feb 9, 2024
@bfops
Copy link
Collaborator

bfops commented Feb 20, 2024

Our reviewers are a bit bottlenecked, and I don't think we should necessarily trust me as solo-reviewer yet 😅 - I moved just the auth.rs changes into their own refactor PR, on the hope that might make it easier to get review on everything. What do you think?

github-merge-queue bot pushed a commit that referenced this pull request May 13, 2024
* [zeke/noa-router-shuffle]: bring over auth.rs changes from #735

* [zeke/noa-router-shuffle]: review
@bfops bfops self-requested a review May 14, 2024 04:08
@coolreader18 coolreader18 requested a review from bfops May 22, 2024 17:02
@@ -199,39 +197,39 @@ pub struct ValidateTokenParams {

pub async fn validate_token(
Path(ValidateTokenParams { identity }): Path<ValidateTokenParams>,
auth: SpacetimeAuthHeader,
SpacetimeAuthRequired(auth): SpacetimeAuthRequired,
Copy link
Collaborator

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?

Copy link
Collaborator Author

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()
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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)),
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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)?;
Copy link
Collaborator

@bfops bfops May 23, 2024

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

We do 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)?;
Copy link
Collaborator

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)?;
Copy link
Collaborator

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)?;
Copy link
Collaborator

@bfops bfops May 23, 2024

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)

@coolreader18 coolreader18 requested a review from bfops May 25, 2024 01:12
@coolreader18 coolreader18 dismissed bfops’s stale review May 25, 2024 01:12

addressed comments - approve?

@coolreader18 coolreader18 enabled auto-merge May 25, 2024 01:12
@bfops bfops removed request for jdetter and cloutiertyler May 28, 2024 16:13
@cloutiertyler cloutiertyler added release-1.0 and removed release-any To be landed in any release window labels Jun 3, 2024
bfops added a commit that referenced this pull request Jun 12, 2024
bfops added a commit that referenced this pull request Jun 12, 2024
@bfops bfops self-assigned this Jun 14, 2024
Copy link
Contributor

@cloutiertyler cloutiertyler left a 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

@coolreader18 coolreader18 added this pull request to the merge queue Jun 25, 2024
Merged via the queue into master with commit 8be8fc1 Jun 25, 2024
6 of 7 checks passed
Copy link
Collaborator

@bfops bfops left a 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.

@bfops bfops deleted the noa/fix-auth branch June 25, 2024 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaks library compatibility This change breaks the SpacetimeDB library interface bugfix Fixes something that was expected to work differently release-1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants