Skip to content

feat!(auth): Consider ordering of scopes for auth#429

Open
lcian wants to merge 11 commits intomainfrom
lcian/jwt-scope-vecs
Open

feat!(auth): Consider ordering of scopes for auth#429
lcian wants to merge 11 commits intomainfrom
lcian/jwt-scope-vecs

Conversation

@lcian
Copy link
Copy Markdown
Member

@lcian lcian commented Apr 13, 2026

This changes the format for the scopes field in our JWTs to be a Vec, and AuthContext to take ordering into account when validating auth.
Previously, scopes was a map, which meant that order didn't matter.

I think this is the way this was intended to work in the first place.
From #199:

  • an AuthContext with a scope of org.123/proj.456 covers objects whose scope is org.123/proj.456
  • an AuthContext with a scope of just org.123 covers all objects whose scope starts with org.123 (i.e., objects from any project in that org)

This is a breaking change as the serialization format for our JWTs changes.
We will need to update our client libraries in all services.
Luckily we're still not enforcing auth, so we can easily make the breaking change now.

@lcian lcian requested a review from a team as a code owner April 13, 2026 16:10
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 13, 2026

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat!(auth): Consider ordering of scopes for auth by lcian in #429

🤖 This preview updates automatically when you update the PR.

@lcian lcian changed the title Store JWT scopes as ordered vectors feat!(auth): Consider order in JWT Scope Apr 13, 2026
@lcian lcian changed the title feat!(auth): Consider order in JWT Scope feat!(auth): Consider order in JWT scopes Apr 13, 2026
Comment thread objectstore-server/src/auth/context.rs Outdated
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d42adaa. Configure here.

Comment thread objectstore-server/src/auth/context.rs
@lcian lcian changed the title feat!(auth): Consider order in JWT scopes feat!(auth): Consider ordering of scopes for auth Apr 13, 2026
@lcian lcian marked this pull request as draft April 13, 2026 16:26
Comment on lines +439 to +443
// Allowed:
// auth_context: (empty vec)
// object: org.123
#[test]
fn test_assert_authorized_empty_scope_allows_any_request() -> Result<(), AuthError> {
Copy link
Copy Markdown
Member Author

@lcian lcian Apr 13, 2026

Choose a reason for hiding this comment

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

This is weird, but consistent with what we want semantically.
It could be used to grant access to the whole usecase.

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.

currently you can do this with a token for org.*/proj.*

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In theory we don't enforce the scope to be of the form org.*/proj.*.

@lcian lcian marked this pull request as ready for review April 13, 2026 17:50
Copy link
Copy Markdown
Contributor

@matt-codecov matt-codecov left a comment

Choose a reason for hiding this comment

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

iirc i actually made scopes unordered on purpose in an attempt to preserve the service-agnostic design of the token. it is less awkward for other services to ignore objectstore-specific scopes if the token doesn't encode those objectstore scopes as the root of a hierarchy

scope order/hierarchy is now up to the application to interpret. for objectstore, the interpretation is:

  1. the os:usecase scope is required and its value must match the first part of an object's scope
  2. every subsequent part of the object's scope must be present in the token's scopes
  3. any token scope that isn't present in the object's scope is ignored
  4. wildcards must be explicit; org.123 does not imply org.123/proj.*

i guess i never updated the PR description or notion doc (whoops! sorry) but the tests show the expected behavior:

// Allowed:
// auth_context: org.123 / proj.123
// object: org.123 / proj.123
#[test]
fn test_assert_authorized_exact_scope_allowed() -> Result<(), AuthError> {
let auth_context = sample_auth_context("123", "456", max_permission());
let object = sample_object_context("123", "456");
auth_context.assert_authorized(Permission::ObjectRead, &object)?;
Ok(())
}
// Allowed:
// auth_context: org.123 / proj.*
// object: org.123 / proj.123
#[test]
fn test_assert_authorized_wildcard_project_allowed() -> Result<(), AuthError> {
let auth_context = sample_auth_context("123", "*", max_permission());
let object = sample_object_context("123", "456");
auth_context.assert_authorized(Permission::ObjectRead, &object)?;
Ok(())
}
// Allowed:
// auth_context: org.123 / proj.456
// object: org.123
#[test]
fn test_assert_authorized_org_only_path_allowed() -> Result<(), AuthError> {
let auth_context = sample_auth_context("123", "456", max_permission());
let object = ObjectContext {
usecase: "attachments".into(),
scopes: Scopes::from_iter([Scope::create("org", "123").unwrap()]),
};
auth_context.assert_authorized(Permission::ObjectRead, &object)?;
Ok(())
}
// Not allowed:
// auth_context: org.123 / proj.456
// object: org.123 / proj.999
//
// auth_context: org.123 / proj.456
// object: org.999 / proj.456
#[test]
fn test_assert_authorized_scope_mismatch_fails() -> Result<(), AuthError> {
let auth_context = sample_auth_context("123", "456", max_permission());
let object = sample_object_context("123", "999");
let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
assert_eq!(result, Err(AuthError::NotPermitted));
let auth_context = sample_auth_context("123", "456", max_permission());
let object = sample_object_context("999", "456");
let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
assert_eq!(result, Err(AuthError::NotPermitted));
Ok(())
}

#\3 above is a bit awkward. consider this possibly counterintuitive example:

// Allowed:
//   auth_context: org.123 / proj.456
//         object: org.123

because there is no project component in the object scope, any project component in the token scope is ignored. it behaves this way for two reasons:

  • this is how objectstore can ignore scopes specific to other services
  • something like debug symbols may be uploaded at org scope yet still be accessible to multiple projects that need to read them to symbolicate errors

that said, i admit it is weird.

all this said: do you still want to move forward with this change? i'm not necessarily opposed. sentry does need a real service-agnostic auth scheme, but if i pick that effort up again it will be on another team.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants