From fba9e4da52c03ff17b90e35ac3e9e4f49409fbf2 Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Fri, 14 Feb 2025 17:17:03 +0100 Subject: [PATCH 1/2] feat(rust): node control openapi documentation --- .../rust/ockam/ockam_api/Cargo.toml | 2 +- .../control_api/backend/authority_member.rs | 49 ++++++---- .../src/control_api/backend/common.rs | 2 +- .../src/control_api/backend/entrypoint.rs | 20 ++-- .../src/control_api/backend/inlet.rs | 77 ++++++++------- .../src/control_api/backend/outlet.rs | 77 ++++++++------- .../src/control_api/backend/relay.rs | 51 ++++++---- .../src/control_api/backend/ticket.rs | 67 +++++++++---- .../ockam_api/src/control_api/frontend.rs | 26 ++--- .../ockam_api/src/control_api/openapi.rs | 94 ++++++++++++++++++- .../control_api/protocol/authority_member.rs | 16 +--- .../src/control_api/protocol/common.rs | 27 ++++++ .../src/control_api/protocol/inlet.rs | 11 ++- .../src/control_api/protocol/outlet.rs | 8 +- .../src/control_api/protocol/ticket.rs | 14 +-- ...control_api.bats => node_control_api.bats} | 64 ++++++------- 16 files changed, 396 insertions(+), 209 deletions(-) rename implementations/rust/ockam/ockam_command/tests/bats/orchestrator/{control_api.bats => node_control_api.bats} (88%) diff --git a/implementations/rust/ockam/ockam_api/Cargo.toml b/implementations/rust/ockam/ockam_api/Cargo.toml index 106d590b692..747a5ca315f 100644 --- a/implementations/rust/ockam/ockam_api/Cargo.toml +++ b/implementations/rust/ockam/ockam_api/Cargo.toml @@ -120,7 +120,7 @@ tracing-error = "0.2.0" tracing-opentelemetry = "0.27.0" tracing-subscriber = { version = "0.3", features = ["json"] } url = "2.5.2" -utoipa = { version = "^5.3", features = ["yaml"] } +utoipa = { version = "^5.3", features = ["yaml", "openapi_extensions"] } ockam_multiaddr = { path = "../ockam_multiaddr", version = "0.69.0", features = ["cbor", "serde"] } ockam_transport_core = { path = "../ockam_transport_core", version = "^0.101.0" } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs index ef9fbfe3568..1f4c79cfb20 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs @@ -7,6 +7,7 @@ use crate::control_api::protocol::authority_member::{ AddOrUpdateAuthorityMemberRequest, AuthorityMember, GetAuthorityMemberRequest, ListAuthorityMembersRequest, RemoveAuthorityMemberRequest, }; +use crate::control_api::protocol::common::{Attributes, ErrorResponse, NodeName}; use crate::control_api::ControlApiError; use crate::nodes::NodeManager; use http::StatusCode; @@ -21,7 +22,7 @@ impl HttpControlNodeApiBackend { resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "authority-member"; + let resource_name = "authority-members"; let resource_name_identifier = "authority_member_identity"; match method { "PUT" => match resource_id { @@ -61,13 +62,17 @@ impl HttpControlNodeApiBackend { put, operation_id = "add_or_update_authority_member", summary = "Add or update an Authority Member", - path = "/{node}/authority-member/{member}", - tags = ["authority-member"], + description = +"Add or update an Authority Member with the specified attributes. +Attributes will overwrite the existing ones if the member already exists.", + path = "/{node}/authority-members/{member}", + tags = ["Authority Members"], responses( (status = CREATED, description = "Successfully created"), + (status = NOT_FOUND, description = "Specified project not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ("member" = String, description = "Member identity", example = "Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a"), ), request_body( @@ -90,7 +95,7 @@ async fn handle_authority_member_add_or_update( create_authority_client(node_manager, &request.authority, &request.identity).await?; let result = authority_client - .add_member(context, member_identity, request.attributes) + .add_member(context, member_identity, request.attributes.0) .await; match result { Ok(_) => Ok(ControlApiHttpResponse::without_body(StatusCode::CREATED)?), @@ -105,18 +110,20 @@ async fn handle_authority_member_add_or_update( get, operation_id = "list_authority_members", summary = "List Authority Members", - path = "/{node}/authority-member", - tags = ["authority-member"], + description = "List all members of the Authority.", + path = "/{node}/authority-members", + tags = ["Authority Members"], responses( (status = OK, description = "Successfully retrieved", body = Vec), + (status = NOT_FOUND, description = "Specified project not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ), request_body( content = ListAuthorityMembersRequest, content_type = "application/json", - description = "Creation request" + description = "Optional list request" ) )] async fn handle_authority_member_list( @@ -136,7 +143,7 @@ async fn handle_authority_member_list( .into_iter() .map(|(identity, attributes_entry)| AuthorityMember { identity: identity.to_string(), - attributes: attributes_entry.string_attributes(), + attributes: Attributes(attributes_entry.string_attributes()), }) .collect(); Ok(ControlApiHttpResponse::with_body(StatusCode::OK, members)?) @@ -152,19 +159,21 @@ async fn handle_authority_member_list( get, operation_id = "get_authority_member", summary = "Get Authority Member", - path = "/{node}/authority-member/{member}", - tags = ["authority-member"], + description = "Get the specified member of the Authority by identity.", + path = "/{node}/authority-members/{member}", + tags = ["Authority Members"], responses( (status = OK, description = "Successfully retrieved", body = AuthorityMember), + (status = NOT_FOUND, description = "Specified project not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ("member" = String, description = "Member identity", example = "Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a"), ), request_body( content = GetAuthorityMemberRequest, content_type = "application/json", - description = "Get member request" + description = "Optional get member request" ) )] async fn handle_authority_member_get( @@ -188,7 +197,7 @@ async fn handle_authority_member_get( StatusCode::OK, AuthorityMember { identity: member_identity.to_string(), - attributes: attributes_entry.string_attributes(), + attributes: Attributes(attributes_entry.string_attributes()), }, )?), Err(error) => { @@ -203,19 +212,21 @@ async fn handle_authority_member_get( delete, operation_id = "remove_authority_member", summary = "Remove an Authority Member", - path = "/{node}/authority-member/{member}", - tags = ["authority-member"], + description = "Remove the specified member of the Authority by identity.", + path = "/{node}/authority-members/{member}", + tags = ["Authority Members"], responses( (status = OK, description = "Successfully removed"), + (status = NOT_FOUND, description = "Specified project not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ("member" = String, description = "Member identity", example = "Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a"), ), request_body( content = RemoveAuthorityMemberRequest, content_type = "application/json", - description = "Remove member request" + description = "Optional remove member request" ) )] async fn handle_authority_member_remove( diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs index d9f250cd518..11a60d28c11 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs @@ -61,7 +61,7 @@ pub async fn create_authority_client( } Err(error) => { warn!("No default project: {error:?}"); - return ControlApiHttpResponse::bad_request("No default project"); + return ControlApiHttpResponse::not_found("Default default project not found"); } } } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs index 6fe7151d0cc..e21b930a93d 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs @@ -69,23 +69,23 @@ impl Worker for HttpControlNodeApiBackend { let resource_id = path.get(3).copied(); let result: Result = match resource_kind.as_str() { - "tcp-inlet" => { + "tcp-inlets" => { self.handle_tcp_inlet(context, request.method.as_str(), resource_id, request.body) .await } - "tcp-outlet" => { + "tcp-outlets" => { self.handle_tcp_outlet(context, request.method.as_str(), resource_id, request.body) .await } - "relay" => { + "relays" => { self.handle_relay(context, request.method.as_str(), resource_id, request.body) .await } - "ticket" => Ok(self + "tickets" => Ok(self .handle_ticket(context, request.method.as_str(), resource_id, request.body) .await .unwrap()), - "authority-member" => { + "authority-members" => { self.handle_authority_member( context, request.method.as_str(), @@ -97,11 +97,11 @@ impl Worker for HttpControlNodeApiBackend { _ => { warn!("Invalid resource kind: {resource_kind}"); let valid_resources = [ - "tcp-inlet", - "tcp-outlet", - "relay", - "ticket", - "authority-member", + "tcp-inlets", + "tcp-outlets", + "relays", + "tickets", + "authority-members", ]; let message = format!( "Invalid resource kind: {resource_kind}. Possible: {}", diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs index 806ba5e37f5..bd638c6d718 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs @@ -1,6 +1,7 @@ use crate::control_api::backend::common; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; +use crate::control_api::protocol::common::{ErrorResponse, NodeName}; use crate::control_api::protocol::inlet::{CreateInletRequest, InletKind, InletTls}; use crate::control_api::protocol::inlet::{InletStatus, UpdateInletRequest}; use crate::control_api::ControlApiError; @@ -21,10 +22,10 @@ impl HttpControlNodeApiBackend { resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "tcp-inlet"; + let resource_name = "tcp-inlets"; let resource_name_identifier = "tcp_inlet_name"; match method { - "PUT" => handle_tcp_inlet_create(context, &self.node_manager, body).await, + "POST" => handle_tcp_inlet_create(context, &self.node_manager, body).await, "GET" => match resource_id { None => handle_tcp_inlet_list(&self.node_manager).await, Some(id) => handle_tcp_inlet_get(&self.node_manager, id).await, @@ -47,7 +48,7 @@ impl HttpControlNodeApiBackend { warn!("Invalid method: {method}"); ControlApiHttpResponse::invalid_method( method, - vec!["PUT", "GET", "PATCH", "DELETE"], + vec!["POST", "GET", "PATCH", "DELETE"], ) } } @@ -55,16 +56,21 @@ impl HttpControlNodeApiBackend { } #[utoipa::path( - put, + post, operation_id = "create_tcp_inlet", summary = "Create a new TCP Inlet", - path = "/{node}/tcp-inlet", - tags = ["portal", "tcp-inlet"], + description = +"Create a TCP Inlet, the main parameters are the destination `to`, and the bind address `from`. +You can also choose to the listen with a valid TLS certificate, restrict access to the Inlet with +`authorized` and `allow`, and select a specialized Portals with `kind`. +The creation will be asynchronous and the initial status will be `down`.", + path = "/{node}/tcp-inlets", + tags = ["Portals"], responses( (status = CREATED, description = "Successfully created", body = InletStatus), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ), request_body( content = CreateInletRequest, @@ -194,15 +200,19 @@ async fn handle_tcp_inlet_create( patch, operation_id = "update_tcp_inlet", summary = "Update a TCP Inlet", - path = "/{node}/tcp-inlet/{resource_id}", - tags = ["portal", "tcp-inlet"], + description = +"Update the specified TCP Inlet by name. +Currently only `allow` policy expression can be updated, for more advanced updates it's necessary +to delete the TCP Inlet and create a new one.", + path = "/{node}/tcp-inlets/{tcp_inlet_name}", + tags = ["Portals"], responses( (status = OK, description = "Successfully updated", body = InletStatus), - (status = NOT_FOUND, description = "Not found"), + (status = NOT_FOUND, description = "TCP Inlet not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Resource ID") + ("node" = NodeName,), + ("tcp_inlet_name" = String, description = "TCP Inlet name"), ), request_body( content = UpdateInletRequest, @@ -218,7 +228,7 @@ async fn handle_tcp_inlet_update( let request: UpdateInletRequest = common::parse_request_body(body)?; if node_manager.show_inlet(resource_id).await.is_none() { - return ControlApiHttpResponse::not_found("Inlet not found"); + return ControlApiHttpResponse::not_found("TCP Inlet not found"); } if let Some(allow) = request.allow { @@ -247,13 +257,14 @@ async fn handle_tcp_inlet_update( get, operation_id = "list_tcp_inlet", summary = "List all TCP Inlets", - path = "/{node}/tcp-inlet", - tags = ["portal", "tcp-inlet"], + description = "List all TCP Inlets created in the node regardless of their status.", + path = "/{node}/tcp-inlets", + tags = ["Portals"], responses( (status = OK, description = "Successfully listed", body = Vec), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ) )] async fn handle_tcp_inlet_list( @@ -270,14 +281,15 @@ async fn handle_tcp_inlet_list( delete, operation_id = "delete_tcp_inlet", summary = "Delete a TCP Inlet", - path = "/{node}/tcp-inlet/{resource_id}", - tags = ["portal", "tcp-inlet"], + description = "Delete the specified TCP Inlet by name.", + path = "/{node}/tcp-inlets/{tcp_inlet_name}", + tags = ["Portals"], responses( (status = NO_CONTENT, description = "Successfully deleted"), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Resource ID") + ("node" = NodeName,), + ("tcp_inlet_name" = String, description = "TCP Inlet name"), ) )] async fn handle_tcp_inlet_delete( @@ -300,15 +312,16 @@ async fn handle_tcp_inlet_delete( get, operation_id = "get_tcp_inlet", summary = "Get a TCP Inlet", - path = "/{node}/tcp-inlet/{resource_id}", - tags = ["portal", "tcp-inlet"], + description = "Get the specified TCP Inlet by name", + path = "/{node}/tcp-inlets/{tcp_inlet_name}", + tags = ["Portals"], responses( (status = OK, description = "Successfully retrieved", body = InletStatus), - (status = NOT_FOUND, description = "Resource not found"), + (status = NOT_FOUND, description = "TCP Inlet not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Resource ID") + ("node" = NodeName,), + ("tcp_inlet_name" = String, description = "TCP Inlet name") ) )] async fn handle_tcp_inlet_get( @@ -345,8 +358,8 @@ mod test { .create_control_api_backend(context, None)?; let request = ControlApiHttpRequest { - method: "PUT".to_string(), - uri: "/node-name/tcp-inlet".to_string(), + method: "POST".to_string(), + uri: "/node-name/tcp-inlets".to_string(), body: Some( serde_json::to_vec(&CreateInletRequest { name: Some("inlet-name".to_string()), @@ -388,7 +401,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-inlet/inlet-name".to_string(), + uri: "/node-name/tcp-inlets/inlet-name".to_string(), body: None, }; @@ -409,7 +422,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-inlet".to_string(), + uri: "/node-name/tcp-inlets".to_string(), body: None, }; @@ -431,7 +444,7 @@ mod test { let request = ControlApiHttpRequest { method: "DELETE".to_string(), - uri: "/node-name/tcp-inlet/inlet-name".to_string(), + uri: "/node-name/tcp-inlets/inlet-name".to_string(), body: None, }; @@ -446,7 +459,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-inlet/inlet-name".to_string(), + uri: "/node-name/tcp-inlets/inlet-name".to_string(), body: None, }; @@ -462,7 +475,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-inlet".to_string(), + uri: "/node-name/tcp-inlets".to_string(), body: None, }; diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs index 440c832cd3e..5afa2a10524 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs @@ -1,7 +1,7 @@ use crate::control_api::backend::common; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; -use crate::control_api::protocol::common::ErrorResponse; +use crate::control_api::protocol::common::{ErrorResponse, NodeName}; use crate::control_api::protocol::outlet::{ CreateOutletRequest, OutletKind, OutletStatus, OutletTls, UpdateOutletRequest, }; @@ -23,10 +23,10 @@ impl HttpControlNodeApiBackend { resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "tcp-outlet"; + let resource_name = "tcp-outlets"; let resource_name_identifier = "tcp_outlet_name"; match method { - "PUT" => handle_tcp_outlet_create(context, &self.node_manager, body).await, + "POST" => handle_tcp_outlet_create(context, &self.node_manager, body).await, "GET" => match resource_id { None => handle_tcp_outlet_list(&self.node_manager).await, Some(id) => handle_tcp_outlet_get(&self.node_manager, id).await, @@ -49,7 +49,7 @@ impl HttpControlNodeApiBackend { warn!("Invalid method: {method}"); ControlApiHttpResponse::invalid_method( method, - vec!["PUT", "GET", "PATCH", "DELETE"], + vec!["POST", "GET", "PATCH", "DELETE"], ) } } @@ -57,17 +57,23 @@ impl HttpControlNodeApiBackend { } #[utoipa::path( - put, + post, operation_id = "create_tcp_outlet", summary = "Create a TCP Outlet", - path = "/{node}/tcp-outlet", - tags = ["portal", "tcp-outlet"], + description = +"Create a new TCP Outlet, the main parameter are the destination `to`, and the worker address +`address` which is used to identify the outlet within the node. +The `kind` parameter can be used to create a special outlet, and the `tls` parameter can be used to +connect to TLS endpoints. +The creation will be synchronous, without any blocking operation.", + path = "/{node}/tcp-outlets", + tags = ["Portals"], responses( (status = CREATED, description = "Successfully created", body = OutletStatus), (status = CONFLICT, description = "Already exists", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ), request_body( content = CreateOutletRequest, @@ -133,15 +139,19 @@ async fn handle_tcp_outlet_create( patch, operation_id = "update_tcp_outlet", summary = "Update a TCP Outlet", - path = "/{node}/tcp-outlet/{resource_id}", - tags = ["portal", "tcp-outlet"], + description = +"Update the specified TCP Outlet by address. +Currently only `allow` policy expression can be updated, for more advanced updates it's necessary +to delete the TCP Outlet and create a new one.", + path = "/{node}/tcp-outlets/{tcp_outlet_address}", + tags = ["Portals"], responses( (status = OK, description = "Successfully updated", body = OutletStatus), - (status = NOT_FOUND, description = "Not found"), + (status = NOT_FOUND, description = "TCP Outlet not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Resource ID") + ("node" = NodeName,), + ("tcp_outlet_address" = String, description = "TCP Outlet address"), ), request_body( content = UpdateOutletRequest, @@ -186,8 +196,9 @@ async fn handle_tcp_outlet_update( get, operation_id = "list_tcp_outlets", summary = "List all TCP Outlets", - path = "/{node}/tcp-outlet", - tags = ["portal", "tcp-outlet"], + description = "List all TCP Outlets created in the node.", + path = "/{node}/tcp-outlets", + tags = ["Portals"], responses( (status = OK, description = "Successfully listed", body = Vec), ), @@ -210,15 +221,16 @@ async fn handle_tcp_outlet_list( get, operation_id = "get_tcp_outlet", summary = "Get a TCP Outlet", - path = "/{node}/tcp-outlet/{resource_id}", - tags = ["portal", "tcp-outlet"], + description = "Get the specified TCP Outlet by address.", + path = "/{node}/tcp-outlets/{tcp_outlet_address}", + tags = ["Portals"], responses( (status = OK, description = "Successfully retrieved", body = OutletStatus), - (status = NOT_FOUND, description = "Not found"), + (status = NOT_FOUND, description = "TCP Outlet not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Outlet address"), + ("node" = NodeName,), + ("tcp_outlet_address" = String, description = "TCP Outlet address"), ) )] async fn handle_tcp_outlet_get( @@ -239,15 +251,16 @@ async fn handle_tcp_outlet_get( delete, operation_id = "delete_tcp_outlet", summary = "Delete a TCP Outlet", - path = "/{node}/tcp-outlet/{resource_id}", - tags = ["portal", "tcp-outlet"], + description = "Delete the specified TCP Outlet by address.", + path = "/{node}/tcp-outlets/{tcp_outlet_address}", + tags = ["Portals"], responses( (status = NO_CONTENT, description = "Successfully deleted"), - (status = NOT_FOUND, description = "Not found"), + (status = NOT_FOUND, description = "TCP Outlet not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Outlet address"), + ("node" = NodeName,), + ("tcp_outlet_address" = String, description = "TCP Outlet address"), ) )] async fn handle_tcp_outlet_delete( @@ -284,8 +297,8 @@ mod test { .create_control_api_backend(context, None)?; let request = ControlApiHttpRequest { - method: "PUT".to_string(), - uri: "/node-name/tcp-outlet".to_string(), + method: "POST".to_string(), + uri: "/node-name/tcp-outlets".to_string(), body: Some( serde_json::to_vec(&CreateOutletRequest { kind: OutletKind::Regular, @@ -317,7 +330,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-outlet/outlet-address".to_string(), + uri: "/node-name/tcp-outlets/outlet-address".to_string(), body: None, }; @@ -337,7 +350,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-outlet".to_string(), + uri: "/node-name/tcp-outlets".to_string(), body: None, }; @@ -358,7 +371,7 @@ mod test { let request = ControlApiHttpRequest { method: "DELETE".to_string(), - uri: "/node-name/tcp-outlet/outlet-address".to_string(), + uri: "/node-name/tcp-outlets/outlet-address".to_string(), body: None, }; @@ -373,7 +386,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-outlet/outlet-address".to_string(), + uri: "/node-name/tcp-outlets/outlet-address".to_string(), body: None, }; @@ -389,7 +402,7 @@ mod test { let request = ControlApiHttpRequest { method: "GET".to_string(), - uri: "/node-name/tcp-outlet".to_string(), + uri: "/node-name/tcp-outlets".to_string(), body: None, }; diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs index 19e3f8dd48e..89204797afe 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs @@ -1,6 +1,7 @@ use crate::control_api::backend::common; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; +use crate::control_api::protocol::common::{ErrorResponse, NodeName}; use crate::control_api::protocol::relay::{CreateRelayRequest, RelayStatus}; use crate::control_api::ControlApiError; use crate::nodes::models::relay::ReturnTiming; @@ -20,10 +21,10 @@ impl HttpControlNodeApiBackend { resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "tcp-outlet"; - let resource_name_identifier = "tcp_outlet_name"; + let resource_name = "relays"; + let resource_name_identifier = "relay_name"; match method { - "PUT" => handle_relay_create(context, &self.node_manager, body).await, + "POST" => handle_relay_create(context, &self.node_manager, body).await, "GET" => match resource_id { None => handle_relay_list(&self.node_manager).await, Some(id) => handle_relay_get(&self.node_manager, id).await, @@ -37,23 +38,30 @@ impl HttpControlNodeApiBackend { }, _ => { warn!("Invalid method: {method}"); - ControlApiHttpResponse::invalid_method(method, vec!["PUT", "GET", "DELETE"]) + ControlApiHttpResponse::invalid_method(method, vec!["POST", "GET", "DELETE"]) } } } } #[utoipa::path( - put, + post, operation_id = "create_relay", summary = "Create a new Relay", - path = "/{node}/relay", - tags = ["relay"], + description = +"Create a new Relay, the main parameters are the destination node `to` and the `name`. +The address inherits the name value, but it's possible to specify it to allow the creation +of multiple relays with the same address to different nodes. +The creation will be asynchronous and the initial status will be `down`. +Note that a specific `ockam-relay` attribute is required to allow the relay creation in the +destination node.", + path = "/{node}/relays", + tags = ["Relays"], responses( (status = CREATED, description = "Successfully created", body = RelayStatus), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ), request_body( content = CreateRelayRequest, @@ -119,13 +127,14 @@ async fn handle_relay_create( get, operation_id = "list_relay", summary = "List all Relays", - path = "/{node}/relay", - tags = ["relay"], + description = "List all Relays created in the node regardless of their status.", + path = "/{node}/relays", + tags = ["Relays"], responses( (status = OK, description = "Successfully listed", body = Vec), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ) )] async fn handle_relay_list( @@ -144,14 +153,15 @@ async fn handle_relay_list( delete, operation_id = "delete_relay", summary = "Delete a Relay", - path = "/{node}/relay/{resource_id}", - tags = ["relay"], + description = "Delete the specified Relay by name.", + path = "/{node}/relays/{relay_name}", + tags = ["Relays"], responses( (status = NO_CONTENT, description = "Successfully deleted"), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Resource ID") + ("node" = NodeName,), + ("relay_name" = String, description = "Relay name"), ) )] async fn handle_relay_delete( @@ -174,15 +184,16 @@ async fn handle_relay_delete( get, operation_id = "get_relay", summary = "Get a Relay", - path = "/{node}/relay/{resource_id}", - tags = ["relay"], + description = "Get the specified Relay by name.", + path = "/{node}/relays/{relay_name}", + tags = ["Relays"], responses( (status = OK, description = "Successfully retrieved", body = RelayStatus), - (status = NOT_FOUND, description = "Resource not found"), + (status = NOT_FOUND, description = "Relay not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), - ("resource_id" = String, description = "Resource ID") + ("node" = NodeName,), + ("relay_name" = String, description = "Relay name"), ) )] async fn handle_relay_get( diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs index 673405aa47a..7f1af97fcd5 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs @@ -5,9 +5,9 @@ use crate::control_api::backend::common; use crate::control_api::backend::common::{create_authority_client, parse_identifier}; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; -use crate::control_api::protocol::common::{ErrorResponse, HostnamePort, Project}; +use crate::control_api::protocol::common::{ErrorResponse, HostnamePort, NodeName, Project}; use crate::control_api::protocol::ticket::{ - AuthorityInformation, CreateTicketRequest, EnrollTicketRequest, Ticket, + AuthorityInformation, CreateTicketRequest, EnrollProjectRequest, Ticket, }; use crate::control_api::ControlApiError; use crate::enroll::enrollment::{EnrollStatus, Enrollment}; @@ -26,36 +26,58 @@ impl HttpControlNodeApiBackend { &self, context: &Context, method: &str, - _resource_id: Option<&str>, + resource_id: Option<&str>, body: Option>, ) -> Result { + let resource_name = "tickets"; match method { - "PUT" => handle_ticket_create(context, &self.node_manager, body).await, - "POST" => handle_ticket_enroll(context, &self.node_manager, body).await, + "POST" => { + if let Some(resource_id) = resource_id { + if resource_id == "enroll" { + handle_ticket_enroll(context, &self.node_manager, body).await + } else { + ControlApiHttpResponse::bad_request( + &format!("The HTTP path should be /{{node-name}}/{resource_name} or /{{node-name}}/{resource_name}/enroll") + ) + } + } else { + handle_ticket_create(context, &self.node_manager, body).await + } + } _ => { warn!("Invalid method: {method}"); - ControlApiHttpResponse::invalid_method(method, vec!["PUT", "POST"]) + ControlApiHttpResponse::invalid_method(method, vec!["POST"]) } } } } #[utoipa::path( - put, + post, operation_id = "create_ticket", summary = "Create a new Ticket", - path = "/{node}/ticket", - tags = ["ticket"], + description = +"Create a new Ticket, the main parameters are `attributes`, the list of attributes associated +with the ticket, and `project`, the target project for the ticket. In the vast majority of cases, +specifying the project name will be enough, but it's also possible to specify a custom Ockam +Authority and a custom node acting as a Project. +You can also limit the validity of the ticket by specifying the `usage_count` and `expires_in` +fields. +The ticket is returned as an opaque string that can be used to enroll to a project, either via API +or via CLI with the `ockam project enroll` command.", + path = "/{node}/tickets", + tags = ["Tickets"], responses( (status = CREATED, description = "Successfully created", body = Ticket), + (status = NOT_FOUND, description = "Specified project not found", body = ErrorResponse), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ), request_body( content = CreateTicketRequest, content_type = "application/json", - description = "Creation request" + description = "Create Ticket request" ) )] async fn handle_ticket_create( @@ -75,7 +97,7 @@ async fn handle_ticket_create( let result = authority_client .create_token( context, - request.attributes, + request.attributes.0, Some(Duration::from_secs(request.expires_in)), Some(request.usage_count), ) @@ -197,22 +219,27 @@ async fn create_encoded_ticket( #[utoipa::path( post, - operation_id = "enroll_ticket", - summary = "Enroll a Ticket", - path = "/{node}/ticket", - tags = ["ticket"], + operation_id = "project_enroll", + summary = "Enroll to a Project using a Ticket", + description = +"This API enroll a node to a Project using the provided Ticket. +Note that this API imports the Project in the node database, but the node won't be able to use +it until it restarts. +The easiest way to use a ticket is to specify the ticket directly during the node creation.", + path = "/{node}/tickets/enroll", + tags = ["Tickets"], responses( (status = CREATED, description = "Successfully enrolled, new credential can be used right away", body = AuthorityInformation), (status = OK, description = "The node was already enrolled, no change in state", body = AuthorityInformation), (status = ACCEPTED, description = "Enrolled, but the node needs a restart", body = AuthorityInformation), ), params( - ("node" = String, description = "Destination node name"), + ("node" = NodeName,), ), request_body( - content = EnrollTicketRequest, + content = EnrollProjectRequest, content_type = "application/json", - description = "Enrollment request" + description = "Project enrollment request" ) )] async fn handle_ticket_enroll( @@ -220,7 +247,7 @@ async fn handle_ticket_enroll( node_manager: &Arc, body: Option>, ) -> Result { - let request: EnrollTicketRequest = common::parse_request_body(body)?; + let request: EnrollProjectRequest = common::parse_request_body(body)?; let caller_identifier = if let Some(identity) = request.identity { parse_identifier(&identity, "identity to enroll")? diff --git a/implementations/rust/ockam/ockam_api/src/control_api/frontend.rs b/implementations/rust/ockam/ockam_api/src/control_api/frontend.rs index 08ac0e3a67e..f59452121c0 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/frontend.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/frontend.rs @@ -489,7 +489,7 @@ mod test { .await?; let response: Response = send_http_request( - Request::get(format!("http://{bind_address}/node1/tcp-inlet")) + Request::get(format!("http://{bind_address}/node1/tcp-inlets")) .body(()) .unwrap(), ) @@ -499,7 +499,7 @@ mod test { assert_eq!(response.body().message, "Missing authentication token"); let response: Response = send_http_request( - Request::get(format!("http://{bind_address}/node1/tcp-inlet")) + Request::get(format!("http://{bind_address}/node1/tcp-inlets")) .header("Authorization", "invalid_token") .body(()) .unwrap(), @@ -510,7 +510,7 @@ mod test { assert_eq!(response.body().message, "Invalid authentication token"); let response: Response = send_http_request( - Request::get(format!("http://{bind_address}/node1/tcp-inlet")) + Request::get(format!("http://{bind_address}/node1/tcp-inlets")) .header("Authorization", "Bearer invalid_token") .body(()) .unwrap(), @@ -521,7 +521,7 @@ mod test { assert_eq!(response.body().message, "Invalid authentication token"); let response: Response = send_http_request( - Request::get(format!("http://{bind_address}/node1/tcp-inlet")) + Request::get(format!("http://{bind_address}/node1/tcp-inlets")) .header("Authorization", "Bearer token") .body(()) .unwrap(), @@ -531,7 +531,7 @@ mod test { assert_eq!(response.status().as_u16(), 502); let response: Response = send_http_request( - Request::get(format!("http://{bind_address}/node1/tcp-inlet")) + Request::get(format!("http://{bind_address}/node1/tcp-inlets")) .header("Authorization", "token") .body(()) .unwrap(), @@ -565,7 +565,7 @@ mod test { context.start_worker("forward_to_node1", Hop)?; let response: Response> = send_http_request( - Request::get(format!("http://{bind_address}/node1/tcp-inlet")) + Request::get(format!("http://{bind_address}/node1/tcp-inlets")) .header("Authorization", "Bearer token") .body(()) .unwrap(), @@ -600,7 +600,7 @@ mod test { let response: Response = send_http_request( Request::get(format!( - "http://{bind_address}/non-existing-node/tcp-inlet/" + "http://{bind_address}/non-existing-node/tcp-inlets/" )) .header("Authorization", "Bearer token") .body(()) @@ -639,7 +639,7 @@ mod test { .create_control_api_backend(context, None)?; let response: Response> = send_http_request( - Request::get(format!("http://{bind_address}/node1/tcp-inlet")) + Request::get(format!("http://{bind_address}/node1/tcp-inlets")) .header("Authorization", "Bearer token") .body(()) .unwrap(), @@ -676,10 +676,12 @@ mod test { .create_control_api_backend(context, None)?; let response: Response = send_http_request( - Request::get(format!("http://{bind_address}/non-existing-node/tcp-inlet")) - .header("Authorization", "Bearer token") - .body(()) - .unwrap(), + Request::get(format!( + "http://{bind_address}/non-existing-node/tcp-inlets" + )) + .header("Authorization", "Bearer token") + .body(()) + .unwrap(), ) .await; diff --git a/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs b/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs index e1f782cb63b..d9200aa0ce9 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs @@ -3,6 +3,7 @@ use super::backend::inlet::*; use super::backend::outlet::*; use super::backend::relay::*; use super::backend::ticket::*; +use crate::control_api::protocol::common::NodeName; use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}; use utoipa::{Modify, OpenApi}; @@ -12,9 +13,10 @@ impl Modify for Authentications { fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { if let Some(schema) = openapi.components.as_mut() { schema.add_security_scheme( - "bearer", + "token", SecurityScheme::Http( HttpBuilder::new() + .description(Some("A simple token authentication, this token can be set as startup parameter")) .scheme(HttpAuthScheme::Bearer) .bearer_format("Plaintext") .build(), @@ -24,12 +26,93 @@ impl Modify for Authentications { } } +const MAIN_DESCRIPTION: &str = r#" +## Overview +This API is designed to control Ockam nodes via HTTP requests without having to use Ockam Command or Ockam library. +You can expect a similar abstraction level as Ockam Command, but with a more programmatic approach. + +The endpoint will receive the HTTP request and forward it to the selected node using the `node` +parameter. How the node will be selected starting from the name is configuration dependent. + +## Versioning +Current major `0.Y.Z` is considered unstable and may have breaking changes. +Future versions will follow semantic versioning, breaking changes will be reflected in the major version number. + +## Authentication +Only a simple bearer token is required to authenticate. The token is passed in the `Authorization` +header: `Authorization: Bearer my-secret-token`. + +## Development +The easiest way to get development started is to use this API with the Ockam Command and run a single node acting both as a frontend and backend. +```sh +ockam node create --foreground -vv --launch-configuration '{ + "start_default_services": true, + "startup_services": { + "control_api": { + "authentication_token": "my-secret-token", + "backend": true, + "frontend": true, + "http_bind_address": "127.0.0.1:8080" + } + } +}' +``` + +When an undocumented error is returned, such as internal server error, a message contained by `ErrorResponse` is usually present for debugging purposes. +It's highly recommended to check the logs of the Ockam nodes for more information. +"#; + #[derive(OpenApi)] #[openapi( info( - title = "Ockam Control API", + title = "Ockam Node Control API", version = "0.1.0", - description = "API to control Ockam nodes", + description = MAIN_DESCRIPTION, + ), + tags(( + name = "Portals", + external_docs( + url = "https://docs.ockam.io/reference/command/advanced-routing#portal", + description = "Learn more about Portals on Ockam Command documentation" + ), + description = +" +A TCP Inlet and TCP Outlet together form a Portal, working hand in hand with Relays. A TCP Inlet +defines where a node, running on another machine, listens for connections. The Inlet's route +provides information on how to forward traffic to the Outlet (its address). Relays allow you to +establish end-to-end protocols with services that operate in remote private networks. +" + ),( + name = "Relays", + external_docs( + url = "https://docs.ockam.io/reference/command/advanced-routing#relays", + description = "Learn more about Relays on Ockam Command documentation" + ), + description = +" +Relays make it possible to establish end-to-end protocols with services operating in a remote +private networks, without requiring a remote service to expose listening ports to an outside hostile +network like the Internet. +", + ),( + name = "Tickets", + description = +" +The ticket is plain text representing a one-time use token and the non-sensitive data about the +Project, like the route to reach it and the Project Identity Identifier, which will be used to +validate the Project Identity. The ticket itself can be stored in an environment variable, or a +file. +", + ),( + name = "Authority Members", + description = +" +An Ockam Authority is a Ockam node running a set of services dedicated to managing credentials +and identities. +The Ockam Authority keeps a list of members, which are entitled to receive credentials with proper +attributes. +" + ) ), modifiers(&Authentications), paths( @@ -55,11 +138,14 @@ impl Modify for Authentications { handle_authority_member_remove ), security( - ("bearer" = []) + ("token" = []) ), external_docs( url = "https://docs.ockam.io/", description = "Ockam documentation" + ), + components( + schemas(NodeName), ) )] struct ApiDoc; diff --git a/implementations/rust/ockam/ockam_api/src/control_api/protocol/authority_member.rs b/implementations/rust/ockam/ockam_api/src/control_api/protocol/authority_member.rs index 3c952f5fac8..510f82be2cb 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/protocol/authority_member.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/protocol/authority_member.rs @@ -1,20 +1,12 @@ -use super::common::default_authority; +use super::common::{default_authority, Attributes}; use crate::control_api::protocol::common::Authority; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; use utoipa::ToSchema; #[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "kebab-case")] pub struct AddOrUpdateAuthorityMemberRequest { - /// Attributes in `key=value` format to be attached to the member; - #[schema(examples( - "ockam-role=member", - "ockam-role=enroller", - "ockam-relay=foo", - "my-attribute=my-value" - ))] - pub attributes: BTreeMap, + pub attributes: Attributes, /// Identity to use when contacting the Authority node; /// When omitted, the default identity will be used #[schema(examples("Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a"))] @@ -97,7 +89,5 @@ pub struct AuthorityMember { /// Member identity #[schema(examples("Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a"))] pub identity: String, - /// Attributes attached to the member - #[schema(examples("ockam-role=member, ockam-relay=foo"))] - pub attributes: BTreeMap, + pub attributes: Attributes, } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/protocol/common.rs b/implementations/rust/ockam/ockam_api/src/control_api/protocol/common.rs index 12bbb9e40cd..68a116a890a 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/protocol/common.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/protocol/common.rs @@ -1,8 +1,35 @@ use ockam::identity::{Identity, Vault}; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use std::str::FromStr; use utoipa::ToSchema; +// This is an alias for documentation purposes only +/// The destination node name. +/// Depending on the type of node resolution used, it can be a relay name +/// or a dns address. +/// The special value `self` can be used to refer to the current node. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct NodeName(String); + +// This is an alias for documentation purposes only +#[derive(Serialize, Deserialize, ToSchema, Debug)] +#[schema( + description = +r#" +Credential attributes. +Attributes are key-value pairs that can be used to describe a credential. +Ockam uses `ockam-` as a prefix for its own attributes. +[You can learn more about attributes in the Ockam documentation](https://docs.ockam.io/reference/protocols/access-controls) +"#, + example = json!({ + "ockam-role": "member", + "ockam-relay": "relay-name", + "my-attribute": "my-value", + }) +)] +pub struct Attributes(pub BTreeMap); + #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct HostnamePort { pub hostname: String, diff --git a/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs b/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs index 4768be9dc9d..f5b816b3561 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs @@ -76,7 +76,8 @@ pub struct CreateInletRequest { #[schema(example = "Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a")] pub authorized: Option, /// Policy expression that will be used for access control to the TCP Inlet; - /// When omitted, the policy set for the "tcp-inlet" resource type will be used + /// When omitted, the policy set for the "tcp-inlet" resource type will be used. + /// [Learn more about Policies expression on the Ockam documentation](https://docs.ockam.io/reference/protocols/access-controls). pub allow: Option, /// When connection is lost, how long to wait before retrying to connect to the TCP Outlet; /// In milliseconds; @@ -88,17 +89,25 @@ pub struct CreateInletRequest { #[serde(rename_all = "kebab-case")] pub struct UpdateInletRequest { /// Policy expression that will be used for access control to the TCP Inlet; + /// When omitted, the policy set for the "tcp-inlet" resource type will be used. + /// [Learn more about Policies expression on the Ockam documentation](https://docs.ockam.io/reference/protocols/access-controls). pub allow: Option, } #[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "kebab-case")] pub struct InletStatus { + /// Name of the TCP Inlet pub name: String, + /// Status of the TCP Inlet pub status: ConnectionStatus, + /// Bind address of the TCP Inlet pub bind_address: HostnamePort, + /// The current route of the TCP Inlet, populated only when the status is `up` pub current_route: Option, + /// Multiaddress to the TCP Outlet pub to: String, + /// Whether the TCP Inlet is of privileged kind pub privileged: bool, } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/protocol/outlet.rs b/implementations/rust/ockam/ockam_api/src/control_api/protocol/outlet.rs index add48cc5a49..896b01ff335 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/protocol/outlet.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/protocol/outlet.rs @@ -38,7 +38,8 @@ pub struct CreateOutletRequest { #[schema(default = "None")] pub tls: OutletTls, /// Policy expression that will be used for access control to the TCP Outlet; - /// by default the policy set for the "tcp-outlet" resource type will be used + /// by default, the policy set for the "tcp-outlet" resource type will be used. + /// [Learn more about Policies expression on the Ockam documentation](https://docs.ockam.io/reference/protocols/access-controls). pub allow: Option, } @@ -46,14 +47,19 @@ pub struct CreateOutletRequest { #[serde(rename_all = "kebab-case")] pub struct UpdateOutletRequest { /// Policy expression that will be used for access control to the TCP Outlet; + /// by default, the policy set for the "tcp-outlet" resource type will be used. + /// [Learn more about Policies expression on the Ockam documentation](https://docs.ockam.io/reference/protocols/access-controls). pub allow: Option, } #[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "kebab-case")] pub struct OutletStatus { + /// The address of the outlet pub to: HostnamePort, + /// The address of the worker, this also acts as an identifier of the TCP Outlet within the node pub address: String, + /// Whether the outlet is of privileged kind pub privileged: bool, } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/protocol/ticket.rs b/implementations/rust/ockam/ockam_api/src/control_api/protocol/ticket.rs index 183ee365e77..5f31439fc78 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/protocol/ticket.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/protocol/ticket.rs @@ -1,7 +1,6 @@ -use super::common::{default_project_information, Project}; +use super::common::{default_project_information, Attributes, Project}; use crate::control_api::protocol::common::HostnamePort; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; use utoipa::ToSchema; fn default_usage_count() -> u64 { @@ -15,14 +14,7 @@ fn default_expires_in() -> u64 { #[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "kebab-case")] pub struct CreateTicketRequest { - /// Attributes in `key=value` format to be attached to the member; - #[schema(examples( - "ockam-role=member", - "ockam-role=enroller", - "ockam-relay=foo", - "my-attribute=my-value" - ))] - pub attributes: BTreeMap, + pub attributes: Attributes, /// Identity to use when contacting the Authority node; /// When omitted, the default identity will be used #[schema(examples("Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a"))] @@ -52,7 +44,7 @@ pub struct Ticket { } #[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct EnrollTicketRequest { +pub struct EnrollProjectRequest { /// Identity to enroll; /// When omitted, the default identity will be used #[schema(examples("Id3b788c6a89de8b1f2fd13743eb3123178cf6ec7c9253be8ddcf7e154abe016a"))] diff --git a/implementations/rust/ockam/ockam_command/tests/bats/orchestrator/control_api.bats b/implementations/rust/ockam/ockam_command/tests/bats/orchestrator/node_control_api.bats similarity index 88% rename from implementations/rust/ockam/ockam_command/tests/bats/orchestrator/control_api.bats rename to implementations/rust/ockam/ockam_command/tests/bats/orchestrator/node_control_api.bats index 8d4d8180a4b..71a769bd728 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/orchestrator/control_api.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/orchestrator/node_control_api.bats @@ -45,7 +45,7 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o inlet-list.json \ - "http://localhost:${api_port}/red/tcp-inlet" + "http://localhost:${api_port}/red/tcp-inlets" run_success cat inlet-list.json assert_output "[]" } @@ -82,7 +82,7 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o inlet-list.json \ - "http://localhost:${api_port}/localhost/tcp-inlet" + "http://localhost:${api_port}/localhost/tcp-inlets" run_success cat inlet-list.json assert_output "[]" } @@ -105,11 +105,11 @@ teardown() { # create outlet run_success curl -vf \ - -X PUT \ + -X POST \ -H 'Authorization: Bearer token' \ -d "{\"kind\":\"regular\",\"address\":\"my-outlet\",\"to\":{\"hostname\":\"localhost\", \"port\":$PYTHON_SERVER_PORT}}" \ -o outlet-creation.json \ - "http://localhost:${api_port}/self/tcp-outlet" + "http://localhost:${api_port}/self/tcp-outlets" run_success sh -c "cat outlet-creation.json | jq -rc .address" assert_output "my-outlet" @@ -117,7 +117,7 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o outlet.json \ - "http://localhost:${api_port}/self/tcp-outlet/my-outlet" + "http://localhost:${api_port}/self/tcp-outlets/my-outlet" run_success sh -c "cat outlet.json | jq -rc .address" assert_output "my-outlet" @@ -125,17 +125,17 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o outlet-list.json \ - "http://localhost:${api_port}/self/tcp-outlet" + "http://localhost:${api_port}/self/tcp-outlets" run_success sh -c "cat outlet-list.json | jq -rc .[0].address" assert_output "my-outlet" # create inlet run_success curl -vf \ - -X PUT \ + -X POST \ -H 'Authorization: Bearer token' \ -d "{\"from\":{\"hostname\":\"127.0.0.1\",\"port\":0},\"kind\":\"regular\",\"name\":\"my-inlet\",\"to\":\"/secure/api/service/my-outlet\"}" \ -o inlet-creation.json \ - "http://localhost:${api_port}/self/tcp-inlet" + "http://localhost:${api_port}/self/tcp-inlets" inlet_port=$(cat inlet-creation.json | jq -rc '."bind-address".port') wait_for_port $inlet_port @@ -143,7 +143,7 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o inlet.json \ - "http://localhost:${api_port}/self/tcp-inlet/my-inlet" + "http://localhost:${api_port}/self/tcp-inlets/my-inlet" run_success sh -c "cat inlet.json | jq -rc .name" assert_output "my-inlet" @@ -151,23 +151,23 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o inlet-list.json \ - "http://localhost:${api_port}/self/tcp-inlet" + "http://localhost:${api_port}/self/tcp-inlets" run_success sh -c "cat inlet-list.json | jq -rc .[0].name" assert_output "my-inlet" - # verify that the inlet is working + # verify that the portal is working run_success curl -sfI --retry-all-errors --retry-delay 5 --retry 10 -m 5 "127.0.0.1:$inlet_port" # delete the outlet run_success curl -vf \ -X DELETE \ -H 'Authorization: Bearer token' \ - "http://localhost:${api_port}/self/tcp-outlet/my-outlet" + "http://localhost:${api_port}/self/tcp-outlets/my-outlet" run_success curl -vf \ -H 'Authorization: Bearer token' \ -o outlet-list.json \ - "http://localhost:${api_port}/self/tcp-outlet" + "http://localhost:${api_port}/self/tcp-outlets" run_success sh -c "cat outlet-list.json | jq -rc ." assert_output "[]" @@ -175,12 +175,12 @@ teardown() { run_success curl -vf \ -X DELETE \ -H 'Authorization: Bearer token' \ - "http://localhost:${api_port}/self/tcp-inlet/my-inlet" + "http://localhost:${api_port}/self/tcp-inlets/my-inlet" run_success curl -vf \ -H 'Authorization: Bearer token' \ -o inlet-list.json \ - "http://localhost:${api_port}/self/tcp-inlet" + "http://localhost:${api_port}/self/tcp-inlets" run_success sh -c "cat inlet-list.json | jq -rc ." assert_output "[]" } @@ -204,11 +204,11 @@ teardown() { # create relay run_success curl -vf \ - -X PUT \ + -X POST \ -H 'Authorization: Bearer token' \ -d "{\"address\":\"my-address\",\"name\":\"my-relay\",\"to\":\"/project/default\"}" \ -o relay-creation.json \ - "http://localhost:${api_port}/self/relay" + "http://localhost:${api_port}/self/relays" run_success sh -c "cat relay-creation.json | jq -rc .name" assert_output "my-relay" @@ -217,7 +217,7 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o relay.json \ - "http://localhost:${api_port}/self/relay/my-relay" + "http://localhost:${api_port}/self/relays/my-relay" run_success sh -c "cat relay.json | jq -rc .name" assert_output "my-relay" @@ -235,7 +235,7 @@ teardown() { run_success curl -vf \ -H 'Authorization: Bearer token' \ -o relay-list.json \ - "http://localhost:${api_port}/self/relay" + "http://localhost:${api_port}/self/relays" run_success sh -c "cat relay-list.json | jq -rc .[0].name" assert_output "my-relay" @@ -247,13 +247,13 @@ teardown() { run_success curl -vf \ -X DELETE \ -H 'Authorization: Bearer token' \ - "http://localhost:${api_port}/self/relay/my-relay" + "http://localhost:${api_port}/self/relays/my-relay" # verify that the relay is deleted run_success curl -vf \ -H 'Authorization: Bearer token' \ -o relay-list.json \ - "http://localhost:${api_port}/self/relay" + "http://localhost:${api_port}/self/relays" run_success sh -c "cat relay-list.json | jq -rc ." assert_output "[]" } @@ -277,11 +277,11 @@ teardown() { # create ticket run_success curl -vf \ - -X PUT \ + -X POST \ -H 'Authorization: Bearer token' \ -d "{\"attributes\":{\"ockam-role\":\"member\",\"my-attribute\":\"my-value\"}}" \ -o ticket-creation.json \ - "http://localhost:${api_port}/self/ticket" + "http://localhost:${api_port}/self/tickets" ticket=$(cat ticket-creation.json | jq -rc .encoded) # use the ticket with a dedicated identity @@ -294,7 +294,7 @@ teardown() { -H 'Authorization: Bearer token' \ -d "{\"ticket\":\"$ticket\",\"identity\":\"$identifier\"}" \ -o enrollment.json \ - "http://localhost:${api_port}/self/ticket" + "http://localhost:${api_port}/self/tickets/enroll" run_success sh -c "cat enrollment.json | jq -rc .identity" } @@ -348,11 +348,11 @@ EOF ) run_success curl -vf \ - -X PUT \ + -X POST \ -H 'Authorization: Bearer token' \ -d "${request}" \ -o ticket-creation.json \ - "http://localhost:${api_port}/self/ticket" + "http://localhost:${api_port}/self/tickets" ticket=$(cat ticket-creation.json | jq -rc .encoded) # use the ticket with a dedicated identity @@ -365,7 +365,7 @@ EOF -H 'Authorization: Bearer token' \ -d "{\"ticket\":\"$ticket\",\"identity\":\"$identifier\"}" \ -o enrollment.json \ - "http://localhost:${api_port}/self/ticket" + "http://localhost:${api_port}/self/tickets/enroll" run_success sh -c "cat enrollment.json | jq -rc .identity" } @@ -391,13 +391,13 @@ EOF -X PUT \ -H 'Authorization: Bearer token' \ -d "{\"attributes\":{\"ockam-role\":\"member\",\"my-attribute\":\"my-value\"}}" \ - "http://localhost:${api_port}/self/authority-member/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" + "http://localhost:${api_port}/self/authority-members/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" # get member run_success curl -vf \ -H 'Authorization: Bearer token' \ -o get-member.json \ - "http://localhost:${api_port}/self/authority-member/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" + "http://localhost:${api_port}/self/authority-members/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" run_success sh -c "cat get-member.json | jq -rc .attributes.\\\"my-attribute\\\"" assert_output "my-value" run_success sh -c "cat get-member.json | jq -rc .identity" @@ -407,7 +407,7 @@ EOF run_success curl -vf \ -H 'Authorization: Bearer token' \ -o list-members.json \ - "http://localhost:${api_port}/self/authority-member" + "http://localhost:${api_port}/self/authority-members" # check that the identity is listed as a member by making a search with jq run_success sh -c "cat list-members.json | jq -rc '.[] | select(.identity == \"Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48\").attributes.\"my-attribute\"'" assert_output "my-value" @@ -416,10 +416,10 @@ EOF run_success curl -vf \ -X DELETE \ -H 'Authorization: Bearer token' \ - "http://localhost:${api_port}/self/authority-member/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" + "http://localhost:${api_port}/self/authority-members/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" # verify that the member is deleted run_failure curl -vf \ -H 'Authorization: Bearer token' \ - "http://localhost:${api_port}/self/authority-member/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" + "http://localhost:${api_port}/self/authority-members/Ia641901932d24b8a63b51cb78ebe099b3341dcbd6aaa202cc36868bec72bbd48" } From f562ee89a475b8aa9d2acc4d1780ac8446e10a16 Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Wed, 19 Feb 2025 17:44:32 +0100 Subject: [PATCH 2/2] fix(rust): node control api fixes and minor refactorings --- .../control_api/backend/authority_member.rs | 29 +++---- .../src/control_api/backend/common.rs | 79 ++++++++++++++++++- .../src/control_api/backend/entrypoint.rs | 70 +++++++++------- .../src/control_api/backend/inlet.rs | 36 +++------ .../src/control_api/backend/outlet.rs | 27 +++---- .../src/control_api/backend/relay.rs | 27 +++---- .../src/control_api/backend/ticket.rs | 16 ++-- .../ockam/ockam_api/src/control_api/http.rs | 25 ++---- .../ockam_api/src/control_api/openapi.rs | 19 ++--- .../src/control_api/protocol/inlet.rs | 5 +- 10 files changed, 193 insertions(+), 140 deletions(-) diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs index 1f4c79cfb20..cabb2aab5e0 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/authority_member.rs @@ -1,6 +1,6 @@ use crate::authenticator::direct::Members; use crate::control_api::backend::common; -use crate::control_api::backend::common::create_authority_client; +use crate::control_api::backend::common::{create_authority_client, ResourceKind}; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; use crate::control_api::protocol::authority_member::{ @@ -10,7 +10,7 @@ use crate::control_api::protocol::authority_member::{ use crate::control_api::protocol::common::{Attributes, ErrorResponse, NodeName}; use crate::control_api::ControlApiError; use crate::nodes::NodeManager; -use http::StatusCode; +use http::{Method, StatusCode}; use ockam_node::Context; use std::sync::Arc; @@ -18,41 +18,36 @@ impl HttpControlNodeApiBackend { pub(super) async fn handle_authority_member( &self, context: &Context, - method: &str, + method: Method, resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "authority-members"; - let resource_name_identifier = "authority_member_identity"; match method { - "PUT" => match resource_id { - None => ControlApiHttpResponse::missing_resource_id( - resource_name, - resource_name_identifier, - ), + Method::PUT => match resource_id { + None => ControlApiHttpResponse::missing_resource_id(ResourceKind::AuthorityMembers), Some(id) => { handle_authority_member_add_or_update(context, &self.node_manager, body, id) .await } }, - "GET" => match resource_id { + Method::GET => match resource_id { None => handle_authority_member_list(context, &self.node_manager, body).await, Some(id) => { handle_authority_member_get(context, &self.node_manager, body, id).await } }, - "DELETE" => match resource_id { - None => ControlApiHttpResponse::missing_resource_id( - resource_name, - resource_name_identifier, - ), + Method::DELETE => match resource_id { + None => ControlApiHttpResponse::missing_resource_id(ResourceKind::AuthorityMembers), Some(id) => { handle_authority_member_remove(context, &self.node_manager, body, id).await } }, _ => { warn!("Invalid method: {method}"); - ControlApiHttpResponse::invalid_method(method, vec!["PUT", "GET", "DELETE"]) + ControlApiHttpResponse::invalid_method( + method, + vec![Method::PUT, Method::GET, Method::DELETE], + ) } } } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs index 11a60d28c11..49ef3fdb1c0 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/common.rs @@ -1,16 +1,91 @@ use crate::control_api::http::ControlApiHttpResponse; -use crate::control_api::protocol::common::Authority; +use crate::control_api::protocol::common::{Authority, ErrorResponse}; use crate::control_api::ControlApiError; use crate::nodes::NodeManager; use crate::orchestrator::project::Project; use crate::orchestrator::AuthorityNodeClient; +use http::StatusCode; use ockam::identity::Identifier; use ockam_core::errcode::{Kind, Origin}; use ockam_multiaddr::MultiAddr; use serde::de::DeserializeOwned; +use std::fmt::Display; use std::str::FromStr; use std::sync::Arc; +pub(super) enum ResourceKind { + TcpInlets, + TcpOutlets, + Relays, + Tickets, + AuthorityMembers, +} + +impl ResourceKind { + pub fn enumerate() -> Vec { + vec![ + Self::TcpInlets, + Self::TcpOutlets, + Self::Relays, + Self::Tickets, + Self::AuthorityMembers, + ] + } + pub fn from_str(resource: &str) -> Option { + match resource { + "tcp-inlets" => Some(Self::TcpInlets), + "tcp-outlets" => Some(Self::TcpOutlets), + "relays" => Some(Self::Relays), + "tickets" => Some(Self::Tickets), + "authority-members" => Some(Self::AuthorityMembers), + _ => None, + } + } + + pub fn name(&self) -> &'static str { + match self { + Self::TcpInlets => "tcp-inlets", + Self::TcpOutlets => "tcp-outlets", + Self::Relays => "relays", + Self::Tickets => "tickets", + Self::AuthorityMembers => "authority-members", + } + } + + pub fn parameter_name(&self) -> &'static str { + match self { + Self::TcpInlets => "inlet_name", + Self::TcpOutlets => "outlet_address", + Self::Relays => "relay_name", + Self::Tickets => "", + Self::AuthorityMembers => "authority_member", + } + } +} + +impl Display for ResourceKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl ControlApiHttpResponse { + pub(super) fn missing_resource_id( + resource_kind: ResourceKind, + ) -> Result { + let resource_name = resource_kind.name(); + let resource_name_identifier = resource_kind.parameter_name(); + + Err(Self::with_body( + StatusCode::BAD_REQUEST, + ErrorResponse { + message: format!("Missing parameter {resource_name}. The HTTP path should be /{{node-name}}/{resource_name}/{{{resource_name_identifier}}}"), + }, + )? + .into()) + } +} + pub async fn create_authority_client( node_manager: &Arc, authority: &Authority, @@ -61,7 +136,7 @@ pub async fn create_authority_client( } Err(error) => { warn!("No default project: {error:?}"); - return ControlApiHttpResponse::not_found("Default default project not found"); + return ControlApiHttpResponse::not_found("Default project not found"); } } } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs index e21b930a93d..9e62b53779b 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/entrypoint.rs @@ -1,9 +1,11 @@ +use crate::control_api::backend::common::ResourceKind; use crate::control_api::http::{ControlApiHttpRequest, ControlApiHttpResponse}; use crate::control_api::protocol::common::ErrorResponse; use crate::control_api::ControlApiError; use crate::nodes::NodeManager; use crate::DefaultAddress; -use http::{StatusCode, Uri}; +use http::{Method, StatusCode, Uri}; +use itertools::Itertools; use ockam_abac::{IncomingAbac, OutgoingAbac, PolicyExpression}; use ockam_core::errcode::Kind; use ockam_core::{ @@ -64,48 +66,56 @@ impl Worker for HttpControlNodeApiBackend { // we can ignore the node identifier since it has been already addressed by // the reverse proxy let _node_identifier = path[1]; + let raw_resource_kind = path[2].to_lowercase(); - let resource_kind = path[2].to_lowercase(); + let resource_kind = ResourceKind::from_str(&raw_resource_kind); let resource_id = path.get(3).copied(); + let method = match Method::try_from(request.method.as_str()) { + Ok(method) => method, + Err(_) => { + warn!("Invalid method: {}", request.method); + return context + .send( + message.return_route().clone(), + NeutralMessage::from(minicbor::to_vec( + &ControlApiHttpResponse::with_body( + StatusCode::BAD_REQUEST, + ErrorResponse { + message: "Invalid method".to_string(), + }, + )?, + )?), + ) + .await; + } + }; - let result: Result = match resource_kind.as_str() { - "tcp-inlets" => { - self.handle_tcp_inlet(context, request.method.as_str(), resource_id, request.body) + let result: Result = match resource_kind { + Some(ResourceKind::TcpInlets) => { + self.handle_tcp_inlet(context, method, resource_id, request.body) .await } - "tcp-outlets" => { - self.handle_tcp_outlet(context, request.method.as_str(), resource_id, request.body) + Some(ResourceKind::TcpOutlets) => { + self.handle_tcp_outlet(context, method, resource_id, request.body) .await } - "relays" => { - self.handle_relay(context, request.method.as_str(), resource_id, request.body) + Some(ResourceKind::Relays) => { + self.handle_relay(context, method, resource_id, request.body) .await } - "tickets" => Ok(self - .handle_ticket(context, request.method.as_str(), resource_id, request.body) + Some(ResourceKind::Tickets) => Ok(self + .handle_ticket(context, method, resource_id, request.body) .await .unwrap()), - "authority-members" => { - self.handle_authority_member( - context, - request.method.as_str(), - resource_id, - request.body, - ) - .await + Some(ResourceKind::AuthorityMembers) => { + self.handle_authority_member(context, method, resource_id, request.body) + .await } - _ => { - warn!("Invalid resource kind: {resource_kind}"); - let valid_resources = [ - "tcp-inlets", - "tcp-outlets", - "relays", - "tickets", - "authority-members", - ]; + None => { + warn!("Invalid resource kind: {raw_resource_kind}"); let message = format!( - "Invalid resource kind: {resource_kind}. Possible: {}", - valid_resources.join(", ") + "Invalid resource kind: {raw_resource_kind}. Possible: {}", + ResourceKind::enumerate().iter().join(", ") ); ControlApiHttpResponse::bad_request(&message) } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs index bd638c6d718..e121fbc2988 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/inlet.rs @@ -1,4 +1,5 @@ use crate::control_api::backend::common; +use crate::control_api::backend::common::ResourceKind; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; use crate::control_api::protocol::common::{ErrorResponse, NodeName}; @@ -6,7 +7,7 @@ use crate::control_api::protocol::inlet::{CreateInletRequest, InletKind, InletTl use crate::control_api::protocol::inlet::{InletStatus, UpdateInletRequest}; use crate::control_api::ControlApiError; use crate::nodes::NodeManager; -use http::StatusCode; +use http::{Method, StatusCode}; use ockam_abac::{Action, Expr, PolicyExpression, ResourceName}; use ockam_core::compat::rand::random_string; use ockam_core::Route; @@ -18,37 +19,29 @@ impl HttpControlNodeApiBackend { pub(super) async fn handle_tcp_inlet( &self, context: &Context, - method: &str, + method: Method, resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "tcp-inlets"; - let resource_name_identifier = "tcp_inlet_name"; match method { - "POST" => handle_tcp_inlet_create(context, &self.node_manager, body).await, - "GET" => match resource_id { + Method::POST => handle_tcp_inlet_create(context, &self.node_manager, body).await, + Method::GET => match resource_id { None => handle_tcp_inlet_list(&self.node_manager).await, Some(id) => handle_tcp_inlet_get(&self.node_manager, id).await, }, - "PATCH" => match resource_id { - None => ControlApiHttpResponse::missing_resource_id( - resource_name, - resource_name_identifier, - ), + Method::PATCH => match resource_id { + None => ControlApiHttpResponse::missing_resource_id(ResourceKind::TcpInlets), Some(id) => handle_tcp_inlet_update(&self.node_manager, id, body).await, }, - "DELETE" => match resource_id { - None => ControlApiHttpResponse::missing_resource_id( - resource_name, - resource_name_identifier, - ), + Method::DELETE => match resource_id { + None => ControlApiHttpResponse::missing_resource_id(ResourceKind::TcpInlets), Some(id) => handle_tcp_inlet_delete(&self.node_manager, id).await, }, _ => { warn!("Invalid method: {method}"); ControlApiHttpResponse::invalid_method( method, - vec!["POST", "GET", "PATCH", "DELETE"], + vec![Method::POST, Method::GET, Method::PATCH, Method::DELETE], ) } } @@ -61,8 +54,8 @@ impl HttpControlNodeApiBackend { summary = "Create a new TCP Inlet", description = "Create a TCP Inlet, the main parameters are the destination `to`, and the bind address `from`. -You can also choose to the listen with a valid TLS certificate, restrict access to the Inlet with -`authorized` and `allow`, and select a specialized Portals with `kind`. +You can also choose to listen with a valid TLS certificate, restrict access to the Inlet with +`authorized` and `allow`, and select a specialized Portal with `kind`. The creation will be asynchronous and the initial status will be `down`.", path = "/{node}/tcp-inlets", tags = ["Portals"], @@ -202,7 +195,7 @@ async fn handle_tcp_inlet_create( summary = "Update a TCP Inlet", description = "Update the specified TCP Inlet by name. -Currently only `allow` policy expression can be updated, for more advanced updates it's necessary +Currently the only `allow` policy expression can be updated, for more advanced updates it's necessary to delete the TCP Inlet and create a new one.", path = "/{node}/tcp-inlets/{tcp_inlet_name}", tags = ["Portals"], @@ -393,7 +386,6 @@ mod test { assert_eq!(inlet_status.status, ConnectionStatus::Down); assert_eq!(inlet_status.current_route, None); assert_eq!(inlet_status.to, "/service/outlet"); - assert!(!inlet_status.privileged); assert_eq!(inlet_status.bind_address.hostname, "127.0.0.1"); assert!(inlet_status.bind_address.port > 0); @@ -418,7 +410,6 @@ mod test { assert_eq!(inlet_status.status, ConnectionStatus::Up); assert_eq!(inlet_status.current_route, Some("0#outlet".to_string())); assert_eq!(inlet_status.to, "/service/outlet"); - assert!(!inlet_status.privileged); let request = ControlApiHttpRequest { method: "GET".to_string(), @@ -440,7 +431,6 @@ mod test { assert_eq!(inlets[0].status, ConnectionStatus::Up); assert_eq!(inlets[0].current_route, Some("0#outlet".to_string())); assert_eq!(inlets[0].to, "/service/outlet"); - assert!(!inlets[0].privileged); let request = ControlApiHttpRequest { method: "DELETE".to_string(), diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs index 5afa2a10524..d88ccbd047a 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/outlet.rs @@ -1,4 +1,5 @@ use crate::control_api::backend::common; +use crate::control_api::backend::common::ResourceKind; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; use crate::control_api::protocol::common::{ErrorResponse, NodeName}; @@ -8,7 +9,7 @@ use crate::control_api::protocol::outlet::{ use crate::control_api::ControlApiError; use crate::nodes::models::portal::OutletAccessControl; use crate::nodes::NodeManager; -use http::StatusCode; +use http::{Method, StatusCode}; use ockam_abac::{Action, Expr, PolicyExpression, ResourceName}; use ockam_core::errcode::Kind; use ockam_core::Address; @@ -19,37 +20,29 @@ impl HttpControlNodeApiBackend { pub(super) async fn handle_tcp_outlet( &self, context: &Context, - method: &str, + method: Method, resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "tcp-outlets"; - let resource_name_identifier = "tcp_outlet_name"; match method { - "POST" => handle_tcp_outlet_create(context, &self.node_manager, body).await, - "GET" => match resource_id { + Method::POST => handle_tcp_outlet_create(context, &self.node_manager, body).await, + Method::GET => match resource_id { None => handle_tcp_outlet_list(&self.node_manager).await, Some(id) => handle_tcp_outlet_get(&self.node_manager, id).await, }, - "PATCH" => match resource_id { - None => ControlApiHttpResponse::missing_resource_id( - resource_name, - resource_name_identifier, - ), + Method::PATCH => match resource_id { + None => ControlApiHttpResponse::missing_resource_id(ResourceKind::TcpOutlets), Some(id) => handle_tcp_outlet_update(&self.node_manager, id, body).await, }, - "DELETE" => match resource_id { - None => ControlApiHttpResponse::missing_resource_id( - resource_name, - resource_name_identifier, - ), + Method::DELETE => match resource_id { + None => ControlApiHttpResponse::missing_resource_id(ResourceKind::TcpOutlets), Some(id) => handle_tcp_outlet_delete(&self.node_manager, id).await, }, _ => { warn!("Invalid method: {method}"); ControlApiHttpResponse::invalid_method( method, - vec!["POST", "GET", "PATCH", "DELETE"], + vec![Method::POST, Method::GET, Method::PATCH, Method::DELETE], ) } } diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs index 89204797afe..fab66bebc09 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/relay.rs @@ -1,4 +1,5 @@ use crate::control_api::backend::common; +use crate::control_api::backend::common::ResourceKind; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; use crate::control_api::protocol::common::{ErrorResponse, NodeName}; @@ -6,7 +7,7 @@ use crate::control_api::protocol::relay::{CreateRelayRequest, RelayStatus}; use crate::control_api::ControlApiError; use crate::nodes::models::relay::ReturnTiming; use crate::nodes::NodeManager; -use http::StatusCode; +use http::{Method, StatusCode}; use ockam::identity::Identifier; use ockam_core::compat::rand::random_string; use ockam_multiaddr::MultiAddr; @@ -17,28 +18,26 @@ impl HttpControlNodeApiBackend { pub(super) async fn handle_relay( &self, context: &Context, - method: &str, + method: Method, resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "relays"; - let resource_name_identifier = "relay_name"; match method { - "POST" => handle_relay_create(context, &self.node_manager, body).await, - "GET" => match resource_id { + Method::POST => handle_relay_create(context, &self.node_manager, body).await, + Method::GET => match resource_id { None => handle_relay_list(&self.node_manager).await, Some(id) => handle_relay_get(&self.node_manager, id).await, }, - "DELETE" => match resource_id { - None => ControlApiHttpResponse::missing_resource_id( - resource_name, - resource_name_identifier, - ), + Method::DELETE => match resource_id { + None => ControlApiHttpResponse::missing_resource_id(ResourceKind::Relays), Some(id) => handle_relay_delete(&self.node_manager, id).await, }, _ => { warn!("Invalid method: {method}"); - ControlApiHttpResponse::invalid_method(method, vec!["POST", "GET", "DELETE"]) + ControlApiHttpResponse::invalid_method( + method, + vec![Method::POST, Method::GET, Method::DELETE], + ) } } } @@ -53,8 +52,8 @@ impl HttpControlNodeApiBackend { The address inherits the name value, but it's possible to specify it to allow the creation of multiple relays with the same address to different nodes. The creation will be asynchronous and the initial status will be `down`. -Note that a specific `ockam-relay` attribute is required to allow the relay creation in the -destination node.", +Note that, to allow the relay creation in the destination node, the caller identity credential must +have an `ockam-relay` attribute set with the relay name.", path = "/{node}/relays", tags = ["Relays"], responses( diff --git a/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs b/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs index 7f1af97fcd5..39e4d416c6a 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/backend/ticket.rs @@ -2,7 +2,9 @@ use crate::authenticator::enrollment_tokens::TokenIssuer; use crate::authenticator::one_time_code::OneTimeCode; use crate::cli_state::{ExportedEnrollmentTicket, ProjectRoute}; use crate::control_api::backend::common; -use crate::control_api::backend::common::{create_authority_client, parse_identifier}; +use crate::control_api::backend::common::{ + create_authority_client, parse_identifier, ResourceKind, +}; use crate::control_api::backend::entrypoint::HttpControlNodeApiBackend; use crate::control_api::http::ControlApiHttpResponse; use crate::control_api::protocol::common::{ErrorResponse, HostnamePort, NodeName, Project}; @@ -13,7 +15,7 @@ use crate::control_api::ControlApiError; use crate::enroll::enrollment::{EnrollStatus, Enrollment}; use crate::nodes::NodeManager; use crate::orchestrator::HasSecureClient; -use http::StatusCode; +use http::{Method, StatusCode}; use ockam::identity::{Identity, Vault}; use ockam_core::errcode::{Kind, Origin}; use ockam_node::Context; @@ -25,13 +27,13 @@ impl HttpControlNodeApiBackend { pub(super) async fn handle_ticket( &self, context: &Context, - method: &str, + method: Method, resource_id: Option<&str>, body: Option>, ) -> Result { - let resource_name = "tickets"; + let resource_name = ResourceKind::Tickets.name(); match method { - "POST" => { + Method::POST => { if let Some(resource_id) = resource_id { if resource_id == "enroll" { handle_ticket_enroll(context, &self.node_manager, body).await @@ -46,7 +48,7 @@ impl HttpControlNodeApiBackend { } _ => { warn!("Invalid method: {method}"); - ControlApiHttpResponse::invalid_method(method, vec!["POST"]) + ControlApiHttpResponse::invalid_method(method, vec![Method::POST]) } } } @@ -222,7 +224,7 @@ async fn create_encoded_ticket( operation_id = "project_enroll", summary = "Enroll to a Project using a Ticket", description = -"This API enroll a node to a Project using the provided Ticket. +"This API enrolls a node to a Project using the provided Ticket. Note that this API imports the Project in the node database, but the node won't be able to use it until it restarts. The easiest way to use a ticket is to specify the ticket directly during the node creation.", diff --git a/implementations/rust/ockam/ockam_api/src/control_api/http.rs b/implementations/rust/ockam/ockam_api/src/control_api/http.rs index 757254ca943..3915d7f9966 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/http.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/http.rs @@ -1,7 +1,7 @@ use crate::control_api::protocol::common::ErrorResponse; use crate::control_api::ControlApiError; use bytes::Bytes; -use http::StatusCode; +use http::{Method, StatusCode}; use http_body_util::Full; use minicbor::{CborLen, Decode, Encode}; use ockam_core::errcode::{Kind, Origin}; @@ -98,33 +98,24 @@ impl ControlApiHttpResponse { } pub fn invalid_method( - method: &str, - allowed_methods: Vec<&str>, + method: Method, + allowed_methods: Vec, ) -> Result { Err(Self::with_body( StatusCode::METHOD_NOT_ALLOWED, ErrorResponse { message: format!( "Invalid method {method} for this API. Supported methods are: {}", - allowed_methods.join(", ") + allowed_methods + .iter() + .map(|m| m.as_str()) + .collect::>() + .join(", ") ), }, )? .into()) } - - pub fn missing_resource_id( - resource_name: &str, - resource_name_identifier: &str, - ) -> Result { - Err(Self::with_body( - StatusCode::BAD_REQUEST, - ErrorResponse { - message: format!("Missing parameter {resource_name}. The HTTP path should be /{{node-name}}/{resource_name}/{{{resource_name_identifier}}}"), - }, - )? - .into()) - } } pub fn build_error_body(message: &str) -> Full { diff --git a/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs b/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs index d9200aa0ce9..bd56d70647b 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/openapi.rs @@ -28,8 +28,8 @@ impl Modify for Authentications { const MAIN_DESCRIPTION: &str = r#" ## Overview -This API is designed to control Ockam nodes via HTTP requests without having to use Ockam Command or Ockam library. -You can expect a similar abstraction level as Ockam Command, but with a more programmatic approach. +This API is designed to control Ockam nodes via HTTP requests without having to use Ockam command or Ockam library. +You can expect a similar abstraction level as Ockam command, but with a more programmatic approach. The endpoint will receive the HTTP request and forward it to the selected node using the `node` parameter. How the node will be selected starting from the name is configuration dependent. @@ -39,11 +39,12 @@ Current major `0.Y.Z` is considered unstable and may have breaking changes. Future versions will follow semantic versioning, breaking changes will be reflected in the major version number. ## Authentication -Only a simple bearer token is required to authenticate. The token is passed in the `Authorization` -header: `Authorization: Bearer my-secret-token`. +Only a simple bearer token is required to authenticate. The token is provided by the user in the +configuration or via environment variable, it's then passed in the `Authorization` header: + `Authorization: Bearer my-secret-token`. -## Development -The easiest way to get development started is to use this API with the Ockam Command and run a single node acting both as a frontend and backend. +## Getting Started +The easiest way to get development started is to use this API with the Ockam command and run a single node acting both as a frontend and backend. ```sh ockam node create --foreground -vv --launch-configuration '{ "start_default_services": true, @@ -73,7 +74,7 @@ It's highly recommended to check the logs of the Ockam nodes for more informatio name = "Portals", external_docs( url = "https://docs.ockam.io/reference/command/advanced-routing#portal", - description = "Learn more about Portals on Ockam Command documentation" + description = "Learn more about Portals on Ockam command documentation" ), description = " @@ -86,12 +87,12 @@ establish end-to-end protocols with services that operate in remote private netw name = "Relays", external_docs( url = "https://docs.ockam.io/reference/command/advanced-routing#relays", - description = "Learn more about Relays on Ockam Command documentation" + description = "Learn more about Relays on Ockam command documentation" ), description = " Relays make it possible to establish end-to-end protocols with services operating in a remote -private networks, without requiring a remote service to expose listening ports to an outside hostile +private network, without requiring a remote service to expose listening ports to an outside hostile network like the Internet. ", ),( diff --git a/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs b/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs index f5b816b3561..37b6124c374 100644 --- a/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs +++ b/implementations/rust/ockam/ockam_api/src/control_api/protocol/inlet.rs @@ -77,7 +77,7 @@ pub struct CreateInletRequest { pub authorized: Option, /// Policy expression that will be used for access control to the TCP Inlet; /// When omitted, the policy set for the "tcp-inlet" resource type will be used. - /// [Learn more about Policies expression on the Ockam documentation](https://docs.ockam.io/reference/protocols/access-controls). + /// [Learn more about Policies expressions on the Ockam documentation](https://docs.ockam.io/reference/protocols/access-controls). pub allow: Option, /// When connection is lost, how long to wait before retrying to connect to the TCP Outlet; /// In milliseconds; @@ -107,8 +107,6 @@ pub struct InletStatus { pub current_route: Option, /// Multiaddress to the TCP Outlet pub to: String, - /// Whether the TCP Inlet is of privileged kind - pub privileged: bool, } impl TryFrom for InletStatus { @@ -123,7 +121,6 @@ impl TryFrom for InletStatus { name: status.alias, current_route: status.outlet_route.map(|r| r.to_string()), to: status.outlet_addr, - privileged: status.privileged, }) } }