diff --git a/objectstore-server/src/auth/error.rs b/objectstore-server/src/auth/error.rs index 01cccae7..8d47b444 100644 --- a/objectstore-server/src/auth/error.rs +++ b/objectstore-server/src/auth/error.rs @@ -1,3 +1,4 @@ +use objectstore_types::auth::Permission; use thiserror::Error; /// Error type for different authorization failure scenarios. @@ -25,3 +26,25 @@ pub enum AuthError { #[error("operation not allowed")] NotPermitted, } + +impl AuthError { + /// Return a shortname for the failure reason that can be used to tag metrics. + pub fn code(&self) -> &'static str { + match self { + Self::BadRequest(_) => "bad_request", + Self::InternalError(_) => "internal_error", + Self::ValidationFailure(_) => "validation_failure", + Self::VerificationFailure => "verification_failure", + Self::NotPermitted => "not_permitted", + } + } + + /// Increment a counter and emit a debug log for this auth failure. + pub fn log(&self, permission: Option, usecase: Option<&str>) { + objectstore_metrics::counter!( + "server.auth.failure": 1, + "code" => self.code(), + ); + tracing::debug!(?permission, ?usecase, ?self, "Authorization failure") + } +} diff --git a/objectstore-server/src/auth/service.rs b/objectstore-server/src/auth/service.rs index 897a43fc..47823ae2 100644 --- a/objectstore-server/src/auth/service.rs +++ b/objectstore-server/src/auth/service.rs @@ -4,7 +4,7 @@ use objectstore_service::{PayloadStream, StorageService}; use objectstore_types::auth::Permission; use objectstore_types::metadata::Metadata; -use crate::auth::AuthContext; +use crate::auth::{AuthContext, AuthError}; use crate::endpoints::common::ApiResult; /// Wrapper around [`StorageService`] that ensures each operation is authorized. @@ -32,23 +32,47 @@ use crate::endpoints::common::ApiResult; pub struct AuthAwareService { service: StorageService, context: Option, + enforce: bool, } impl AuthAwareService { - /// Creates a new `AuthAwareService` using the given service and auth context. + /// Creates a new `AuthAwareService` using the given [`StorageService`], [`AuthContext`], and + /// enforcement setting. /// - /// If no auth context is provided, authorization is disabled and all operations will be - /// permitted. - pub fn new(service: StorageService, context: Option) -> Self { - Self { service, context } + /// If enforcement is enabled, an `AuthContext` must be provided and its checks must succeed + /// for an operation to be permitted. + /// + /// If enforcement is disabled, an `AuthContext` is not required. If one is provided, its + /// checks will be run but their results ignored. All operations will be permitted. + pub fn new( + service: StorageService, + context: Option, + enforce: bool, + ) -> ApiResult { + if enforce && context.is_none() { + let err = AuthError::InternalError("Missing auth context".into()); + err.log(None, None); + Err(err.into()) + } else { + Ok(Self { + service, + context, + enforce, + }) + } } fn assert_authorized(&self, perm: Permission, context: &ObjectContext) -> ApiResult<()> { - if let Some(auth) = &self.context { - auth.assert_authorized(perm, context)?; + let auth_result = match &self.context { + Some(auth) => auth.assert_authorized(perm, context), + None => Ok(()), } + .inspect_err(|err| err.log(Some(perm), Some(context.usecase.as_str()))); - Ok(()) + match self.enforce { + true => Ok(auth_result?), + false => Ok(()), + } } /// Checks whether the request is authorized for the given permission on the given context. diff --git a/objectstore-server/src/extractors/service.rs b/objectstore-server/src/extractors/service.rs index 31a2b7a3..8268c0d2 100644 --- a/objectstore-server/src/extractors/service.rs +++ b/objectstore-server/src/extractors/service.rs @@ -14,21 +14,29 @@ impl FromRequestParts for AuthAwareService { parts: &mut Parts, state: &ServiceState, ) -> Result { - let service = state.service.clone(); - if !state.config.auth.enforce { - return Ok(AuthAwareService::new(service, None)); - } - let encoded_token = parts .headers .get(header::AUTHORIZATION) .and_then(|v| v.to_str().ok()) .and_then(strip_bearer); - let context = AuthContext::from_encoded_jwt(encoded_token, &state.key_directory) - .inspect_err(|err| tracing::debug!("Authorization rejected: `{:?}`", err))?; + // Attempt to decode / verify the JWT, logging failure + let auth_result = AuthContext::from_encoded_jwt(encoded_token, &state.key_directory) + .inspect_err(|err| err.log(None, None)); + + // If auth enforcement is enabled, `from_encoded_jwt()` must have succeeded. + // If auth enforcement is disabled, we'll pass the context along if it succeeded but will + // still proceed with `None` if it failed. + let auth_context = match state.config.auth.enforce { + true => Some(auth_result?), + false => auth_result.ok(), + }; - Ok(AuthAwareService::new(service, Some(context))) + AuthAwareService::new( + state.service.clone(), + auth_context, + state.config.auth.enforce, + ) } } diff --git a/objectstore-types/src/auth.rs b/objectstore-types/src/auth.rs index 0aa5fd3b..4cd4615b 100644 --- a/objectstore-types/src/auth.rs +++ b/objectstore-types/src/auth.rs @@ -8,7 +8,7 @@ use std::collections::HashSet; use serde::{Deserialize, Serialize}; /// Permissions that control whether different operations are authorized. -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, Copy)] pub enum Permission { /// Read / download objects (serialized as `"object.read"`). #[serde(rename = "object.read")] @@ -33,3 +33,14 @@ impl Permission { ]) } } + +impl std::fmt::Display for Permission { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s = match self { + Self::ObjectRead => "object.read", + Self::ObjectWrite => "object.write", + Self::ObjectDelete => "object.delete", + }; + f.write_str(s) + } +}