fix(auth): iam check
This commit is contained in:
@@ -4,15 +4,11 @@ use std::sync::{
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{Json, Router, routing::post};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Json, Router, routing::post};
|
||||
use cms_service::infrastructure::iam_client::{IamClient, IamClientConfig};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthorizationCheckRequest {
|
||||
permission: String,
|
||||
}
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AuthorizationCheckResponse {
|
||||
@@ -31,28 +27,28 @@ async fn start_mock_iam(
|
||||
call_count: Arc<AtomicUsize>,
|
||||
fail: Arc<AtomicBool>,
|
||||
) -> (String, tokio::task::JoinHandle<()>) {
|
||||
let app = Router::new().route(
|
||||
"/authorize/check",
|
||||
post(move |Json(body): Json<AuthorizationCheckRequest>| {
|
||||
let call_count = call_count.clone();
|
||||
let fail = fail.clone();
|
||||
async move {
|
||||
call_count.fetch_add(1, Ordering::SeqCst);
|
||||
if fail.load(Ordering::SeqCst) {
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "fail").into_response();
|
||||
}
|
||||
|
||||
let allowed = body.permission == "cms:article:read";
|
||||
let resp = ApiSuccessResponse {
|
||||
code: 0,
|
||||
message: "ok".to_string(),
|
||||
data: AuthorizationCheckResponse { allowed },
|
||||
trace_id: None,
|
||||
};
|
||||
(axum::http::StatusCode::OK, Json(resp)).into_response()
|
||||
let handler = move |Json(_body): Json<Value>| {
|
||||
let call_count = call_count.clone();
|
||||
let fail = fail.clone();
|
||||
async move {
|
||||
call_count.fetch_add(1, Ordering::SeqCst);
|
||||
if fail.load(Ordering::SeqCst) {
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "fail").into_response();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let resp = ApiSuccessResponse {
|
||||
code: 0,
|
||||
message: "ok".to_string(),
|
||||
data: AuthorizationCheckResponse { allowed: true },
|
||||
trace_id: None,
|
||||
};
|
||||
(axum::http::StatusCode::OK, Json(resp)).into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/authorize/check-expr", post(handler.clone()))
|
||||
.route("/api/v1/authorize/check-expr", post(handler));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
@@ -81,11 +77,11 @@ async fn iam_client_caches_decisions() {
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
client
|
||||
.require_permission(tenant_id, user_id, "cms:article:read", "token")
|
||||
.require_permission(tenant_id, user_id, "cms:article:edit", "token")
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.require_permission(tenant_id, user_id, "cms:article:read", "token")
|
||||
.require_permission(tenant_id, user_id, "cms:article:edit", "token")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -111,7 +107,7 @@ async fn iam_client_uses_stale_cache_on_error() {
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
client
|
||||
.require_permission(tenant_id, user_id, "cms:article:read", "token")
|
||||
.require_permission(tenant_id, user_id, "cms:article:edit", "token")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -119,7 +115,7 @@ async fn iam_client_uses_stale_cache_on_error() {
|
||||
fail.store(true, Ordering::SeqCst);
|
||||
|
||||
client
|
||||
.require_permission(tenant_id, user_id, "cms:article:read", "token")
|
||||
.require_permission(tenant_id, user_id, "cms:article:edit", "token")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
68
tests/iam_client_expr.rs
Normal file
68
tests/iam_client_expr.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{Json, Router, routing::post};
|
||||
use axum::response::IntoResponse;
|
||||
use cms_service::infrastructure::iam_client::{IamClient, IamClientConfig};
|
||||
use serde_json::Value;
|
||||
|
||||
async fn start_mock_iam(call_count: Arc<AtomicUsize>) -> (String, tokio::task::JoinHandle<()>) {
|
||||
let app = Router::new().route(
|
||||
"/api/v1/authorize/check-expr",
|
||||
post(move |Json(body): Json<Value>| {
|
||||
let call_count = call_count.clone();
|
||||
async move {
|
||||
call_count.fetch_add(1, Ordering::SeqCst);
|
||||
let allowed = body.get("expr").is_some();
|
||||
let resp = serde_json::json!({
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": { "allowed": allowed }
|
||||
});
|
||||
(axum::http::StatusCode::OK, Json(resp)).into_response()
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let base_url = format!("http://{}", addr);
|
||||
let handle = tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
(base_url, handle)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn iam_client_check_expr_hits_endpoint() {
|
||||
let call_count = Arc::new(AtomicUsize::new(0));
|
||||
let (base_url, handle) = start_mock_iam(call_count.clone()).await;
|
||||
|
||||
let client = IamClient::new(IamClientConfig {
|
||||
base_url,
|
||||
timeout: Duration::from_millis(500),
|
||||
cache_ttl: Duration::from_secs(5),
|
||||
cache_stale_if_error: Duration::from_secs(30),
|
||||
cache_max_entries: 1000,
|
||||
});
|
||||
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
client
|
||||
.require_any_permissions(
|
||||
tenant_id,
|
||||
user_id,
|
||||
&["cms:article:edit", "cms:article:create"],
|
||||
"token",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(call_count.load(Ordering::SeqCst), 1);
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
40
tests/iam_refresh_decode.rs
Normal file
40
tests/iam_refresh_decode.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AppResponse<T> {
|
||||
code: i32,
|
||||
message: String,
|
||||
data: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RefreshResponseData {
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
token_type: Option<String>,
|
||||
expires_in: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_decode_iam_refresh_response_snake_case() {
|
||||
let json = r#"
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"access_token": "a",
|
||||
"refresh_token": "r",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 7200
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let parsed: AppResponse<RefreshResponseData> = serde_json::from_str(json).unwrap();
|
||||
let data = parsed.data.unwrap();
|
||||
assert_eq!(data.access_token, "a");
|
||||
assert_eq!(data.refresh_token, "r");
|
||||
assert_eq!(data.token_type.as_deref(), Some("Bearer"));
|
||||
assert_eq!(data.expires_in, 7200);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user