From 0ae9a0911d344ac5cc4300df5443cbf0989c6933 Mon Sep 17 00:00:00 2001 From: Denis Molokanov Date: Tue, 13 Aug 2019 11:41:55 -0700 Subject: [PATCH] Refactor auth integration tests to unit tests (#1563) Refactors integration tests for authentication in edgelet-kube crate to be unit tests instead. --- edgelet/edgelet-kube/src/lib.rs | 70 ++- .../edgelet-kube/src/module/authentication.rs | 269 +++++++++ edgelet/edgelet-kube/src/module/create.rs | 68 +-- .../edgelet-kube/src/module/trust_bundle.rs | 72 +-- edgelet/edgelet-kube/tests/runtime.rs | 563 ------------------ 5 files changed, 347 insertions(+), 695 deletions(-) delete mode 100644 edgelet/edgelet-kube/tests/runtime.rs diff --git a/edgelet/edgelet-kube/src/lib.rs b/edgelet/edgelet-kube/src/lib.rs index 9495cb8f909..0eefc393a5a 100644 --- a/edgelet/edgelet-kube/src/lib.rs +++ b/edgelet/edgelet-kube/src/lib.rs @@ -23,10 +23,21 @@ pub use settings::Settings; #[cfg(test)] mod tests { - use crate::settings::Settings; use config::{Config, File, FileFormat}; + use futures::future; + use hyper::service::Service; + use hyper::{Body, Request, Response, StatusCode}; use json_patch::merge; + use native_tls::TlsConnector; use serde_json::{self, json, Value as JsonValue}; + use typed_headers::{mime, ContentLength, ContentType, HeaderMapExt}; + use url::Url; + + use edgelet_test_utils::web::ResponseFuture; + use kube_client::{Client as KubeClient, Config as KubeConfig, Error, TokenSource}; + + use crate::settings::Settings; + use crate::KubeModuleRuntime; pub const PROXY_TRUST_BUNDLE_CONFIG_MAP_NAME: &str = "device1-iotedged-proxy-trust-bundle"; @@ -80,4 +91,61 @@ mod tests { config.try_into().unwrap() } + + #[derive(Clone)] + pub struct TestTokenSource; + + impl TokenSource for TestTokenSource { + type Error = Error; + + fn get(&self) -> kube_client::error::Result> { + Ok(None) + } + } + + pub fn create_runtime( + settings: Settings, + service: S, + ) -> KubeModuleRuntime { + let client = KubeClient::with_client(get_config(), service); + + KubeModuleRuntime::new(client, settings) + } + + pub fn get_config() -> KubeConfig { + KubeConfig::new( + Url::parse("https://localhost:443").unwrap(), + "/api".to_string(), + TestTokenSource, + TlsConnector::new().unwrap(), + ) + } + + pub fn response( + status_code: StatusCode, + response: impl Fn() -> String + Clone + Send + 'static, + ) -> ResponseFuture { + let response = response(); + let response_len = response.len(); + + let mut response = Response::new(response.into()); + *response.status_mut() = status_code; + response + .headers_mut() + .typed_insert(&ContentLength(response_len as u64)); + response + .headers_mut() + .typed_insert(&ContentType(mime::APPLICATION_JSON)); + + Box::new(future::ok(response)) as ResponseFuture + } + + pub fn not_found_handler(_: Request) -> ResponseFuture { + let response = Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::default()) + .unwrap(); + + Box::new(future::ok(response)) + } } diff --git a/edgelet/edgelet-kube/src/module/authentication.rs b/edgelet/edgelet-kube/src/module/authentication.rs index 7966378af31..cd1f1e3cd5a 100644 --- a/edgelet/edgelet-kube/src/module/authentication.rs +++ b/edgelet/edgelet-kube/src/module/authentication.rs @@ -116,3 +116,272 @@ where None => Either::B(future::ok(AuthId::None)), } } + +#[cfg(test)] +mod tests { + use futures::future; + use hyper::service::service_fn; + use hyper::{header, Body, Method, Request, Response, StatusCode}; + use maplit::btreemap; + use serde_json::json; + use tokio::runtime::Runtime; + use typed_headers::{mime, ContentLength, ContentType, HeaderMapExt}; + + use edgelet_core::{AuthId, Authenticator}; + use edgelet_test_utils::routes; + use edgelet_test_utils::web::{ + make_req_dispatcher, HttpMethod, RequestHandler, RequestPath, ResponseFuture, + }; + + use crate::tests::{create_runtime, make_settings, not_found_handler, response}; + use crate::ErrorKind; + + #[test] + fn it_authenticates_with_none_when_no_auth_token_provided() { + let settings = make_settings(None); + + let dispatch_table = routes!( + POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() + ); + + let handler = make_req_dispatcher(dispatch_table, Box::new(not_found_handler)); + let service = service_fn(handler); + let runtime = create_runtime(settings, service); + let req = Request::default(); + + let task = runtime.authenticate(&req); + + let mut runtime = Runtime::new().unwrap(); + let auth_id = runtime.block_on(task).unwrap(); + + assert_eq!(auth_id, AuthId::None) + } + + #[test] + fn it_authenticates_with_none_when_invalid_auth_header_provided() { + let settings = make_settings(None); + + let dispatch_table = routes!( + POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() + ); + + let handler = make_req_dispatcher(dispatch_table, Box::new(not_found_handler)); + let service = service_fn(handler); + let runtime = create_runtime(settings, service); + let mut req = Request::default(); + req.headers_mut() + .insert(header::AUTHORIZATION, "BeErer token".parse().unwrap()); + + let task = runtime.authenticate(&req); + + let mut runtime = Runtime::new().unwrap(); + let auth_id = runtime.block_on(task).unwrap(); + + assert_eq!(auth_id, AuthId::None) + } + + #[test] + fn it_authenticates_with_none_when_invalid_auth_token_provided() { + let settings = make_settings(None); + + let dispatch_table = routes!( + POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() + ); + + let handler = make_req_dispatcher(dispatch_table, Box::new(not_found_handler)); + let service = service_fn(handler); + let runtime = create_runtime(settings, service); + let mut req = Request::default(); + req.headers_mut().insert( + header::AUTHORIZATION, + "\u{3aa}\u{3a9}\u{3a4}".parse().unwrap(), + ); + + let task = runtime.authenticate(&req); + + let mut runtime = Runtime::new().unwrap(); + let err = runtime.block_on(task).unwrap_err(); + + assert_eq!(err.kind(), &ErrorKind::ModuleAuthenticationError); + } + + #[test] + fn it_authenticates_with_none_when_unknown_auth_token_provided() { + let settings = make_settings(None); + + let dispatch_table = routes!( + POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() + ); + + let handler = make_req_dispatcher(dispatch_table, Box::new(not_found_handler)); + let service = service_fn(handler); + let runtime = create_runtime(settings, service); + let mut req = Request::default(); + req.headers_mut().insert( + header::AUTHORIZATION, + "Bearer token-unknown".parse().unwrap(), + ); + + let task = runtime.authenticate(&req); + + let mut runtime = Runtime::new().unwrap(); + let auth_id = runtime.block_on(task).unwrap(); + + assert_eq!(auth_id, AuthId::None) + } + + #[test] + fn it_authenticates_with_none_when_module_auth_token_provided_but_sa_does_not_exists() { + let settings = make_settings(None); + + let dispatch_table = routes!( + POST "/apis/authentication.k8s.io/v1/tokenreviews" => authenticated_token_review_handler(), + ); + + let handler = make_req_dispatcher(dispatch_table, Box::new(not_found_handler)); + let service = service_fn(handler); + let runtime = create_runtime(settings, service); + let mut req = Request::default(); + req.headers_mut() + .insert(header::AUTHORIZATION, "Bearer token".parse().unwrap()); + + let task = runtime.authenticate(&req); + + let mut runtime = Runtime::new().unwrap(); + let err = runtime.block_on(task).unwrap_err(); + + assert_eq!(err.kind(), &ErrorKind::KubeClient); + } + + #[test] + fn it_authenticates_with_sa_name_when_sa_does_not_contain_original_name() { + let settings = make_settings(None); + + let dispatch_table = routes!( + POST "/apis/authentication.k8s.io/v1/tokenreviews" => authenticated_token_review_handler(), + GET format!("/api/v1/namespaces/{}/serviceaccounts/edgeagent", settings.namespace()) => get_service_account_without_annotations_handler(), + ); + + let handler = make_req_dispatcher(dispatch_table, Box::new(not_found_handler)); + let service = service_fn(handler); + let runtime = create_runtime(settings, service); + let mut req = Request::default(); + req.headers_mut() + .insert(header::AUTHORIZATION, "Bearer token".parse().unwrap()); + + let task = runtime.authenticate(&req); + + let mut runtime = Runtime::new().unwrap(); + let auth_id = runtime.block_on(task).unwrap(); + + assert_eq!(auth_id, AuthId::Value("edgeagent".into())); + } + + #[test] + fn it_authenticates_with_original_name_when_module_auth_token_provided() { + let settings = make_settings(None); + + let dispatch_table = routes!( + POST "/apis/authentication.k8s.io/v1/tokenreviews" => authenticated_token_review_handler(), + GET format!("/api/v1/namespaces/{}/serviceaccounts/edgeagent", settings.namespace()) => get_service_account_with_annotations_handler(), + ); + + let handler = make_req_dispatcher(dispatch_table, Box::new(not_found_handler)); + let service = service_fn(handler); + let runtime = create_runtime(settings, service); + let mut req = Request::default(); + req.headers_mut() + .insert(header::AUTHORIZATION, "Bearer token".parse().unwrap()); + + let task = runtime.authenticate(&req); + + let mut runtime = Runtime::new().unwrap(); + let auth_id = runtime.block_on(task).unwrap(); + + assert_eq!(auth_id, AuthId::Value("$edgeAgent".into())); + } + + fn authenticated_token_review_handler() -> impl Fn(Request) -> ResponseFuture + Clone { + make_token_review_handler(|| { + json!({ + "kind": "TokenReview", + "spec": { "token": "token" }, + "status": { + "authenticated": true, + "user": { + "username": "system:serviceaccount:my-namespace:edgeagent" + } + }} + ) + .to_string() + }) + } + + fn unauthenticated_token_review_handler() -> impl Fn(Request) -> ResponseFuture + Clone { + make_token_review_handler(|| { + json!({ + "kind": "TokenReview", + "spec": { "token": "token" }, + "status": { + "authenticated": false, + }} + ) + .to_string() + }) + } + + fn make_token_review_handler( + on_token_review: impl Fn() -> String + Clone + Send + 'static, + ) -> impl Fn(Request) -> ResponseFuture + Clone { + move |_| { + let response = on_token_review(); + let response_len = response.len(); + + let mut response = Response::new(response.into()); + response + .headers_mut() + .typed_insert(&ContentLength(response_len as u64)); + response + .headers_mut() + .typed_insert(&ContentType(mime::APPLICATION_JSON)); + Box::new(future::ok(response)) as ResponseFuture + } + } + + fn get_service_account_with_annotations_handler( + ) -> impl Fn(Request) -> ResponseFuture + Clone { + move |_| { + response(StatusCode::OK, || { + json!({ + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": "edgeagent", + "namespace": "my-namespace", + "annotations": { + "net.azure-devices.edge.original-moduleid": "$edgeAgent" + } + } + }) + .to_string() + }) + } + } + + fn get_service_account_without_annotations_handler( + ) -> impl Fn(Request) -> ResponseFuture + Clone { + move |_| { + response(StatusCode::OK, || { + json!({ + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": "edgeagent", + "namespace": "my-namespace", + } + }) + .to_string() + }) + } + } +} diff --git a/edgelet/edgelet-kube/src/module/create.rs b/edgelet/edgelet-kube/src/module/create.rs index 7c350cc1145..7b1c6c1228f 100644 --- a/edgelet/edgelet-kube/src/module/create.rs +++ b/edgelet/edgelet-kube/src/module/create.rs @@ -223,15 +223,11 @@ where mod tests { use std::collections::HashMap; - use futures::future; - use hyper::service::{service_fn, Service}; - use hyper::{Body, Method, Request, Response, StatusCode}; + use hyper::service::service_fn; + use hyper::{Body, Method, Request, StatusCode}; use maplit::btreemap; - use native_tls::TlsConnector; use serde_json::json; use tokio::runtime::Runtime; - use typed_headers::{mime, ContentLength, ContentType, HeaderMapExt}; - use url::Url; use docker::models::{AuthConfig, ContainerCreateBody, HostConfig, Mount}; use edgelet_core::{ImagePullPolicy, ModuleSpec}; @@ -240,15 +236,13 @@ mod tests { use edgelet_test_utils::web::{ make_req_dispatcher, HttpMethod, RequestHandler, RequestPath, ResponseFuture, }; - use kube_client::{Client as KubeClient, Config as KubeConfig, TokenSource}; use crate::module::create::{ create_or_update_deployment, create_or_update_role_binding, create_or_update_service_account, }; use crate::module::create_module; - use crate::tests::make_settings; - use crate::{Error, KubeModuleRuntime, Settings}; + use crate::tests::{create_runtime, make_settings, not_found_handler, response}; #[test] fn it_creates_new_deployment_if_does_not_exist() { @@ -547,34 +541,6 @@ mod tests { } } - fn response( - status_code: StatusCode, - response: impl Fn() -> String + Clone + Send + 'static, - ) -> ResponseFuture { - let response = response(); - let response_len = response.len(); - - let mut response = Response::new(response.into()); - *response.status_mut() = status_code; - response - .headers_mut() - .typed_insert(&ContentLength(response_len as u64)); - response - .headers_mut() - .typed_insert(&ContentType(mime::APPLICATION_JSON)); - - Box::new(future::ok(response)) as ResponseFuture - } - - fn not_found_handler(_: Request) -> ResponseFuture { - let response = Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::default()) - .unwrap(); - - Box::new(future::ok(response)) - } - fn create_module_spec(name: &str) -> ModuleSpec { let create_body = ContainerCreateBody::new() .with_host_config( @@ -626,32 +592,4 @@ mod tests { ) .unwrap() } - - fn create_runtime( - settings: Settings, - service: S, - ) -> KubeModuleRuntime { - let client = KubeClient::with_client(get_config(), service); - KubeModuleRuntime::new(client, settings) - } - - fn get_config() -> KubeConfig { - KubeConfig::new( - Url::parse("https://localhost:443").unwrap(), - "/api".to_string(), - TestTokenSource, - TlsConnector::new().unwrap(), - ) - } - - #[derive(Clone)] - struct TestTokenSource; - - impl TokenSource for TestTokenSource { - type Error = Error; - - fn get(&self) -> kube_client::error::Result> { - Ok(None) - } - } } diff --git a/edgelet/edgelet-kube/src/module/trust_bundle.rs b/edgelet/edgelet-kube/src/module/trust_bundle.rs index a2c5eea82cc..4ca7d78418d 100644 --- a/edgelet/edgelet-kube/src/module/trust_bundle.rs +++ b/edgelet/edgelet-kube/src/module/trust_bundle.rs @@ -81,15 +81,11 @@ where #[cfg(test)] mod tests { - use futures::future; - use hyper::service::{service_fn, Service}; + use hyper::service::service_fn; use hyper::{Body, Error as HyperError, Method, Request, Response, StatusCode}; use maplit::btreemap; - use native_tls::TlsConnector; use serde_json::json; use tokio::runtime::Runtime; - use typed_headers::{mime, ContentLength, ContentType, HeaderMapExt}; - use url::Url; use edgelet_test_utils::cert::TestCert; use edgelet_test_utils::crypto::TestHsm; @@ -97,12 +93,13 @@ mod tests { use edgelet_test_utils::web::{ make_req_dispatcher, HttpMethod, RequestHandler, RequestPath, ResponseFuture, }; - use kube_client::{Client as KubeClient, Config as KubeConfig, Error, TokenSource}; use crate::module::init_trust_bundle; - use crate::tests::{make_settings, PROXY_TRUST_BUNDLE_CONFIG_MAP_NAME}; - use crate::Settings; - use crate::{ErrorKind, KubeModuleRuntime}; + use crate::tests::{ + create_runtime, make_settings, not_found_handler, response, + PROXY_TRUST_BUNDLE_CONFIG_MAP_NAME, + }; + use crate::ErrorKind; #[test] fn it_fails_when_trust_bundle_unavailable() { @@ -270,61 +267,4 @@ mod tests { }) } } - - fn response( - status_code: StatusCode, - response: impl Fn() -> String + Clone + Send + 'static, - ) -> ResponseFuture { - let response = response(); - let response_len = response.len(); - - let mut response = Response::new(response.into()); - *response.status_mut() = status_code; - response - .headers_mut() - .typed_insert(&ContentLength(response_len as u64)); - response - .headers_mut() - .typed_insert(&ContentType(mime::APPLICATION_JSON)); - - Box::new(future::ok(response)) as ResponseFuture - } - - fn not_found_handler(_: Request) -> ResponseFuture { - let response = Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::default()) - .unwrap(); - - Box::new(future::ok(response)) - } - - fn create_runtime( - settings: Settings, - service: S, - ) -> KubeModuleRuntime { - let client = KubeClient::with_client(get_config(), service); - - KubeModuleRuntime::new(client, settings) - } - - fn get_config() -> KubeConfig { - KubeConfig::new( - Url::parse("https://localhost:443").unwrap(), - "/api".to_string(), - TestTokenSource, - TlsConnector::new().unwrap(), - ) - } - - #[derive(Clone)] - struct TestTokenSource; - - impl TokenSource for TestTokenSource { - type Error = Error; - - fn get(&self) -> kube_client::error::Result> { - Ok(None) - } - } } diff --git a/edgelet/edgelet-kube/tests/runtime.rs b/edgelet/edgelet-kube/tests/runtime.rs deleted file mode 100644 index 2beaab0bf98..00000000000 --- a/edgelet/edgelet-kube/tests/runtime.rs +++ /dev/null @@ -1,563 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#![deny(rust_2018_idioms, warnings)] -#![deny(clippy::all, clippy::pedantic)] - -use std::path::Path; - -use config::{Config, File, FileFormat}; -use futures::future::FutureResult; -use futures::{future, Future}; -use hyper::client::{Client as HyperClient, HttpConnector}; -use hyper::{header, Body, Method, Request, Response, StatusCode}; -use json_patch::merge; -use maplit::btreemap; -use native_tls::TlsConnector; -use serde_json::{self, json, Value as JsonValue}; -use tokio::runtime::Runtime; -use typed_headers::{mime, ContentLength, ContentType, HeaderMapExt}; -use url::Url; - -use edgelet_core::{ - AuthId, Authenticator, Certificates, Connect, GetTrustBundle, Listen, MakeModuleRuntime, - ModuleSpec, Provisioning, ProvisioningResult as CoreProvisioningResult, RuntimeSettings, - WatchdogSettings, -}; -use edgelet_docker::DockerConfig; -use edgelet_kube::{ErrorKind, KubeModuleRuntime, Settings}; -use edgelet_test_utils::crypto::TestHsm; -use edgelet_test_utils::web::{ - make_req_dispatcher, HttpMethod, RequestHandler, RequestPath, ResponseFuture, -}; -use edgelet_test_utils::{get_unused_tcp_port, routes, run_tcp_server}; -use kube_client::{Client as KubeClient, Config as KubeConfig, Error, HttpClient, TokenSource}; -use provisioning::{ProvisioningResult, ReprovisioningStatus}; - -fn not_found_handler(_: Request) -> ResponseFuture { - let response = Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::default()) - .unwrap(); - - Box::new(future::ok(response)) -} - -fn make_settings(merge_json: Option) -> Settings { - let mut config = Config::default(); - let mut config_json = json!({ - "provisioning": { - "source": "manual", - "device_connection_string": "HostName=moo.azure-devices.net;DeviceId=boo;SharedAccessKey=boo" - }, - "agent": { - "name": "edgeAgent", - "type": "docker", - "env": {}, - "config": { - "image": "mcr.microsoft.com/azureiotedge-agent:1.0", - "auth": {} - } - }, - "hostname": "default1", - "connect": { - "management_uri": "http://localhost:35000", - "workload_uri": "http://localhost:35001" - }, - "listen": { - "management_uri": "http://localhost:35000", - "workload_uri": "http://localhost:35001" - }, - "homedir": "/var/lib/iotedge", - "namespace": "default", - "use_pvc": true, - "iot_hub_hostname": "iotHub", - "device_id": "device1", - "proxy_image": "proxy:latest", - "proxy_config_path": "/etc/traefik", - "proxy_config_map_name": "device1-iotedged-proxy-config", - "proxy_trust_bundle_path": "/etc/trust-bundle", - "proxy_trust_bundle_config_map_name": "device1-iotedged-proxy-trust-bundle", - "image_pull_policy": "IfNotPresent", - "service_account_name": "iotedge", - "device_hub_selector": "", - }); - - if let Some(merge_json) = merge_json { - merge(&mut config_json, &merge_json); - } - - config - .merge(File::from_str(&config_json.to_string(), FileFormat::Json)) - .unwrap(); - - config.try_into().unwrap() -} - -#[test] -fn authenticate_returns_none_when_no_auth_token_provided() { - let port = get_unused_tcp_port(); - - let dispatch_table = routes!( - POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() - ); - - let server = run_tcp_server( - "127.0.0.1", - port, - make_req_dispatcher(dispatch_table, Box::new(not_found_handler)), - ) - .map_err(|err| eprintln!("{}", err)); - - let (_, runtime) = create_runtime(&format!("http://localhost:{}", port)); - - let req = Request::default(); - - let task = runtime.authenticate(&req); - - let mut runtime = Runtime::new().unwrap(); - runtime.spawn(server); - let auth_id = runtime.block_on(task).unwrap(); - - assert_eq!(auth_id, AuthId::None) -} - -#[test] -fn authenticate_returns_none_when_invalid_auth_header_provided() { - let port = get_unused_tcp_port(); - - let dispatch_table = routes!( - POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() - ); - - let server = run_tcp_server( - "127.0.0.1", - port, - make_req_dispatcher(dispatch_table, Box::new(not_found_handler)), - ) - .map_err(|err| eprintln!("{}", err)); - - let (_, runtime) = create_runtime(&format!("http://localhost:{}", port)); - - let mut req = Request::default(); - req.headers_mut() - .insert(header::AUTHORIZATION, "BeErer token".parse().unwrap()); - - let task = runtime.authenticate(&req); - - let mut runtime = Runtime::new().unwrap(); - runtime.spawn(server); - let auth_id = runtime.block_on(task).unwrap(); - - assert_eq!(auth_id, AuthId::None) -} - -#[test] -fn authenticate_returns_none_when_invalid_auth_token_provided() { - let port = get_unused_tcp_port(); - - let dispatch_table = routes!( - POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() - ); - - let server = run_tcp_server( - "127.0.0.1", - port, - make_req_dispatcher(dispatch_table, Box::new(not_found_handler)), - ) - .map_err(|err| eprintln!("{}", err)); - - let (_, runtime) = create_runtime(&format!("http://localhost:{}", port)); - - let mut req = Request::default(); - req.headers_mut().insert( - header::AUTHORIZATION, - "\u{3aa}\u{3a9}\u{3a4}".parse().unwrap(), - ); - - let task = runtime.authenticate(&req); - - let mut runtime = Runtime::new().unwrap(); - runtime.spawn(server); - let err = runtime.block_on(task).unwrap_err(); - - assert_eq!(err.kind(), &ErrorKind::ModuleAuthenticationError); -} - -#[test] -fn authenticate_returns_none_when_unknown_auth_token_provided() { - let port = get_unused_tcp_port(); - - let dispatch_table = routes!( - POST "/apis/authentication.k8s.io/v1/tokenreviews" => unauthenticated_token_review_handler() - ); - - let server = run_tcp_server( - "127.0.0.1", - port, - make_req_dispatcher(dispatch_table, Box::new(not_found_handler)), - ) - .map_err(|err| eprintln!("{}", err)); - - let (_, runtime) = create_runtime(&format!("http://localhost:{}", port)); - - let mut req = Request::default(); - req.headers_mut().insert( - header::AUTHORIZATION, - "Bearer token-unknown".parse().unwrap(), - ); - - let task = runtime.authenticate(&req); - - let mut runtime = Runtime::new().unwrap(); - runtime.spawn(server); - let auth_id = runtime.block_on(task).unwrap(); - - assert_eq!(auth_id, AuthId::None) -} - -#[test] -fn authenticate_returns_none_when_module_auth_token_provided_but_service_account_does_not_exists() { - let port = get_unused_tcp_port(); - - let (_, runtime) = create_runtime(&format!("http://localhost:{}", port)); - - let dispatch_table = routes!( - POST "/apis/authentication.k8s.io/v1/tokenreviews" => authenticated_token_review_handler(), - ); - - let server = run_tcp_server( - "127.0.0.1", - port, - make_req_dispatcher(dispatch_table, Box::new(not_found_handler)), - ) - .map_err(|err| eprintln!("{}", err)); - - let mut req = Request::default(); - req.headers_mut() - .insert(header::AUTHORIZATION, "Bearer token".parse().unwrap()); - - let task = runtime.authenticate(&req); - - let mut runtime = Runtime::new().unwrap(); - runtime.spawn(server); - let err = runtime.block_on(task).unwrap_err(); - - assert_eq!(err.kind(), &ErrorKind::KubeClient); -} - -#[test] -fn authenticate_returns_sa_name_when_module_auth_token_provided_but_service_account_does_not_contain_original_name( -) { - let port = get_unused_tcp_port(); - - let (settings, runtime) = create_runtime(&format!("http://localhost:{}", port)); - - let dispatch_table = routes!( - POST "/apis/authentication.k8s.io/v1/tokenreviews" => authenticated_token_review_handler(), - GET format!("/api/v1/namespaces/{}/serviceaccounts/edgeagent", settings.namespace()) => get_service_account_without_annotations_handler(), - ); - - let server = run_tcp_server( - "127.0.0.1", - port, - make_req_dispatcher(dispatch_table, Box::new(not_found_handler)), - ) - .map_err(|err| eprintln!("{}", err)); - - let mut req = Request::default(); - req.headers_mut() - .insert(header::AUTHORIZATION, "Bearer token".parse().unwrap()); - - let task = runtime.authenticate(&req); - - let mut runtime = Runtime::new().unwrap(); - runtime.spawn(server); - let auth_id = runtime.block_on(task).unwrap(); - - assert_eq!(auth_id, AuthId::Value("edgeagent".into())); -} - -#[test] -fn authenticate_returns_auth_id_when_module_auth_token_provided() { - let port = get_unused_tcp_port(); - - let (settings, runtime) = create_runtime(&format!("http://localhost:{}", port)); - - let dispatch_table = routes!( - POST "/apis/authentication.k8s.io/v1/tokenreviews" => authenticated_token_review_handler(), - GET format!("/api/v1/namespaces/{}/serviceaccounts/edgeagent", settings.namespace()) => get_service_account_with_annotations_handler(), - ); - - let server = run_tcp_server( - "127.0.0.1", - port, - make_req_dispatcher(dispatch_table, Box::new(not_found_handler)), - ) - .map_err(|err| eprintln!("{}", err)); - - let mut req = Request::default(); - req.headers_mut() - .insert(header::AUTHORIZATION, "Bearer token".parse().unwrap()); - - let task = runtime.authenticate(&req); - - let mut runtime = Runtime::new().unwrap(); - runtime.spawn(server); - let auth_id = runtime.block_on(task).unwrap(); - - assert_eq!(auth_id, AuthId::Value("$edgeAgent".into())); -} - -#[derive(Clone)] -struct TestKubeSettings { - kube_settings: Settings, - api_server: Url, -} - -impl TestKubeSettings { - fn new(kube_settings: Settings, api_server: Url) -> Self { - Self { - kube_settings, - api_server, - } - } - - fn into_kube_settings(self) -> Settings { - self.kube_settings - } - - fn api_server(&self) -> &Url { - &self.api_server - } - - fn with_device_id(mut self, device_id: &str) -> Self { - self.kube_settings = self.kube_settings.with_device_id(device_id); - self - } - - fn with_iot_hub_hostname(mut self, iot_hub_hostname: &str) -> Self { - self.kube_settings = self.kube_settings.with_iot_hub_hostname(iot_hub_hostname); - self - } - - fn namespace(&self) -> &str { - self.kube_settings.namespace() - } -} - -impl RuntimeSettings for TestKubeSettings { - type Config = DockerConfig; - - fn provisioning(&self) -> &Provisioning { - self.kube_settings.provisioning() - } - - fn agent(&self) -> &ModuleSpec { - self.kube_settings.agent() - } - - fn agent_mut(&mut self) -> &mut ModuleSpec { - self.kube_settings.agent_mut() - } - - fn hostname(&self) -> &str { - self.kube_settings.hostname() - } - - fn connect(&self) -> &Connect { - self.kube_settings.connect() - } - - fn listen(&self) -> &Listen { - self.kube_settings.listen() - } - - fn homedir(&self) -> &Path { - self.kube_settings.homedir() - } - - fn certificates(&self) -> Option<&Certificates> { - self.kube_settings.certificates() - } - - fn watchdog(&self) -> &WatchdogSettings { - self.kube_settings.watchdog() - } -} - -struct TestKubeModuleRuntime(KubeModuleRuntime>); - -impl MakeModuleRuntime for TestKubeModuleRuntime { - type Config = DockerConfig; - type Settings = TestKubeSettings; - type ProvisioningResult = ProvisioningResult; - type ModuleRuntime = KubeModuleRuntime>; - type Error = Error; - type Future = FutureResult; - - fn make_runtime( - settings: Self::Settings, - provisioning_result: Self::ProvisioningResult, - _: impl GetTrustBundle, - ) -> Self::Future { - let settings = settings - .with_device_id(provisioning_result.device_id()) - .with_iot_hub_hostname(provisioning_result.hub_name()); - - future::ok(KubeModuleRuntime::new( - KubeClient::with_client( - get_config(settings.api_server()), - HttpClient(HyperClient::new()), - ), - settings.into_kube_settings(), - )) - } -} - -fn create_runtime( - url: &str, -) -> ( - TestKubeSettings, - KubeModuleRuntime>, -) { - let provisioning_result = ProvisioningResult::new( - "my_device_id", - "iothostname", - None, - ReprovisioningStatus::DeviceDataNotUpdated, - None, - ); - let settings = TestKubeSettings::new(make_settings(None), url.parse().unwrap()); - let runtime = TestKubeModuleRuntime::make_runtime( - settings.clone(), - provisioning_result, - TestHsm::default(), - ) - .wait() - .unwrap(); - - (settings, runtime) -} - -fn get_config(api_server: &Url) -> KubeConfig { - KubeConfig::new( - api_server.clone(), - "/api".to_string(), - TestTokenSource, - TlsConnector::new().unwrap(), - ) -} - -fn authenticated_token_review_handler() -> impl Fn(Request) -> ResponseFuture + Clone { - make_token_review_handler(|| { - json!({ - "kind": "TokenReview", - "spec": { "token": "token" }, - "status": { - "authenticated": true, - "user": { - "username": "system:serviceaccount:my-namespace:edgeagent" - } - }} - ) - .to_string() - }) -} - -fn unauthenticated_token_review_handler() -> impl Fn(Request) -> ResponseFuture + Clone { - make_token_review_handler(|| { - json!({ - "kind": "TokenReview", - "spec": { "token": "token" }, - "status": { - "authenticated": false, - }} - ) - .to_string() - }) -} - -fn make_token_review_handler( - on_token_review: impl Fn() -> String + Clone + Send + 'static, -) -> impl Fn(Request) -> ResponseFuture + Clone { - move |_| { - let response = on_token_review(); - let response_len = response.len(); - - let mut response = Response::new(response.into()); - response - .headers_mut() - .typed_insert(&ContentLength(response_len as u64)); - response - .headers_mut() - .typed_insert(&ContentType(mime::APPLICATION_JSON)); - Box::new(future::ok(response)) as ResponseFuture - } -} - -fn get_service_account_with_annotations_handler() -> impl Fn(Request) -> ResponseFuture + Clone -{ - move |_| { - response(StatusCode::OK, || { - json!({ - "kind": "ServiceAccount", - "apiVersion": "v1", - "metadata": { - "name": "edgeagent", - "namespace": "my-namespace", - "annotations": { - "net.azure-devices.edge.original-moduleid": "$edgeAgent" - } - } - }) - .to_string() - }) - } -} - -fn get_service_account_without_annotations_handler( -) -> impl Fn(Request) -> ResponseFuture + Clone { - move |_| { - response(StatusCode::OK, || { - json!({ - "kind": "ServiceAccount", - "apiVersion": "v1", - "metadata": { - "name": "edgeagent", - "namespace": "my-namespace", - } - }) - .to_string() - }) - } -} - -fn response( - status_code: StatusCode, - response: impl Fn() -> String + Clone + Send + 'static, -) -> ResponseFuture { - let response = response(); - let response_len = response.len(); - - let mut response = Response::new(response.into()); - *response.status_mut() = status_code; - response - .headers_mut() - .typed_insert(&ContentLength(response_len as u64)); - response - .headers_mut() - .typed_insert(&ContentType(mime::APPLICATION_JSON)); - - Box::new(future::ok(response)) as ResponseFuture -} - -#[derive(Clone)] -struct TestTokenSource; - -impl TokenSource for TestTokenSource { - type Error = Error; - - fn get(&self) -> kube_client::error::Result> { - Ok(None) - } -}