From 15ebabb633306a2b9538fa0ddea7bfece450b3bb Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Tue, 10 Oct 2023 16:49:42 +0100 Subject: [PATCH 1/4] Add reference documentation for c8y_auth_proxy Signed-off-by: James Rhodes --- .../extensions/c8y_auth_proxy/src/server.rs | 72 ++++++++++++++++++- docs/src/references/tedge-cumulocity-proxy.md | 27 +++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 docs/src/references/tedge-cumulocity-proxy.md diff --git a/crates/extensions/c8y_auth_proxy/src/server.rs b/crates/extensions/c8y_auth_proxy/src/server.rs index 920830e7c24..96a01b871cc 100644 --- a/crates/extensions/c8y_auth_proxy/src/server.rs +++ b/crates/extensions/c8y_auth_proxy/src/server.rs @@ -138,9 +138,23 @@ async fn respond_to( let client = reqwest::Client::new(); let (body, body_clone) = small_body.try_clone(); + if body_clone.is_none() { + let destination = format!("{host}/tenant/currentTenant"); + let response = client + .head(&destination) + .bearer_auth(&token) + .send() + .await + .into_diagnostic() + .wrap_err_with(|| format!("making HEAD request to {destination}"))?; + if response.status() == StatusCode::UNAUTHORIZED { + token = retrieve_token.not_matching(Some(&token)).await; + } + } + let send_request = |body, token| { client - .request(method.to_owned(), destination.clone()) + .request(method.to_owned(), &destination) .headers(headers.clone()) .bearer_auth(&token) .body(body) @@ -231,6 +245,18 @@ mod tests { assert_eq!(res.status(), 404); } + #[tokio::test] + async fn responds_with_bad_gateway_on_connection_error() { + let _ = env_logger::try_init(); + + let port = start_server_at_url(Arc::from("127.0.0.1:0"), vec!["test-token"]); + + let res = reqwest::get(format!("http://localhost:{port}/c8y/not-a-known-url")) + .await + .unwrap(); + assert_eq!(res.status(), 502); + } + #[tokio::test] async fn sends_query_string_from_original_request() { let _ = env_logger::try_init(); @@ -284,6 +310,41 @@ mod tests { assert_eq!(res.bytes().await.unwrap(), Bytes::from("Some response")); } + #[tokio::test] + async fn regenerates_token_proactively_if_the_request_cannot_be_retried() { + let _ = env_logger::try_init(); + let mut server = mockito::Server::new(); + let head_request = server + .mock("HEAD", "/tenant/currentTenant") + .match_header("Authorization", "Bearer old-token") + .with_status(401) + .create(); + let _mock = server + .mock("PUT", "/hello") + .match_header("Authorization", "Bearer test-token") + .match_body("A body") + .with_body("Some response") + .with_status(200) + .create(); + + let port = start_server(&server, vec!["old-token", "test-token"]); + + let client = reqwest::Client::new(); + let body = "A body"; + let res = client + .put(format!("http://localhost:{port}/c8y/hello")) + .body(reqwest::Body::wrap_stream(futures::stream::once( + futures::future::ready(Ok::<_, std::convert::Infallible>(body)), + ))) + .send() + .await + .unwrap(); + + head_request.assert(); + assert_eq!(res.status(), 200); + assert_eq!(res.bytes().await.unwrap(), Bytes::from("Some response")); + } + #[tokio::test] async fn retries_get_request_on_401() { let _ = env_logger::try_init(); @@ -311,10 +372,17 @@ mod tests { } fn start_server(server: &mockito::Server, tokens: Vec>>) -> u16 { + start_server_at_url(server.url().into(), tokens) + } + + fn start_server_at_url( + target_host: Arc, + tokens: Vec>>, + ) -> u16 { let mut retriever = IterJwtRetriever::builder(tokens); for port in 3000..3100 { let state = AppState { - target_host: server.url().into(), + target_host: target_host.clone(), token_manager: TokenManager::new(JwtRetriever::new("TEST => JWT", &mut retriever)) .shared(), }; diff --git a/docs/src/references/tedge-cumulocity-proxy.md b/docs/src/references/tedge-cumulocity-proxy.md new file mode 100644 index 00000000000..4f939a207cf --- /dev/null +++ b/docs/src/references/tedge-cumulocity-proxy.md @@ -0,0 +1,27 @@ +--- +title: Thin Edge Cumulocity HTTP Proxy +tags: [ Reference, HTTP, Cumulocity ] +sidebar_position: 12 +--- + +# Thin Edge Cumulocity Proxy + +The `tedge-mapper` (when running in `c8y` mode) hosts a proxy server to access the Cumulocity HTTP API as the thin-edge +device. It automatically handles authenticating with a JWT, avoiding the need for clients to support MQTT to retrieve +this information. + +The API can be accessed at `http://{ip}:8001/c8y/{c8y-endpoint}`. `ip` is configured using +the `c8y.proxy.bind.address` configuration (the default value is `127.0.0.1`) and the port can be changed by +setting `c8y.proxy.bind.port`. + +For example, you can access the current tenant information +at [http://127.0.0.1:8001/c8y/tenant/currentTenant](http://127.0.0.1:8001/c8y/tenant/currentTenant) from the +machine running `tedge-mapper`. The server supports all possible request methods ( +e.g. `HEAD`/`GET`/`PUT`/`PATCH`/`DELETE`). + +At the time of writing, this service is unauthenticated and does not support incoming HTTPS connections (when the +request is forwarded to Cumulocity, however, this will use HTTPS). Due to the underlying JWT handling in Cumulocity, +requests to the proxy API are occasionally spuriously rejected with a `401 Not Authorized` status code. The proxy server +currently forwards this response directly to the client, as well as all other errors responses from Cumulocity. If there +is an error connecting to Cumulocity to make the request, a plain text response with the status code `502 Bad Gateway` +will be returned. From e44677ae8f9bf33ba9a104d23fcb420582ba8b50 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 12 Oct 2023 14:20:12 +0100 Subject: [PATCH 2/4] Use authorization header if provided to auth proxy Signed-off-by: James Rhodes --- .../extensions/c8y_auth_proxy/src/server.rs | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/crates/extensions/c8y_auth_proxy/src/server.rs b/crates/extensions/c8y_auth_proxy/src/server.rs index 96a01b871cc..ed5363b355c 100644 --- a/crates/extensions/c8y_auth_proxy/src/server.rs +++ b/crates/extensions/c8y_auth_proxy/src/server.rs @@ -122,6 +122,12 @@ async fn respond_to( Some(Path(p)) => p.as_str(), None => "", }; + let auth: fn(reqwest::RequestBuilder, &str) -> reqwest::RequestBuilder = + if headers.contains_key("Authorization") { + |req, _token| req + } else { + |req, token| req.bearer_auth(token) + }; // Cumulocity revokes the device token if we access parts of the frontend UI, // so deny requests to these proactively @@ -152,15 +158,17 @@ async fn respond_to( } } - let send_request = |body, token| { - client - .request(method.to_owned(), &destination) - .headers(headers.clone()) - .bearer_auth(&token) - .body(body) - .send() + let send_request = |body, token: &str| { + auth( + client + .request(method.to_owned(), &destination) + .headers(headers.clone()), + token, + ) + .body(body) + .send() }; - let mut res = send_request(body, token.clone()) + let mut res = send_request(body, &token) .await .into_diagnostic() .wrap_err_with(|| format!("making proxied request to {destination}"))?; @@ -168,7 +176,7 @@ async fn respond_to( if res.status() == StatusCode::UNAUTHORIZED { token = retrieve_token.not_matching(Some(&token)).await; if let Some(body) = body_clone { - res = send_request(Body::from(body), token.clone()) + res = send_request(Body::from(body), &token) .await .into_diagnostic() .wrap_err_with(|| format!("making proxied request to {destination}"))?; @@ -277,6 +285,30 @@ mod tests { assert_eq!(res.status(), 200); } + #[tokio::test] + async fn uses_authorization_header_passed_by_user_if_one_is_provided() { + let _ = env_logger::try_init(); + let mut server = mockito::Server::new(); + let _mock = server + .mock("GET", "/inventory/managedObjects") + .match_header("authorization", "Basic dGVzdDp0ZXN0") + .with_status(200) + .create(); + + let port = start_server(&server, vec!["test-token"]); + + let client = reqwest::Client::new(); + let res = client + .get(format!( + "http://localhost:{port}/c8y/inventory/managedObjects" + )) + .basic_auth("test", Some("test")) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + } + #[tokio::test] async fn retries_requests_with_small_bodies() { let _ = env_logger::try_init(); From 112518a1a915ec90d9533f30e8d96c1ab75aa76b Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 12 Oct 2023 14:21:54 +0100 Subject: [PATCH 3/4] Reformat docs and clarify auth header Signed-off-by: James Rhodes --- docs/src/references/tedge-cumulocity-proxy.md | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/src/references/tedge-cumulocity-proxy.md b/docs/src/references/tedge-cumulocity-proxy.md index 4f939a207cf..c3f5aa38ec8 100644 --- a/docs/src/references/tedge-cumulocity-proxy.md +++ b/docs/src/references/tedge-cumulocity-proxy.md @@ -6,22 +6,27 @@ sidebar_position: 12 # Thin Edge Cumulocity Proxy -The `tedge-mapper` (when running in `c8y` mode) hosts a proxy server to access the Cumulocity HTTP API as the thin-edge -device. It automatically handles authenticating with a JWT, avoiding the need for clients to support MQTT to retrieve -this information. +The `tedge-mapper` (when running in `c8y` mode) hosts a proxy server to access the Cumulocity HTTP API from the +thin-edge device. +It automatically handles authenticating with a JWT, avoiding the need for clients to support MQTT to retrieve this +information. -The API can be accessed at `http://{ip}:8001/c8y/{c8y-endpoint}`. `ip` is configured using -the `c8y.proxy.bind.address` configuration (the default value is `127.0.0.1`) and the port can be changed by -setting `c8y.proxy.bind.port`. +The API can be accessed at `http://{ip}:8001/c8y/{c8y-endpoint}`. `ip` is configured using the `c8y.proxy.bind.address` +configuration +(the default value is `127.0.0.1`) and the port can be changed by setting `c8y.proxy.bind.port`. For example, you can access the current tenant information -at [http://127.0.0.1:8001/c8y/tenant/currentTenant](http://127.0.0.1:8001/c8y/tenant/currentTenant) from the -machine running `tedge-mapper`. The server supports all possible request methods ( +at [http://127.0.0.1:8001/c8y/tenant/currentTenant](http://127.0.0.1:8001/c8y/tenant/currentTenant) +from the machine running `tedge-mapper`. The server supports all possible request methods ( e.g. `HEAD`/`GET`/`PUT`/`PATCH`/`DELETE`). +There is no need to provide an `Authorization` header (or any other authentication method) when accessing the API. +If an `Authorization` header is provided, this will be used to authenticate the request instead of the device JWT. -At the time of writing, this service is unauthenticated and does not support incoming HTTPS connections (when the -request is forwarded to Cumulocity, however, this will use HTTPS). Due to the underlying JWT handling in Cumulocity, -requests to the proxy API are occasionally spuriously rejected with a `401 Not Authorized` status code. The proxy server -currently forwards this response directly to the client, as well as all other errors responses from Cumulocity. If there -is an error connecting to Cumulocity to make the request, a plain text response with the status code `502 Bad Gateway` -will be returned. +At the time of writing, this service is unauthenticated and does not support incoming HTTPS connections +(when the request is forwarded to Cumulocity, however, this will use HTTPS). +Due to the underlying JWT handling in Cumulocity, requests to the proxy API are occasionally spuriously rejected with +a `401 Not Authorized` status code. +The proxy server currently forwards this response directly to the client, as well as all other errors responses from +Cumulocity. +If there is an error connecting to Cumulocity to make the request, a plain text response with the status +code `502 Bad Gateway` will be returned. From cfba699ad662d926adfb3c38d39bff0ff6b5e86e Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 13 Oct 2023 14:46:06 +0100 Subject: [PATCH 4/4] Clarify that all APIs are supported Signed-off-by: James Rhodes --- docs/src/references/tedge-cumulocity-proxy.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/references/tedge-cumulocity-proxy.md b/docs/src/references/tedge-cumulocity-proxy.md index c3f5aa38ec8..95e2029b3f4 100644 --- a/docs/src/references/tedge-cumulocity-proxy.md +++ b/docs/src/references/tedge-cumulocity-proxy.md @@ -17,8 +17,9 @@ configuration For example, you can access the current tenant information at [http://127.0.0.1:8001/c8y/tenant/currentTenant](http://127.0.0.1:8001/c8y/tenant/currentTenant) -from the machine running `tedge-mapper`. The server supports all possible request methods ( -e.g. `HEAD`/`GET`/`PUT`/`PATCH`/`DELETE`). +from the machine running `tedge-mapper`. +The server supports all public REST APIs of Cumulocity, and all possible request methods +(e.g. `HEAD`/`GET`/`PUT`/`PATCH`/`DELETE`). There is no need to provide an `Authorization` header (or any other authentication method) when accessing the API. If an `Authorization` header is provided, this will be used to authenticate the request instead of the device JWT.