1#![warn(clippy::all)]
93#![warn(missing_copy_implementations)]
94#![warn(missing_debug_implementations)]
95
96pub mod error;
98
99pub mod acs;
101pub mod security;
102
103pub use security::security_and_compliance::{
105 SecurityAndComplianceV1Service, SecurityAndComplianceV2Service,
106};
107
108use acs::acs::AcsProject;
110use security::security_and_compliance::SecurityAndComplianceProject;
111
112pub use crate::error::SecurityError;
114
115pub use openlark_core::config::Config;
117
118#[derive(Debug, Clone)]
127pub struct SecurityClient {
128 config: openlark_core::config::Config,
129 pub acs: AcsProject,
131 pub security_and_compliance: SecurityAndComplianceProject,
133}
134
135impl SecurityClient {
136 pub fn new(config: openlark_core::config::Config) -> Self {
140 Self {
141 acs: AcsProject::new(config.clone()),
142 security_and_compliance: SecurityAndComplianceProject::new(config.clone()),
143 config,
144 }
145 }
146
147 pub fn config(&self) -> &openlark_core::config::Config {
149 &self.config
150 }
151}
152
153pub type SecurityResult<T> = Result<T, crate::error::SecurityError>;
158
159pub mod prelude {
161 pub use super::{SecurityClient, SecurityResult};
162
163 pub use super::acs::acs::AcsV1Service;
165 pub use super::security::security_and_compliance::{
166 SecurityAndComplianceV1Service, SecurityAndComplianceV2Service,
167 };
168}
169
170#[cfg(test)]
171mod construction_tests {
172 use super::*;
173 use openlark_core::auth::{TokenProvider, TokenRequest};
174 use openlark_core::error::ErrorTrait;
175 use std::future::Future;
176 use std::pin::Pin;
177 use wiremock::matchers::{header, method, path};
178 use wiremock::{Mock, MockServer, ResponseTemplate};
179
180 #[derive(Debug, Clone)]
182 struct TestTokenProvider(&'static str);
183
184 impl TokenProvider for TestTokenProvider {
185 fn get_token(
186 &self,
187 _request: TokenRequest,
188 ) -> Pin<Box<dyn Future<Output = openlark_core::SDKResult<String>> + Send + '_>> {
189 let token = self.0.to_string();
190 Box::pin(async move { Ok(token) })
191 }
192 }
193
194 #[tokio::test]
199 async fn security_client_new_canonical_config_propagates_base_headers_and_token_provider() {
200 let server = MockServer::start().await;
201
202 Mock::given(method("GET"))
204 .and(path("/open-apis/acs/v1/users"))
205 .and(header("Authorization", "Bearer test_tok_from_provider"))
206 .and(header("X-Custom-Prop", "yes"))
207 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
208 "code": 0,
209 "msg": "success",
210 "data": { "has_more": false, "items": [] }
211 })))
212 .mount(&server)
213 .await;
214
215 let base = Config::builder()
216 .app_id("test_app")
217 .app_secret("test_secret")
218 .base_url(server.uri())
219 .allow_custom_base_url(true)
220 .add_header("X-Custom-Prop", "yes")
221 .req_timeout(std::time::Duration::from_secs(30))
222 .max_response_size(8 * 1024 * 1024)
223 .build();
224
225 let config_with_provider =
226 base.with_token_provider(TestTokenProvider("test_tok_from_provider"));
227
228 let client = SecurityClient::new(config_with_provider);
229
230 let _resp = client
235 .acs
236 .v1()
237 .users()
238 .list()
239 .execute()
240 .await
241 .expect("wiremock 应返回成功响应");
242
243 let received = server.received_requests().await.unwrap_or_default();
244 assert_eq!(received.len(), 1, "应只发一次 security leaf 请求");
245
246 let req = &received[0];
249 assert!(
250 req.url.path() == "/open-apis/acs/v1/users"
251 || req.url.as_str().contains("/acs/v1/users"),
252 "请求路径应指向 ACS leaf,实际: {}",
253 req.url
254 );
255 }
256
257 #[tokio::test]
260 async fn security_client_new_canonical_config_propagates_to_compliance_v2_leaf() {
261 let server = MockServer::start().await;
262
263 Mock::given(method("GET"))
265 .and(path(
266 "/open-apis/security_and_compliance/v2/device_records/mine",
267 ))
268 .and(header("Authorization", "Bearer test_tok_from_provider"))
269 .and(header("X-Compliance-Test", "propagated"))
270 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
271 "code": 0,
272 "msg": "success",
273 "data": { "has_more": false, "items": [] }
274 })))
275 .mount(&server)
276 .await;
277
278 let base = Config::builder()
279 .app_id("test_app")
280 .app_secret("test_secret")
281 .base_url(server.uri())
282 .allow_custom_base_url(true)
283 .add_header("X-Compliance-Test", "propagated")
284 .req_timeout(std::time::Duration::from_secs(30))
285 .max_response_size(8 * 1024 * 1024)
286 .build();
287
288 let config_with_provider =
289 base.with_token_provider(TestTokenProvider("test_tok_from_provider"));
290
291 let client = SecurityClient::new(config_with_provider);
292
293 let _ = client
296 .security_and_compliance
297 .v2()
298 .device_records()
299 .mine()
300 .execute()
301 .await
302 .expect("compliance v2 leaf 应成功");
303
304 let received = server.received_requests().await.unwrap_or_default();
305 assert_eq!(received.len(), 1, "应只发一次 compliance leaf 请求");
306 assert!(
307 received[0].url.path().contains("/device_records/mine"),
308 "请求路径应指向 compliance v2 leaf"
309 );
310 }
311
312 #[tokio::test]
314 async fn security_client_new_canonical_config_propagates_to_compliance_v1_leaf() {
315 let server = MockServer::start().await;
316
317 Mock::given(method("POST"))
318 .and(path(
319 "/open-apis/security_and_compliance/v1/openapi_logs/list_data",
320 ))
321 .and(header("Authorization", "Bearer test_tok_from_provider"))
322 .and(header("X-Compliance-V1", "propagated"))
323 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
324 "code": 0,
325 "msg": "success",
326 "data": { "items": [{ "request_id": "r1" }], "has_more": false }
327 })))
328 .mount(&server)
329 .await;
330
331 let base = Config::builder()
332 .app_id("test_app")
333 .app_secret("test_secret")
334 .base_url(server.uri())
335 .allow_custom_base_url(true)
336 .add_header("X-Compliance-V1", "propagated")
337 .build();
338
339 let config_with_provider =
340 base.with_token_provider(TestTokenProvider("test_tok_from_provider"));
341
342 let client = SecurityClient::new(config_with_provider);
343
344 use serde_json::json;
348 let _ = client
349 .security_and_compliance
350 .v1()
351 .openapi_logs()
352 .list_data()
353 .body(json!({ "start_time": 1700000000, "end_time": 1700003600 }))
354 .execute()
355 .await
356 .expect("compliance v1 leaf 应成功");
357
358 let received = server.received_requests().await.unwrap_or_default();
359 assert_eq!(received.len(), 1);
360 assert!(received[0].url.path().contains("/openapi_logs/list_data"));
361 }
362
363 #[tokio::test]
365 async fn compliance_v1_preserves_retryable_error_and_request_id() {
366 let server = MockServer::start().await;
367 Mock::given(method("POST"))
368 .and(path(
369 "/open-apis/security_and_compliance/v1/openapi_logs/list_data",
370 ))
371 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
372 "code": 503,
373 "msg": "compliance v1 unavailable",
374 "request_id": "req-compliance-v1-503"
375 })))
376 .mount(&server)
377 .await;
378
379 let config = Config::builder()
380 .app_id("test_app")
381 .app_secret("test_secret")
382 .base_url(server.uri())
383 .allow_custom_base_url(true)
384 .build()
385 .with_token_provider(TestTokenProvider("compliance_v1_token"));
386 let client = SecurityClient::new(config);
387
388 let err = client
389 .security_and_compliance
390 .v1()
391 .openapi_logs()
392 .list_data()
393 .body(serde_json::json!({}))
394 .execute()
395 .await
396 .expect_err("compliance v1 业务错误必须向上传播");
397
398 assert!(err.is_retryable());
399 assert_eq!(err.context().request_id(), Some("req-compliance-v1-503"));
400 }
401
402 #[tokio::test]
404 async fn compliance_v2_preserves_retryable_error_and_request_id() {
405 let server = MockServer::start().await;
406 Mock::given(method("GET"))
407 .and(path(
408 "/open-apis/security_and_compliance/v2/device_records/mine",
409 ))
410 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
411 "code": 429,
412 "msg": "compliance v2 rate limited",
413 "request_id": "req-compliance-v2-429"
414 })))
415 .mount(&server)
416 .await;
417
418 let config = Config::builder()
419 .app_id("test_app")
420 .app_secret("test_secret")
421 .base_url(server.uri())
422 .allow_custom_base_url(true)
423 .build()
424 .with_token_provider(TestTokenProvider("compliance_v2_token"));
425 let client = SecurityClient::new(config);
426
427 let err = client
428 .security_and_compliance
429 .v2()
430 .device_records()
431 .mine()
432 .execute()
433 .await
434 .expect_err("compliance v2 业务错误必须向上传播");
435
436 assert!(err.is_retryable());
437 assert_eq!(err.context().request_id(), Some("req-compliance-v2-429"));
438 }
439
440 #[tokio::test]
443 async fn security_client_timeout_propagates_and_triggers_timeout_error() {
444 let server = MockServer::start().await;
445
446 Mock::given(method("GET"))
448 .and(path("/open-apis/acs/v1/users"))
449 .respond_with(
450 ResponseTemplate::new(200)
451 .set_body_json(serde_json::json!({"code":0,"msg":"ok","data":{"has_more":false,"items":[]}}))
452 .set_delay(std::time::Duration::from_millis(800)),
453 )
454 .mount(&server)
455 .await;
456
457 let base = Config::builder()
458 .app_id("test_app")
459 .app_secret("test_secret")
460 .base_url(server.uri())
461 .allow_custom_base_url(true)
462 .req_timeout(std::time::Duration::from_millis(50)) .build();
464
465 let config = base.with_token_provider(TestTokenProvider("test_tok_for_timeout_test"));
466
467 let client = SecurityClient::new(config);
468
469 let result = client.acs.v1().users().list().execute().await;
470
471 assert!(result.is_err(), "应因 timeout 配置触发错误");
473 let err = result.unwrap_err().to_string().to_lowercase();
474 assert!(
476 err.contains("timeout")
477 || err.contains("time")
478 || err.contains("deadline")
479 || err.contains("network")
480 || err.contains("send"),
481 "错误应体现超时或网络失败,实际: {}",
482 err
483 );
484 }
485
486 #[tokio::test]
488 async fn security_client_max_response_size_propagates_and_triggers_size_error() {
489 let server = MockServer::start().await;
490
491 let big_body = serde_json::json!({
493 "code": 0,
494 "msg": "ok",
495 "data": { "has_more": false, "items": [ {"x": "y".repeat(1024)} ] }
496 });
497 let big_json = serde_json::to_vec(&big_body).unwrap();
498
499 Mock::given(method("GET"))
500 .and(path("/open-apis/acs/v1/users"))
501 .respond_with(ResponseTemplate::new(200).set_body_raw(big_json, "application/json"))
502 .mount(&server)
503 .await;
504
505 let base = Config::builder()
506 .app_id("test_app")
507 .app_secret("test_secret")
508 .base_url(server.uri())
509 .allow_custom_base_url(true)
510 .max_response_size(512) .build();
512
513 let config = base.with_token_provider(TestTokenProvider("test_tok_for_size_test"));
514
515 let client = SecurityClient::new(config);
516
517 let result = client.acs.v1().users().list().execute().await;
518
519 assert!(result.is_err(), "应因 response size 超限触发错误");
520 let err = result.unwrap_err().to_string();
521 assert!(
523 err.contains("响应体过大")
524 || err.to_lowercase().contains("large")
525 || err.to_lowercase().contains("size")
526 || err.contains("超过限制"),
527 "错误应体现响应过大,实际: {}",
528 err
529 );
530 }
531}