Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
acb69e5
Create modelardb_auth crate
CGodiksen May 17, 2026
ff4c06b
Add Permission enum
CGodiksen May 17, 2026
7481a71
Define the Authenticator trait
CGodiksen May 17, 2026
fe13ad8
Implement NoAuth for Authenticator
CGodiksen May 17, 2026
79cb752
Move Authenticator to separate file for easier merging with closed-so…
CGodiksen May 17, 2026
239674a
Move NoAuth implementation to separate file for consistency
CGodiksen May 17, 2026
acce865
Fix closing bracket bug
CGodiksen May 17, 2026
e48b771
Move remote.rs into a separate module folder
CGodiksen May 17, 2026
b6c072c
Add http-body-util dependency
CGodiksen May 17, 2026
94ff465
Fixed dependency issue
CGodiksen May 26, 2026
38d264a
Add tower auth middleware layer
CGodiksen May 26, 2026
b8d34b1
Make the handling of cloning inner services slightly more clear
CGodiksen May 26, 2026
42459c6
Remove axum dependency
CGodiksen May 26, 2026
a9a7668
Use tonic body instead of axum explicitly
CGodiksen May 26, 2026
9e38f44
Add auth layer to Apache Arrow Flight server
CGodiksen May 26, 2026
ef7f24f
Use matches! macro to fix clippy issue
CGodiksen May 26, 2026
aa50974
Add simple unit tests for Permission and NoAuth
CGodiksen May 26, 2026
e027f0e
Add mock authenticator
CGodiksen May 27, 2026
180b69e
Add helper methods for auth layer tests
CGodiksen May 27, 2026
8e5ddb8
Add more specific warn messages for cluster key handling
CGodiksen May 27, 2026
67208bc
Use actual error messages instead of only unauthenticated("Unauthoriz…
CGodiksen May 27, 2026
3f321bc
Removed cluster_key_in_request()
CGodiksen May 27, 2026
320aa76
Only pass cluster key instead of entire configuration manager to auth…
CGodiksen May 27, 2026
f77b9fe
Add tests for cluster key validation in auth layer
CGodiksen May 28, 2026
37432c1
Add test to ensure list actions bypasses authenticator
CGodiksen May 28, 2026
6ac6c3e
Add test for authorizing an unknown path
CGodiksen May 28, 2026
7a6aaf8
Add tests for permission mapping
CGodiksen May 28, 2026
e4db166
Add test util method to create do get request
CGodiksen May 28, 2026
6ac3e8d
Add permission mapping tests for do_get
CGodiksen May 28, 2026
0d9f565
Add test to ensure that do get reconstructs the body correctly
CGodiksen May 28, 2026
b61afb9
Add test to ensure we handle do get request with too short body
CGodiksen May 28, 2026
0d5469f
Add test to ensure we handle do get request with invalid protobuf mes…
CGodiksen May 28, 2026
bcbfd7b
Add test to ensure authorize fails when parsing invalid sql
CGodiksen May 28, 2026
9153893
Add test to ensure authorize fails when parsing non utf8 tickets
CGodiksen May 28, 2026
15ce340
Remove duplicated test code with test util functions
CGodiksen May 28, 2026
e090e74
Fix clippy warning for missing default
CGodiksen May 28, 2026
8c8e5e2
Remove unused dependency
CGodiksen May 29, 2026
f70d87c
Add BearerInterceptor for attaching bearer tokens to outgoing requests
CGodiksen Jun 1, 2026
99f7c31
Use BearerInterceptor when connecting to embedded Client
CGodiksen Jun 1, 2026
20f471d
Make it possible to still initialize lazy client
CGodiksen Jun 1, 2026
1df0d02
Add maybe_token_ptr parameter to C-API
CGodiksen Jun 1, 2026
aa5b6f2
Update C header to match C-API change
CGodiksen Jun 1, 2026
513f26d
Add optional token parameter to Python API
CGodiksen Jun 1, 2026
c8276c7
Move BearerInterceptor to modelardb_auth
CGodiksen Jun 1, 2026
b0adbbb
Add token to client doc
CGodiksen Jun 1, 2026
1e68daf
Add very simple arg parsing for --token parameter
CGodiksen Jun 1, 2026
4549261
Connect with optional bearer token in client
CGodiksen Jun 1, 2026
46544be
Move parsing of token into AsciiMetadataValue to BearerInterceptor::t…
CGodiksen Jun 1, 2026
e82e3fd
Pass authorization header down for INCLUDE requests
CGodiksen Jun 22, 2026
5e842b4
Make Authenticator::authorize() async
CGodiksen Jun 22, 2026
9f61378
Add tests for BearerInterceptor
CGodiksen Jun 22, 2026
e0911da
Formatted with Rustfmt
CGodiksen Jun 22, 2026
1879511
Merge branch 'main' into dev/security-manager
CGodiksen Jun 23, 2026
2708cac
Add token to clap parsing in modelardb_client
CGodiksen Jun 23, 2026
cab26de
Merge branch 'main' into dev/security-manager
CGodiksen Jun 28, 2026
a7ae644
Update Authenticator based on comments from @skejserjensen
CGodiksen Jul 2, 2026
7cd5abf
Use AsciiMetadataValue instead of MetadataValue<Ascii>
CGodiksen Jul 2, 2026
a112d8c
Make calling LIST_ACTIONS require admin permission
CGodiksen Jul 2, 2026
b011abc
Use None instead of NoAuth
CGodiksen Jul 2, 2026
76cdc54
Remove NoAuth
CGodiksen Jul 2, 2026
c90fbfe
Merge branch 'main' into dev/security-manager
CGodiksen Jul 3, 2026
cb3705c
Add support for optimize in auth layer
CGodiksen Jul 3, 2026
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
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ delta_kernel = { package = "buoyant_kernel", version = "0.22.2" }
deltalake = "0.32.4"
dirs = "6.0.0"
futures = "0.3.32"
http = "1.4.0"
http-body-util = "0.1.3"
log = "0.4.29"
object_store = "0.13.2"
proptest = "1.11.0"
Expand All @@ -50,6 +52,7 @@ tokio = "1.52.2"
tokio-stream = "0.1.18"
toml = "1.1.2"
tonic = "0.14.4"
tower = "0.5.3"
tracing = "0.1.44"
tracing-subscriber = "0.3.23"
url = "2.5.8"
Expand Down
27 changes: 27 additions & 0 deletions crates/modelardb_auth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 The ModelarDB Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[package]
name = "modelardb_auth"
version = "0.1.0"
license = "Apache-2.0"
edition = "2024"
authors = ["Soeren Kejser Jensen <devel@kejserjensen.dk>"]

[dependencies]
async-trait.workspace = true
tonic.workspace = true

[features]
testing = []
Comment thread
CGodiksen marked this conversation as resolved.
69 changes: 69 additions & 0 deletions crates/modelardb_auth/src/authenticator/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* Copyright 2026 The ModelarDB Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//! A mock [`Authenticator`] for use in tests. Always grants access and records every permission
Comment thread
skejserjensen marked this conversation as resolved.
//! that was requested so tests can assert which permission the auth layer required for a given
//! request.

use std::sync::Mutex;

use async_trait::async_trait;
use tonic::Status;
use tonic::metadata::MetadataMap;

use crate::Permission;
use crate::authenticator::Authenticator;

/// An [`Authenticator`] for use in tests that always grants access and records every
/// [`Permission`] passed to [`authorize`](Authenticator::authorize).
pub struct MockAuthenticator {
/// Every [`Permission`] passed to [`authorize`](Authenticator::authorize). A [`Mutex`] is used
/// because [`authorize`](Authenticator::authorize) records through `&self`, which requires
/// interior mutability.
permissions: Mutex<Vec<Permission>>,
}

impl MockAuthenticator {
pub fn new() -> Self {
Self {
permissions: Mutex::new(vec![]),
}
}

/// Return a copy of all permissions passed to [`authorize`](Authenticator::authorize)
/// in the order they were received.
pub fn permissions(&self) -> Vec<Permission> {
self.permissions.lock().unwrap().clone()
}
}

impl Default for MockAuthenticator {
// Trait implemented to silence clippy warning.
fn default() -> Self {
Self::new()
Comment thread
skejserjensen marked this conversation as resolved.
}
}

#[async_trait]
impl Authenticator for MockAuthenticator {
async fn authorize(
&self,
_metadata: &MetadataMap,
required_permission: Permission,
) -> Result<(), Status> {
self.permissions.lock().unwrap().push(required_permission);
Ok(())
}
}
44 changes: 44 additions & 0 deletions crates/modelardb_auth/src/authenticator/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* Copyright 2026 The ModelarDB Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//! Defines the [`Authenticator`] trait for validating credentials and authorizing access to
//! ModelarDB.

#[cfg(feature = "testing")]
pub mod mock;

use async_trait::async_trait;
use tonic::Status;
use tonic::metadata::MetadataMap;

use crate::Permission;

/// Validates the credentials in `metadata` and checks that the caller has the `required_permission`.
/// Implementations must return:
///
/// - `Ok(())` if authorized.
/// - `Err(Status::unauthenticated(...))` if credentials are missing or invalid.
/// - `Err(Status::permission_denied(...))` if credentials are valid but the caller lacks the
/// required permission.
///
/// Error messages must not reveal the specific reason for rejection to prevent information leakage.
#[async_trait]
pub trait Authenticator: Send + Sync {
async fn authorize(
&self,
metadata: &MetadataMap,
required_permission: Permission,
) -> Result<(), Status>;
}
171 changes: 171 additions & 0 deletions crates/modelardb_auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/* Copyright 2026 The ModelarDB Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//! Types to support authentication and authorization in ModelarDB.

use tonic::metadata::AsciiMetadataValue;
use tonic::service::Interceptor;
use tonic::{Request, Status};

pub mod authenticator;

/// The permission required to perform an operation in a ModelarDB server.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Permission {
Read,
Write,
Admin,
}

impl Permission {
/// Return [`true`] if `granted` satisfies this required permission using the hierarchy
/// Admin ⊇ Write ⊇ Read, otherwise [`false`] is returned.
#[allow(clippy::match_like_matches_macro)]
pub fn is_satisfied_by(&self, granted: &Permission) -> bool {
match (self, granted) {
(Permission::Read, _) => true,
(Permission::Write, Permission::Write | Permission::Admin) => true,
(Permission::Admin, Permission::Admin) => true,
_ => false,
}
}
}

/// Tonic interceptor that attaches a bearer token to every outgoing request when one is present.
Comment thread
skejserjensen marked this conversation as resolved.
#[derive(Clone)]
pub struct BearerInterceptor {
/// The value of the `authorization` header. This is either `Bearer <token>` or [`None`] if no
/// token has been provided. The header is only attached to an outgoing request if this is not
/// [`None`].
pub maybe_authorization: Option<AsciiMetadataValue>,
}

impl BearerInterceptor {
/// Create a new [`BearerInterceptor`] that attaches the bearer token in `maybe_token` to every
Comment thread
skejserjensen marked this conversation as resolved.
/// outgoing request if `maybe_token` is not [`None`]. If `maybe_token` is [`Some`] but the
/// token is not ASCII, [`Status`] is returned.
pub fn try_new(maybe_token: Option<&str>) -> Result<Self, Status> {
let maybe_authorization = maybe_token
.map(|token| {
format!("Bearer {token}")
.parse::<AsciiMetadataValue>()
.map_err(|error| Status::invalid_argument(format!("Token is not ASCII: {error}.")))
})
.transpose()?;

Ok(Self {
maybe_authorization,
})
}
}

impl Interceptor for BearerInterceptor {
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
if let Some(authorization) = &self.maybe_authorization {
request
.metadata_mut()
.insert("authorization", authorization.clone());
}

Ok(request)
}
}

#[cfg(test)]
mod tests {
use super::*;

// Tests for Permission.
#[test]
fn test_read_satisfied_by_read() {
assert!(Permission::Read.is_satisfied_by(&Permission::Read));
}

#[test]
fn test_read_satisfied_by_write() {
assert!(Permission::Read.is_satisfied_by(&Permission::Write));
}

#[test]
fn test_read_satisfied_by_admin() {
assert!(Permission::Read.is_satisfied_by(&Permission::Admin));
}

#[test]
fn test_write_not_satisfied_by_read() {
assert!(!Permission::Write.is_satisfied_by(&Permission::Read));
}

#[test]
fn test_write_satisfied_by_write() {
assert!(Permission::Write.is_satisfied_by(&Permission::Write));
}

#[test]
fn test_write_satisfied_by_admin() {
assert!(Permission::Write.is_satisfied_by(&Permission::Admin));
}

#[test]
fn test_admin_not_satisfied_by_read() {
assert!(!Permission::Admin.is_satisfied_by(&Permission::Read));
}

#[test]
fn test_admin_not_satisfied_by_write() {
assert!(!Permission::Admin.is_satisfied_by(&Permission::Write));
}

#[test]
fn test_admin_satisfied_by_admin() {
assert!(Permission::Admin.is_satisfied_by(&Permission::Admin));
}

// Tests for BearerInterceptor.
#[test]
fn test_try_new_bearer_interceptor_with_invalid_token() {
let result = BearerInterceptor::try_new(Some("sec\nret"));

assert_eq!(
result.err().unwrap().to_string(),
"code: 'Client specified an invalid argument', \
message: \"Token is not ASCII: failed to parse metadata value.\""
);
}

#[test]
fn test_bearer_interceptor_call_attaches_authorization_header() {
let mut interceptor = BearerInterceptor::try_new(Some("secret")).unwrap();
let request = interceptor.call(Request::new(())).unwrap();

assert_eq!(
request
.metadata()
.get("authorization")
.unwrap()
.to_str()
.unwrap(),
"Bearer secret"
);
}

#[test]
fn test_bearer_interceptor_call_without_token_attaches_nothing() {
let mut interceptor = BearerInterceptor::try_new(None).unwrap();
let request = interceptor.call(Request::new(())).unwrap();

assert!(request.metadata().get("authorization").is_none());
}
}
1 change: 1 addition & 0 deletions crates/modelardb_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ arrow = { workspace = true, features = ["prettyprint"] }
bytes.workspace = true
clap.workspace = true
dirs.workspace = true
modelardb_auth = { path = "../modelardb_auth" }
rustyline.workspace = true
tokio.workspace = true
tonic.workspace = true
Loading
Loading