Skip to content

Commit

Permalink
Merge pull request #519 from stumpapp/release/v0.0.9
Browse files Browse the repository at this point in the history
🔖 Release v0.0.9
  • Loading branch information
aaronleopold authored Dec 8, 2024
2 parents b8b837e + f341789 commit a2c4280
Show file tree
Hide file tree
Showing 16 changed files with 393 additions and 312 deletions.
9 changes: 9 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

<a name="0.0.9"></a>
## 0.0.9 (2024-12-08)

### Fixed

- 🐛 Fix API key OPDS links in v1.2 feed ([#518](https://github.com/stumpapp/stump/issues/518)) [[6984bfe](https://github.com/stumpapp/stump/commit/6984bfecac3c408c4294e1f19ec7c47d3b081d1d)]
- 🐛 Fix Path extractor error in v1.2 OPDS router [[b06ee48](https://github.com/stumpapp/stump/commit/b06ee4896e86de22c3073dc6ee38bab2abc22ee5)]


<a name="0.0.8"></a>
## 0.0.8 (2024-12-04)

Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ members = [
]

[workspace.package]
version = "0.0.8"
version = "0.0.9"
rust-version = "1.81.0"

[workspace.dependencies]
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stump/desktop",
"version": "0.0.8",
"version": "0.0.9",
"description": "",
"license": "MIT",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@
"tailwindcss": "3.3.2"
},
"name": "@stump/mobile",
"version": "0.0.8",
"version": "0.0.9",
"private": true
}
120 changes: 97 additions & 23 deletions apps/server/src/middleware/auth.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use axum::{
body::Body,
extract::{OriginalUri, Path, Request, State},
Expand All @@ -9,6 +11,7 @@ use axum::{
use base64::{engine::general_purpose::STANDARD, Engine};
use prefixed_api_key::{PrefixedApiKey, PrefixedApiKeyController};
use prisma_client_rust::or;
use serde::Deserialize;
use stump_core::{
db::entity::{APIKeyPermissions, User, UserPermission, API_KEY_PREFIX},
opds::v2_0::{
Expand Down Expand Up @@ -44,17 +47,24 @@ use super::host::HostExtractor;
/// - They have a valid bearer token (session may not exist)
/// - They have valid basic auth credentials (session is created after successful authentication)
#[derive(Debug, Clone)]
pub struct RequestContext(User);
pub struct RequestContext {
user: User,
api_key: Option<String>,
}

impl RequestContext {
/// Get a reference to the current user
pub fn user(&self) -> &User {
&self.0
&self.user
}

/// Get the ID of the current user
pub fn id(&self) -> String {
self.0.id.clone()
self.user.id.clone()
}

pub fn api_key(&self) -> Option<String> {
self.api_key.clone()
}

/// Enforce that the current user has all the permissions provided, otherwise return an error
Expand Down Expand Up @@ -155,7 +165,10 @@ pub async fn auth_middleware(

if let Some(user) = session_user {
if !user.is_locked {
req.extensions_mut().insert(RequestContext(user));
req.extensions_mut().insert(RequestContext {
user,
api_key: None,
});
return Ok(next.run(req).await);
}
}
Expand Down Expand Up @@ -186,7 +199,7 @@ pub async fn auth_middleware(
return Err(APIError::Unauthorized.into_response());
};

let user = match auth_header {
let req_ctx = match auth_header {
_ if auth_header.starts_with("Bearer ") && auth_header.len() > 7 => {
let token = auth_header[7..].to_owned();
handle_bearer_auth(token, &ctx.db)
Expand All @@ -202,11 +215,20 @@ pub async fn auth_middleware(
_ => return Err(APIError::Unauthorized.into_response()),
};

req.extensions_mut().insert(RequestContext(user));
req.extensions_mut().insert(req_ctx);

Ok(next.run(req).await)
}

#[derive(Debug, Deserialize)]
pub struct APIKeyPath(HashMap<String, String>);

impl APIKeyPath {
fn get_key(&self) -> Option<String> {
self.0.get("api_key").cloned()
}
}

/// A middleware to authenticate a user by an API key in a *very* specific way. This middleware
/// assumes that a fully qualified API key is provided in the path. This is used for two features today:
///
Expand All @@ -218,10 +240,15 @@ pub async fn auth_middleware(
/// hashing algorithm, therefore the default auth method would not work.
pub async fn api_key_middleware(
State(ctx): State<AppState>,
Path(api_key): Path<String>,
Path(params): Path<APIKeyPath>,
mut req: Request,
next: Next,
) -> Result<Response, impl IntoResponse> {
let Some(api_key) = params.get_key() else {
tracing::error!("No API key provided");
return Err(APIError::Unauthorized.into_response());
};

let Ok(pak) = PrefixedApiKey::from_string(api_key.as_str()) else {
tracing::error!("Failed to parse API key");
return Err(APIError::Unauthorized.into_response());
Expand All @@ -231,7 +258,10 @@ pub async fn api_key_middleware(
.await
.map_err(|e| e.into_response())?;

req.extensions_mut().insert(RequestContext(user));
req.extensions_mut().insert(RequestContext {
user,
api_key: Some(api_key),
});

Ok(next.run(req).await)
}
Expand Down Expand Up @@ -338,10 +368,18 @@ pub async fn validate_api_key(
/// A function to handle bearer token authentication. This function will verify the token and
/// return the user if the token is valid.
#[tracing::instrument(skip_all)]
async fn handle_bearer_auth(token: String, client: &PrismaClient) -> APIResult<User> {
async fn handle_bearer_auth(
token: String,
client: &PrismaClient,
) -> APIResult<RequestContext> {
match PrefixedApiKey::from_string(token.as_str()) {
Ok(api_key) if api_key.prefix() == API_KEY_PREFIX => {
return validate_api_key(api_key, client).await;
return validate_api_key(api_key, client)
.await
.map(|user| RequestContext {
user,
api_key: Some(token),
});
},
_ => (),
};
Expand Down Expand Up @@ -371,7 +409,10 @@ async fn handle_bearer_auth(token: String, client: &PrismaClient) -> APIResult<U
));
}

Ok(User::from(user))
Ok(RequestContext {
user: User::from(user),
api_key: None,
})
}

/// A function to handle basic authentication. This function will decode the credentials and
Expand All @@ -384,7 +425,7 @@ async fn handle_basic_auth(
encoded_credentials: String,
client: &PrismaClient,
session: &mut Session,
) -> APIResult<User> {
) -> APIResult<RequestContext> {
let decoded_bytes = STANDARD
.decode(encoded_credentials.as_bytes())
.map_err(|e| APIError::InternalServerError(e.to_string()))?;
Expand Down Expand Up @@ -427,7 +468,10 @@ async fn handle_basic_auth(
enforce_max_sessions(&user, client).await?;
let user = User::from(user);
session.insert(SESSION_USER_KEY, user.clone()).await?;
return Ok(user);
return Ok(RequestContext {
user,
api_key: None,
});
}

Err(APIError::Unauthorized)
Expand Down Expand Up @@ -558,14 +602,20 @@ mod tests {
#[test]
fn test_request_context_user() {
let user = User::default();
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(user.is(request_context.user()));
}

#[test]
fn test_request_context_id() {
let user = User::default();
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert_eq!(user.id, request_context.id());
}

Expand All @@ -575,7 +625,10 @@ mod tests {
is_server_owner: true,
..Default::default()
};
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(request_context
.enforce_permissions(&[UserPermission::AccessBookClub])
.is_ok());
Expand All @@ -587,7 +640,10 @@ mod tests {
permissions: vec![UserPermission::AccessBookClub],
..Default::default()
};
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(request_context
.enforce_permissions(&[UserPermission::AccessBookClub])
.is_ok());
Expand All @@ -596,7 +652,10 @@ mod tests {
#[test]
fn test_request_context_enforce_permissions_when_denied() {
let user = User::default();
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(request_context
.enforce_permissions(&[UserPermission::AccessBookClub])
.is_err());
Expand All @@ -608,7 +667,10 @@ mod tests {
permissions: vec![UserPermission::AccessBookClub],
..Default::default()
};
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(request_context
.enforce_permissions(&[
UserPermission::AccessBookClub,
Expand All @@ -623,7 +685,10 @@ mod tests {
permissions: vec![UserPermission::AccessBookClub],
..Default::default()
};
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(user.is(&request_context
.user_and_enforce_permissions(&[UserPermission::AccessBookClub])
.unwrap()));
Expand All @@ -632,7 +697,10 @@ mod tests {
#[test]
fn test_request_context_user_and_enforce_permissions_when_denied() {
let user = User::default();
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(request_context
.user_and_enforce_permissions(&[UserPermission::AccessBookClub])
.is_err());
Expand All @@ -644,14 +712,20 @@ mod tests {
is_server_owner: true,
..Default::default()
};
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(request_context.enforce_server_owner().is_ok());
}

#[test]
fn test_request_context_enforce_server_owner_when_not_server_owner() {
let user = User::default();
let request_context = RequestContext(user.clone());
let request_context = RequestContext {
user: user.clone(),
api_key: None,
};
assert!(request_context.enforce_server_owner().is_err());
}

Expand Down
Loading

0 comments on commit a2c4280

Please sign in to comment.