-
Notifications
You must be signed in to change notification settings - Fork 5
Add support for authentication and authorization #405
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
Merged
Merged
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 ff4c06b
Add Permission enum
CGodiksen 7481a71
Define the Authenticator trait
CGodiksen fe13ad8
Implement NoAuth for Authenticator
CGodiksen 79cb752
Move Authenticator to separate file for easier merging with closed-so…
CGodiksen 239674a
Move NoAuth implementation to separate file for consistency
CGodiksen acce865
Fix closing bracket bug
CGodiksen e48b771
Move remote.rs into a separate module folder
CGodiksen b6c072c
Add http-body-util dependency
CGodiksen 94ff465
Fixed dependency issue
CGodiksen 38d264a
Add tower auth middleware layer
CGodiksen b8d34b1
Make the handling of cloning inner services slightly more clear
CGodiksen 42459c6
Remove axum dependency
CGodiksen a9a7668
Use tonic body instead of axum explicitly
CGodiksen 9e38f44
Add auth layer to Apache Arrow Flight server
CGodiksen ef7f24f
Use matches! macro to fix clippy issue
CGodiksen aa50974
Add simple unit tests for Permission and NoAuth
CGodiksen e027f0e
Add mock authenticator
CGodiksen 180b69e
Add helper methods for auth layer tests
CGodiksen 8e5ddb8
Add more specific warn messages for cluster key handling
CGodiksen 67208bc
Use actual error messages instead of only unauthenticated("Unauthoriz…
CGodiksen 3f321bc
Removed cluster_key_in_request()
CGodiksen 320aa76
Only pass cluster key instead of entire configuration manager to auth…
CGodiksen f77b9fe
Add tests for cluster key validation in auth layer
CGodiksen 37432c1
Add test to ensure list actions bypasses authenticator
CGodiksen 6ac6c3e
Add test for authorizing an unknown path
CGodiksen 7a6aaf8
Add tests for permission mapping
CGodiksen e4db166
Add test util method to create do get request
CGodiksen 6ac3e8d
Add permission mapping tests for do_get
CGodiksen 0d9f565
Add test to ensure that do get reconstructs the body correctly
CGodiksen b61afb9
Add test to ensure we handle do get request with too short body
CGodiksen 0d5469f
Add test to ensure we handle do get request with invalid protobuf mes…
CGodiksen bcbfd7b
Add test to ensure authorize fails when parsing invalid sql
CGodiksen 9153893
Add test to ensure authorize fails when parsing non utf8 tickets
CGodiksen 15ce340
Remove duplicated test code with test util functions
CGodiksen e090e74
Fix clippy warning for missing default
CGodiksen 8c8e5e2
Remove unused dependency
CGodiksen f70d87c
Add BearerInterceptor for attaching bearer tokens to outgoing requests
CGodiksen 99f7c31
Use BearerInterceptor when connecting to embedded Client
CGodiksen 20f471d
Make it possible to still initialize lazy client
CGodiksen 1df0d02
Add maybe_token_ptr parameter to C-API
CGodiksen aa5b6f2
Update C header to match C-API change
CGodiksen 513f26d
Add optional token parameter to Python API
CGodiksen c8276c7
Move BearerInterceptor to modelardb_auth
CGodiksen b0adbbb
Add token to client doc
CGodiksen 1e68daf
Add very simple arg parsing for --token parameter
CGodiksen 4549261
Connect with optional bearer token in client
CGodiksen 46544be
Move parsing of token into AsciiMetadataValue to BearerInterceptor::t…
CGodiksen e82e3fd
Pass authorization header down for INCLUDE requests
CGodiksen 5e842b4
Make Authenticator::authorize() async
CGodiksen 9f61378
Add tests for BearerInterceptor
CGodiksen e0911da
Formatted with Rustfmt
CGodiksen 1879511
Merge branch 'main' into dev/security-manager
CGodiksen 2708cac
Add token to clap parsing in modelardb_client
CGodiksen cab26de
Merge branch 'main' into dev/security-manager
CGodiksen a7ae644
Update Authenticator based on comments from @skejserjensen
CGodiksen 7cd5abf
Use AsciiMetadataValue instead of MetadataValue<Ascii>
CGodiksen a112d8c
Make calling LIST_ACTIONS require admin permission
CGodiksen b011abc
Use None instead of NoAuth
CGodiksen 76cdc54
Remove NoAuth
CGodiksen c90fbfe
Merge branch 'main' into dev/security-manager
CGodiksen cb3705c
Add support for optimize in auth layer
CGodiksen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = [] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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() | ||
|
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(()) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
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 | ||
|
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()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.