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

turned handling proxy grpc into auth strategy #205

Open
wants to merge 73 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
49f044f
wip - passing userauthcontext instead of http headers
Feb 29, 2024
5fece60
merged main
Mar 1, 2024
946c9ea
revert comment out
shopifyski Mar 1, 2024
ecf2389
fixed temp change
shopifyski Mar 1, 2024
9521d72
Merge branch 'main' into jw/changing-how-jwt-is-passed-around
shopifyski Mar 1, 2024
83e91ff
fixed failing tests
shopifyski Mar 1, 2024
b8bb476
next iteration of unit test fixing
shopifyski Mar 4, 2024
e322b1a
fixed remaining unit tests
shopifyski Mar 4, 2024
6b517ea
reverted debug
shopifyski Mar 4, 2024
e3bd67c
removed accidentally added file
shopifyski Mar 4, 2024
8f6b127
Merge branch 'main' into jw/changing-how-jwt-is-passed-around
shopifyski Mar 4, 2024
f038f39
moved str to userauthcontext conversion to from trait
shopifyski Mar 5, 2024
9bc376b
remove vague comment
shopifyski Mar 5, 2024
37c130f
cargo fmt
shopifyski Mar 5, 2024
b350ce5
cleaned up mod reimportss
shopifyski Mar 5, 2024
d3ccabc
refactored cryptic matching in replica_proxy
shopifyski Mar 6, 2024
9b353cd
marked potentially duplicate code with // todo dupe #auth
shopifyski Mar 6, 2024
e30d5a3
refactored context to custome errors
shopifyski Mar 6, 2024
cadf427
added a factory to produce empty UserAuthContext
shopifyski Mar 6, 2024
5077466
added constructors for UserAuthContext
shopifyski Mar 6, 2024
071dcf3
switched from try_into to using constructors
shopifyski Mar 6, 2024
9f94e4e
cargo fmt
shopifyski Mar 6, 2024
284b36f
added tests for failing cases in parsers
shopifyski Mar 7, 2024
d0ec05a
adding mamespace as param wip
shopifyski Mar 8, 2024
f716a28
cargo fmt
shopifyski Mar 8, 2024
843c9f7
added test for non-asci error
shopifyski Mar 8, 2024
5c79f33
Merge branch 'main' into jw/changing-how-jwt-is-passed-around
shopifyski Mar 8, 2024
1340d87
Merge branch 'jw/changing-how-jwt-is-passed-around' into jw/namespace…
shopifyski Mar 8, 2024
047823b
incremental changes to make namespace as param work
shopifyski Mar 8, 2024
d32bd5f
fixed failing test
shopifyski Mar 11, 2024
12df9f9
Merge branch 'jw/changing-how-jwt-is-passed-around' into jw/namespace…
shopifyski Mar 11, 2024
4125383
fixed log message
shopifyski Mar 13, 2024
37dc162
merged main
shopifyski Mar 13, 2024
3105025
removed unnecessary error mapping
shopifyski Mar 14, 2024
bccfe77
turned context to result
shopifyski Mar 14, 2024
37dc3af
removing dummy tokens from tests
shopifyski Mar 14, 2024
57861f6
cargo fmt + cleanup
shopifyski Mar 14, 2024
91bd024
Merge branch 'jw/changing-how-jwt-is-passed-around' into jw/namespace…
shopifyski Mar 14, 2024
91cc1fd
Merge branch 'main' into jw/namespace-passing-and-auth
shopifyski Mar 18, 2024
9df60da
namespace passing exammple
shopifyski Mar 18, 2024
9f97d3e
added namespace config for the example
shopifyski Mar 22, 2024
ce73236
Merge branch 'main' into jw/namespace-passing-and-auth
shopifyski Mar 22, 2024
d0a5646
remove unnecessary dummy token
shopifyski Mar 22, 2024
b9f10a7
reverting accidental commit
shopifyski Mar 22, 2024
1cb0ecd
wip
shopifyski Mar 27, 2024
3e7999a
wip
shopifyski Mar 27, 2024
844dd54
custom fields in auth context
shopifyski Mar 28, 2024
6afb112
fixed compilation errors, wip
shopifyski Mar 28, 2024
7f8b0b4
refactored auth api with on demand headers
shopifyski Mar 29, 2024
6aba6e5
removing optionality of UserAuthContext
shopifyski Mar 29, 2024
1f2de7e
clean up dead code
shopifyski Mar 29, 2024
fc582ec
wip
shopifyski Mar 29, 2024
4f49949
turned handling proxy grpc into auth strategy
shopifyski Mar 29, 2024
ba204fd
bottomless: emit restored snapshot for waiters (#1252)
LucioFranco Mar 22, 2024
7226bf9
fix conn upgrade lock (#1244)
MarinPostma Mar 22, 2024
14b05cd
prevent primary to remove itself in case of load error (#1253)
MarinPostma Mar 23, 2024
c2ca36c
server: fix interactive txn schema panic (#1250)
LucioFranco Mar 25, 2024
ade4fb7
libsql-ffi: Fix sqlite3mc build output directory (#1234)
penberg Mar 25, 2024
7e02941
server: add shutdown timeout (#1258)
LucioFranco Mar 25, 2024
56b1221
server: release v0.24.4 (#1259)
LucioFranco Mar 25, 2024
b23231c
server: allow explain queries without bind parameters (#1256)
Mar 25, 2024
c731a3a
add libsql-hrana crate (#1260)
LucioFranco Mar 25, 2024
e81ab33
add windows ci (#1263)
LucioFranco Mar 26, 2024
70d4324
libsql: prepare v0.3.1 release (#1264)
LucioFranco Mar 26, 2024
6cd1e00
libsql-hrana: add description (#1265)
LucioFranco Mar 26, 2024
0730b4f
deduplicated handling of hrana hello and repreated hello. Code is ful…
shopifyski Mar 27, 2024
90aee49
fixed early return
shopifyski Mar 27, 2024
57052b5
lazy unwrapping
shopifyski Mar 27, 2024
8a1b675
made session fields private again
shopifyski Mar 28, 2024
dabcfd1
fmt
shopifyski Mar 28, 2024
9938645
cleaned up nesting in conn
shopifyski Mar 28, 2024
632a640
fmt
shopifyski Mar 29, 2024
725ae17
Merge branch 'main' into jw/proxy-grpc-auth
shopifyski Mar 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions libsql-server/src/auth/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub enum AuthError {
AuthStringMalformed,
#[error("Expected authorization header but none given")]
AuthHeaderNotFound,
#[error("Expected authorization proxy header but none given")]
AuthProxyHeaderNotFound,
#[error("Failed to parse auth proxy header")]
AuthProxyHeaderInvalid,
#[error("Non-ASCII auth header")]
AuthHeaderNonAscii,
#[error("Authentication failed")]
Expand All @@ -47,6 +51,8 @@ impl AuthError {
Self::JwtImmature => "AUTH_JWT_IMMATURE",
Self::AuthStringMalformed => "AUTH_HEADER_MALFORMED",
Self::AuthHeaderNotFound => "AUTH_HEADER_NOT_FOUND",
Self::AuthProxyHeaderNotFound => "AUTH_PROXY_HEADER_NOT_FOUND",
Self::AuthProxyHeaderInvalid => "AUTH_PROXY_HEADER_INVALID",
Self::AuthHeaderNonAscii => "AUTH_HEADER_MALFORMED",
Self::Other => "AUTH_FAILED",
}
Expand Down
17 changes: 8 additions & 9 deletions libsql-server/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,23 @@ pub use authorized::Authorized;
pub use errors::AuthError;
pub use parsers::{parse_http_auth_header, parse_http_basic_auth_arg, parse_jwt_key};
pub use permission::Permission;
pub use user_auth_strategies::{Disabled, HttpBasic, Jwt, UserAuthContext, UserAuthStrategy};
pub use user_auth_strategies::{
Disabled, HttpBasic, Jwt, ProxyGrpc, UserAuthContext, UserAuthStrategy,
};

#[derive(Clone)]
pub struct Auth {
pub user_strategy: Arc<dyn UserAuthStrategy + Send + Sync>,
pub strategy: Arc<dyn UserAuthStrategy + Send + Sync>,
}

impl Auth {
pub fn new(user_strategy: impl UserAuthStrategy + Send + Sync + 'static) -> Self {
pub fn new(strategy: impl UserAuthStrategy + Send + Sync + 'static) -> Self {
Self {
user_strategy: Arc::new(user_strategy),
strategy: Arc::new(strategy),
}
}

pub fn authenticate(
&self,
context: Result<UserAuthContext, AuthError>,
) -> Result<Authenticated, AuthError> {
self.user_strategy.authenticate(context)
pub fn authenticate(&self, context: UserAuthContext) -> Result<Authenticated, AuthError> {
self.strategy.authenticate(context)
}
}
68 changes: 31 additions & 37 deletions libsql-server/src/auth/parsers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::auth::{constants::GRPC_AUTH_HEADER, AuthError};
use crate::auth::AuthError;

use anyhow::{bail, Context as _, Result};
use axum::http::HeaderValue;
Expand Down Expand Up @@ -36,12 +36,20 @@ pub fn parse_jwt_key(data: &str) -> Result<jsonwebtoken::DecodingKey> {
}
}

pub(crate) fn parse_grpc_auth_header(metadata: &MetadataMap) -> Result<UserAuthContext, AuthError> {
metadata
.get(GRPC_AUTH_HEADER)
.ok_or(AuthError::AuthHeaderNotFound)
.and_then(|h| h.to_str().map_err(|_| AuthError::AuthHeaderNonAscii))
.and_then(|t| UserAuthContext::from_auth_str(t))
pub(crate) fn parse_grpc_auth_header(
metadata: &MetadataMap,
required_fields: &Vec<String>,
) -> UserAuthContext {
let mut context = UserAuthContext::empty();
for field in required_fields.iter() {
metadata
.get(field)
.map(|header| header.to_str().ok())
.and_then(|r| r)
.map(|v| context.add_field(field.into(), v.into()));
}

context
}

pub fn parse_http_auth_header<'a>(
Expand Down Expand Up @@ -79,40 +87,26 @@ mod tests {
#[test]
fn parse_grpc_auth_header_returns_valid_context() {
let mut map = tonic::metadata::MetadataMap::new();
map.insert("x-authorization", "bearer 123".parse().unwrap());
let context = parse_grpc_auth_header(&map).unwrap();
assert_eq!(context.scheme().as_ref().unwrap(), "bearer");
assert_eq!(context.token().as_ref().unwrap(), "123");
}

#[test]
fn parse_grpc_auth_header_error_no_header() {
let map = tonic::metadata::MetadataMap::new();
let result = parse_grpc_auth_header(&map);
map.insert(
crate::auth::constants::GRPC_AUTH_HEADER,
"bearer 123".parse().unwrap(),
);
let required_fields = vec!["x-authorization".into()];
let context = parse_grpc_auth_header(&map, &required_fields);
assert_eq!(
result.unwrap_err().to_string(),
"Expected authorization header but none given"
context.custom_fields.get("x-authorization"),
Some(&"bearer 123".to_string())
);
}

#[test]
fn parse_grpc_auth_header_error_non_ascii() {
let mut map = tonic::metadata::MetadataMap::new();
map.insert("x-authorization", "bearer I❤NY".parse().unwrap());
let result = parse_grpc_auth_header(&map);
assert_eq!(result.unwrap_err().to_string(), "Non-ASCII auth header")
}

#[test]
fn parse_grpc_auth_header_error_malformed_auth_str() {
let mut map = tonic::metadata::MetadataMap::new();
map.insert("x-authorization", "bearer123".parse().unwrap());
let result = parse_grpc_auth_header(&map);
assert_eq!(
result.unwrap_err().to_string(),
"Auth string does not conform to '<scheme> <token>' form"
)
}
// #[test] TODO rewrite
// fn parse_grpc_auth_header_error_non_ascii() {
// let mut map = tonic::metadata::MetadataMap::new();
// map.insert("x-authorization", "bearer I❤NY".parse().unwrap());
// let required_fields = Vec::new();
// let result = parse_grpc_auth_header(&map, &required_fields);
// assert_eq!(result.unwrap_err().to_string(), "Non-ASCII auth header")
// }

#[test]
fn parse_http_auth_header_returns_auth_header_param_when_valid() {
Expand Down
7 changes: 2 additions & 5 deletions libsql-server/src/auth/user_auth_strategies/disabled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ use crate::auth::{AuthError, Authenticated};
pub struct Disabled {}

impl UserAuthStrategy for Disabled {
fn authenticate(
&self,
_context: Result<UserAuthContext, AuthError>,
) -> Result<Authenticated, AuthError> {
fn authenticate(&self, _context: UserAuthContext) -> Result<Authenticated, AuthError> {
tracing::trace!("executing disabled auth");
Ok(Authenticated::FullAccess)
}
Expand All @@ -26,7 +23,7 @@ mod tests {
#[test]
fn authenticates() {
let strategy = Disabled::new();
let context = Ok(UserAuthContext::empty());
let context = UserAuthContext::empty();

assert!(matches!(
strategy.authenticate(context).unwrap(),
Expand Down
30 changes: 17 additions & 13 deletions libsql-server/src/auth/user_auth_strategies/http_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,31 @@ pub struct HttpBasic {
}

impl UserAuthStrategy for HttpBasic {
fn authenticate(
&self,
context: Result<UserAuthContext, AuthError>,
) -> Result<Authenticated, AuthError> {
fn authenticate(&self, ctx: UserAuthContext) -> Result<Authenticated, AuthError> {
tracing::trace!("executing http basic auth");
let auth_str = None
.or_else(|| ctx.custom_fields.get("authorization"))
.or_else(|| ctx.custom_fields.get("x-authorization"));

let (_, token) = auth_str
.ok_or(AuthError::AuthHeaderNotFound)
.map(|s| s.split_once(' ').ok_or(AuthError::AuthStringMalformed))
.and_then(|o| o)?;

// NOTE: this naive comparison may leak information about the `expected_value`
// using a timing attack
let expected_value = self.credential.trim_end_matches('=');

let creds_match = match context?.token {
Some(s) => s.contains(expected_value),
None => expected_value.is_empty(),
};

let creds_match = token.contains(expected_value);
if creds_match {
return Ok(Authenticated::FullAccess);
}

Err(AuthError::BasicRejected)
}

fn required_fields(&self) -> Vec<String> {
vec!["authorization".to_string(), "x-authorization".to_string()]
}
}

impl HttpBasic {
Expand All @@ -48,7 +52,7 @@ mod tests {

#[test]
fn authenticates_with_valid_credential() {
let context = Ok(UserAuthContext::basic(CREDENTIAL));
let context = UserAuthContext::basic(CREDENTIAL);

assert!(matches!(
strategy().authenticate(context).unwrap(),
Expand All @@ -59,7 +63,7 @@ mod tests {
#[test]
fn authenticates_with_valid_trimmed_credential() {
let credential = CREDENTIAL.trim_end_matches('=');
let context = Ok(UserAuthContext::basic(credential));
let context = UserAuthContext::basic(credential);

assert!(matches!(
strategy().authenticate(context).unwrap(),
Expand All @@ -69,7 +73,7 @@ mod tests {

#[test]
fn errors_when_credentials_do_not_match() {
let context = Ok(UserAuthContext::basic("abc"));
let context = UserAuthContext::basic("abc");

assert_eq!(
strategy().authenticate(context).unwrap_err(),
Expand Down
36 changes: 17 additions & 19 deletions libsql-server/src/auth/user_auth_strategies/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,27 @@ pub struct Jwt {
}

impl UserAuthStrategy for Jwt {
fn authenticate(
&self,
context: Result<UserAuthContext, AuthError>,
) -> Result<Authenticated, AuthError> {
fn authenticate(&self, ctx: UserAuthContext) -> Result<Authenticated, AuthError> {
tracing::trace!("executing jwt auth");
let auth_str = None
.or_else(|| ctx.custom_fields.get("authorization"))
.or_else(|| ctx.custom_fields.get("x-authorization"))
.ok_or_else(|| AuthError::AuthHeaderNotFound)?;

let ctx = context?;

let UserAuthContext {
scheme: Some(scheme),
token: Some(token),
} = ctx
else {
return Err(AuthError::HttpAuthHeaderInvalid);
};
let (scheme, token) = auth_str
.split_once(' ')
.ok_or(AuthError::AuthStringMalformed)?;

if !scheme.eq_ignore_ascii_case("bearer") {
return Err(AuthError::HttpAuthHeaderUnsupportedScheme);
}

return validate_jwt(&self.key, &token);
}

fn required_fields(&self) -> Vec<String> {
vec!["authentication".to_string()]
}
}

impl Jwt {
Expand Down Expand Up @@ -155,7 +154,7 @@ mod tests {
};
let token = encode(&token, &enc);

let context = Ok(UserAuthContext::bearer(token.as_str()));
let context = UserAuthContext::bearer(token.as_str());

assert!(matches!(
strategy(dec).authenticate(context).unwrap(),
Expand All @@ -177,8 +176,7 @@ mod tests {
};
let token = encode(&token, &enc);

let context = Ok(UserAuthContext::bearer(token.as_str()));

let context = UserAuthContext::bearer(token.as_str());
let Authenticated::Legacy(a) = strategy(dec).authenticate(context).unwrap() else {
panic!()
};
Expand All @@ -190,7 +188,7 @@ mod tests {
#[test]
fn errors_when_jwt_token_invalid() {
let (_enc, dec) = key_pair();
let context = Ok(UserAuthContext::bearer("abc"));
let context = UserAuthContext::bearer("abc");

assert_eq!(
strategy(dec).authenticate(context).unwrap_err(),
Expand All @@ -210,7 +208,7 @@ mod tests {

let token = encode(&token, &enc);

let context = Ok(UserAuthContext::bearer(token.as_str()));
let context = UserAuthContext::bearer(token.as_str());

assert_eq!(
strategy(dec).authenticate(context).unwrap_err(),
Expand All @@ -232,7 +230,7 @@ mod tests {

let token = encode(&token, &enc);

let context = Ok(UserAuthContext::bearer(token.as_str()));
let context = UserAuthContext::bearer(token.as_str());

let Authenticated::Authorized(a) = strategy(dec).authenticate(context).unwrap() else {
panic!()
Expand Down
Loading