webhook tokens of tombstoned users rejected
This commit is contained in:
@@ -51,8 +51,9 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
|||||||
|
|
||||||
async fn find_by_token_hash(&self, hash: &str) -> Result<Option<WebhookToken>, DomainError> {
|
async fn find_by_token_hash(&self, hash: &str) -> Result<Option<WebhookToken>, DomainError> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
"SELECT id, user_id, token_hash, provider, label, created_at, last_used_at \
|
"SELECT t.id, t.user_id, t.token_hash, t.provider, t.label, t.created_at, t.last_used_at \
|
||||||
FROM webhook_tokens WHERE token_hash = ?",
|
FROM webhook_tokens t JOIN users u ON u.id = t.user_id \
|
||||||
|
WHERE t.token_hash = ? AND u.deleted_at IS NULL",
|
||||||
)
|
)
|
||||||
.bind(hash)
|
.bind(hash)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
|
|||||||
@@ -349,3 +349,61 @@ async fn the_service_actor_is_not_deletable_and_offers_no_delete_ui() {
|
|||||||
let (_, _, body) = send(&router, req).await;
|
let (_, _, body) = send(&router, req).await;
|
||||||
assert!(!body.contains("is_service_account"), "{body}");
|
assert!(!body.contains("is_service_account"), "{body}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_deleted_users_webhook_token_stops_ingesting_at_once() {
|
||||||
|
let (router, pool, alice, _bob) = two_users().await;
|
||||||
|
let (status, _, body) = send(
|
||||||
|
&router,
|
||||||
|
json_request(
|
||||||
|
Method::POST,
|
||||||
|
"/api/v1/settings/webhook-tokens",
|
||||||
|
Some(&alice.token),
|
||||||
|
serde_json::json!({ "provider": "jellyfin", "label": null }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, 200, "{body}");
|
||||||
|
let token = serde_json::from_str::<serde_json::Value>(&body).unwrap()["token"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let hook = |token: &str| {
|
||||||
|
Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri("/api/v1/webhooks/jellyfin")
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::json!({
|
||||||
|
"NotificationType": "PlaybackStop", "ItemType": "Movie",
|
||||||
|
"Name": "Heat", "Year": 1995, "PlayedToCompletion": true,
|
||||||
|
"Provider_tmdb": "949"
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let events = || async {
|
||||||
|
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM watch_events")
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (status, _, body) = send(&router, hook(&token)).await;
|
||||||
|
assert_eq!(status, 200, "live token: {body}");
|
||||||
|
assert_eq!(events().await, 1);
|
||||||
|
|
||||||
|
let (status, _, _) = send(
|
||||||
|
&router,
|
||||||
|
delete_json("/api/v1/users/me", &alice.token, PASSWORD),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, 204);
|
||||||
|
|
||||||
|
let (status, _, body) = send(&router, hook(&token)).await;
|
||||||
|
assert_eq!(status, 401, "{body}");
|
||||||
|
assert_eq!(events().await, 1, "no event ingested after deletion");
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,7 +99,11 @@ impl WebhookTokenRepository for InMemoryStore {
|
|||||||
|
|
||||||
async fn find_by_token_hash(&self, hash: &str) -> Result<Option<WebhookToken>, DomainError> {
|
async fn find_by_token_hash(&self, hash: &str) -> Result<Option<WebhookToken>, DomainError> {
|
||||||
let store = self.webhook_tokens.read().unwrap();
|
let store = self.webhook_tokens.read().unwrap();
|
||||||
Ok(store.iter().find(|t| t.token_hash() == hash).cloned())
|
let gone = self.tombstoned.read().unwrap();
|
||||||
|
Ok(store
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.token_hash() == hash && !gone.contains(&t.user_id().value()))
|
||||||
|
.cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_by_user(&self, user_id: &UserId) -> Result<Vec<WebhookToken>, DomainError> {
|
async fn list_by_user(&self, user_id: &UserId) -> Result<Vec<WebhookToken>, DomainError> {
|
||||||
|
|||||||
Reference in New Issue
Block a user