diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 673dc35..470e257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: runs-on: ubuntu-latest env: SPIFFE_ENDPOINT_SOCKET: unix:/tmp/spire-agent/public/api.sock + SPIRE_ADMIN_ENDPOINT_SOCKET: unix:/tmp/spire-agent/admin/api.sock needs: build steps: - name: Check out code diff --git a/scripts/agent.conf b/scripts/agent.conf new file mode 100644 index 0000000..dbbef62 --- /dev/null +++ b/scripts/agent.conf @@ -0,0 +1,32 @@ +agent { + data_dir = "./data/agent" + log_level = "DEBUG" + trust_domain = "example.org" + server_address = "localhost" + server_port = 8081 + + # Insecure bootstrap is NOT appropriate for production use but is ok for + # simple testing/evaluation purposes. + insecure_bootstrap = true + + admin_socket_path = "$STRIPPED_SPIRE_ADMIN_ENDPOINT_SOCKET" + authorized_delegates = [ + "spiffe://example.org/myservice", + ] +} + +plugins { + KeyManager "disk" { + plugin_data { + directory = "./data/agent" + } + } + + NodeAttestor "join_token" { + plugin_data {} + } + + WorkloadAttestor "unix" { + plugin_data {} + } +} \ No newline at end of file diff --git a/scripts/run-spire.sh b/scripts/run-spire.sh index 39d5fa4..f1954ef 100755 --- a/scripts/run-spire.sh +++ b/scripts/run-spire.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + # Constants spire_version="1.7.1" spire_folder="spire-${spire_version}" @@ -35,6 +37,9 @@ mkdir -p /tmp/spire-server bin/spire-server run -config conf/server/server.conf > "${spire_server_log_file}" 2>&1 & wait_for_service "bin/spire-server healthcheck" "SPIRE Server" "${spire_server_log_file}" +export STRIPPED_SPIRE_ADMIN_ENDPOINT_SOCKET=$(echo $SPIRE_ADMIN_ENDPOINT_SOCKET| cut -c6-) +cat $SCRIPT_DIR/agent.conf | envsubst > "conf/agent/agent.conf" + # Run the SPIRE agent with the joint token bin/spire-server token generate -spiffeID ${agent_id} > token cut -d ' ' -f 2 token > token_stripped @@ -48,4 +53,13 @@ for service in "myservice" "myservice2"; do sleep 10 # Derived from the default Agent sync interval done + +uid=$(id -u) +# The UID in the test has to match this, so take the current UID and add 1 +uid_plus_one=$((uid + 1)) +# Register a different UID with the SPIFFE ID "spiffe://example.org/different-process" with a TTL of 5 seconds +bin/spire-server entry create -parentID ${agent_id} -spiffeID spiffe://example.org/different-process -selector unix:uid:${uid_plus_one} -ttl 5 +sleep 10 + + popd diff --git a/spiffe/src/workload_api/client.rs b/spiffe/src/workload_api/client.rs index 4c9bf84..a0e8381 100644 --- a/spiffe/src/workload_api/client.rs +++ b/spiffe/src/workload_api/client.rs @@ -366,7 +366,7 @@ impl WorkloadApiClient { .get(DEFAULT_SVID) .ok_or(ClientError::EmptyResponse) .and_then(|r| { - JwtSvid::from_str(&r.svid).map_err(|err| ClientError::InvalidJwtSvid(err)) + JwtSvid::from_str(&r.svid).map_err(ClientError::InvalidJwtSvid) }) } diff --git a/spire-api/Cargo.toml b/spire-api/Cargo.toml index 6347ba8..11ac0b9 100644 --- a/spire-api/Cargo.toml +++ b/spire-api/Cargo.toml @@ -15,12 +15,17 @@ categories = ["cryptography"] keywords = ["SPIFFE", "SPIRE"] [dependencies] -spiffe = { version = "0.3.1", path = "../spiffe" } +bytes = { version = "1", features = ["serde"] } +spiffe = { path = "../spiffe" } tonic = { version = "0.9", default-features = false, features = ["prost", "codegen", "transport"]} prost = { version = "0.11"} prost-types = {version = "0.11"} +tokio = { "version" = "1", features = ["net", "test-util"]} +tokio-stream = "0.1" +tower = { version = "0.4", features = ["util"] } [dev-dependencies] +once_cell = "1.18" [build-dependencies] tonic-build = { version = "0.9", default-features = false, features = ["prost"] } diff --git a/spire-api/src/agent/delegated_identity.rs b/spire-api/src/agent/delegated_identity.rs new file mode 100644 index 0000000..8fe25b0 --- /dev/null +++ b/spire-api/src/agent/delegated_identity.rs @@ -0,0 +1,392 @@ +//! This module provides an API surface to interact with the DelegateIdentity API. +//! The protobuf definition can be found [here](https://github.com/spiffe/spire-api-sdk/blob/main/proto/spire/api/agent/delegatedidentity/v1/delegatedidentity.proto) +//! +//! More information on it's usage can be found in the [SPIFFE docs](https://spiffe.io/docs/latest/deploying/spire_agent/#delegated-identity-api) +//! +//! Most importantly, this API cannot be used over the standard endpoint, it must be used over the admin socket. +//! The admin socket can be configured in the SPIRE agent configuration document. + +use crate::proto::spire::api::agent::delegatedidentity::v1::{ + delegated_identity_client::DelegatedIdentityClient as DelegatedIdentityApiClient, + FetchJwtsviDsRequest, SubscribeToX509BundlesRequest, SubscribeToX509BundlesResponse, + SubscribeToX509sviDsRequest, SubscribeToX509sviDsResponse, SubscribeToJwtBundlesRequest, + SubscribeToJwtBundlesResponse, +}; +use crate::proto::spire::api::types::Jwtsvid as ProtoJwtSvid; +use spiffe::bundle::x509::{X509Bundle, X509BundleSet}; +use spiffe::bundle::jwt::{JwtBundleSet, JwtBundle}; +use spiffe::spiffe_id::TrustDomain; +use spiffe::svid::jwt::JwtSvid; +use spiffe::svid::x509::X509Svid; +use spiffe::workload_api::address::validate_socket_path; +use tokio_stream::{Stream, StreamExt}; + +use crate::selectors::Selector; +use spiffe::workload_api::client::{ClientError, DEFAULT_SVID}; +use std::convert::{Into, TryFrom}; +use std::str::FromStr; +use tokio::net::UnixStream; +use tonic::transport::{Endpoint, Uri}; +use tower::service_fn; + +/// Name of the environment variable that holds the default socket endpoint path. +pub const ADMIN_SOCKET_ENV: &str = "SPIRE_ADMIN_ENDPOINT_SOCKET"; + +/// Gets the endpoint socket endpoint path from the environment variable `ADMIN_SOCKET_ENV`, +/// as described in [SPIFFE standard](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_Endpoint.md#4-locating-the-endpoint). +pub fn get_admin_socket_path() -> Option { + match std::env::var(ADMIN_SOCKET_ENV) { + Ok(addr) => Some(addr), + Err(_) => None, + } +} + +/// Impl for DelegatedIdentity API +#[derive(Debug, Clone)] +pub struct DelegatedIdentityClient { + client: DelegatedIdentityApiClient, +} + +/// Constructors +impl DelegatedIdentityClient { + const UNIX_PREFIX: &'static str = "unix:"; + const TONIC_DEFAULT_URI: &'static str = "http://[::]:50051"; + + /// Creates a new instance of `DelegatedIdentityClient` by connecting to the specified socket path. + /// + /// # Arguments + /// + /// * `path` - The path to the UNIX domain socket, which can optionally start with "unix:". + /// + /// # Returns + /// + /// * `Result` - Returns an instance of `DelegatedIdentityClient` if successful, otherwise returns an error. + /// + /// # Errors + /// + /// This function will return an error if the provided socket path is invalid or if there are issues connecting. + pub async fn new_from_path(path: &str) -> Result { + validate_socket_path(path)?; + + // Strip the 'unix:' prefix for tonic compatibility. + let stripped_path = path + .strip_prefix(Self::UNIX_PREFIX) + .unwrap_or(path) + .to_string(); + + let channel = Endpoint::try_from(Self::TONIC_DEFAULT_URI)? + .connect_with_connector(service_fn(move |_: Uri| { + // Connect to the UDS socket using the modified path. + UnixStream::connect(stripped_path.clone()) + })) + .await?; + + Ok(DelegatedIdentityClient { + client: DelegatedIdentityApiClient::new(channel), + }) + } + + /// Creates a new `DelegatedIdentityClient` using the default socket endpoint address. + /// + /// Requires that the environment variable `SPIFFE_ENDPOINT_SOCKET` be set with + /// the path to the Workload API endpoint socket. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if environment variable is not set or if + /// the provided socket path is not valid. + pub async fn default() -> Result { + let socket_path = match get_admin_socket_path() { + None => return Err(ClientError::MissingEndpointSocketPath), + Some(s) => s, + }; + Self::new_from_path(socket_path.as_str()).await + } + + /// Constructs a new `DelegatedIdentityClient` using the provided Tonic transport channel. + /// + /// # Arguments + /// + /// * `conn`: A `tonic::transport::Channel` used for gRPC communication. + /// + /// # Returns + /// + /// A `Result` containing a `DelegatedIdentityClient` if successful, or a `ClientError` if an error occurs. + pub fn new(conn: tonic::transport::Channel) -> Result { + Ok(DelegatedIdentityClient { + client: DelegatedIdentityApiClient::new(conn), + }) + } +} + +impl DelegatedIdentityClient { + /// Fetches a single X509 SPIFFE Verifiable Identity Document (SVID). + /// + /// This method connects to the SPIFFE Workload API and returns the first X509 SVID in the response. + /// + /// # Arguments + /// + /// * `selectors` - A list of selectors to filter the stream of [`X509Svid`] updates. + /// + /// # Returns + /// + /// On success, it returns a valid [`X509Svid`] which represents the parsed SVID. + /// If the fetch operation or the parsing fails, it returns a [`ClientError`]. + /// + /// # Errors + /// + /// Returns [`ClientError`] if the gRPC call fails or if the SVID could not be parsed from the gRPC response. + pub async fn fetch_x509_svid( + &mut self, + selectors: Vec, + ) -> Result { + let request = SubscribeToX509sviDsRequest { + selectors: selectors.into_iter().map(|s| s.into()).collect(), + }; + + self.client + .subscribe_to_x509svi_ds(request) + .await? + .into_inner() + .message() + .await? + .ok_or(ClientError::EmptyResponse) + .and_then(DelegatedIdentityClient::parse_x509_svid_from_grpc_response) + } + + /// Watches the stream of [`X509Svid`] updates. + /// + /// This function establishes a stream with the Workload API to continuously receive updates for the [`X509Svid`]. + /// The returned stream can be used to asynchronously yield new `X509Svid` updates as they become available. + /// + /// # Arguments + /// + /// * `selectors` - A list of selectors to filter the stream of [`X509Svid`] updates. + /// + /// # Returns + /// + /// Returns a stream of `Result`. Each item represents an updated [`X509Svid`] or an error if + /// there was a problem processing an update from the stream. + /// + /// # Errors + /// + /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// + /// * There's an issue connecting to the Workload API. + /// * An error occurs while setting up the stream. + /// + /// Individual stream items might also be errors if there's an issue processing the response for a specific update. + pub async fn stream_x509_svids( + &mut self, + selectors: Vec, + ) -> Result>, ClientError> { + let request = SubscribeToX509sviDsRequest { + selectors: selectors.into_iter().map(|s| s.into()).collect(), + }; + + let response: tonic::Response> = + self.client.subscribe_to_x509svi_ds(request).await?; + + let stream = response.into_inner().map(|message| { + message + .map_err(ClientError::from) + .and_then(DelegatedIdentityClient::parse_x509_svid_from_grpc_response) + }); + + Ok(stream) + } + + /// Fetches [`X509BundleSet`], that is a set of [`X509Bundle`] keyed by the trust domain to which they belong. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// there is a problem processing the response. + pub async fn fetch_x509_bundles(&mut self) -> Result { + let request = SubscribeToX509BundlesRequest::default(); + + let response: tonic::Response> = + self.client.subscribe_to_x509_bundles(request).await?; + let initial = response.into_inner().message().await?; + DelegatedIdentityClient::parse_x509_bundle_set_from_grpc_response( + initial.unwrap_or_default(), + ) + } + + /// Watches the stream of [`X509Bundle`] updates. + /// + /// This function establishes a stream with the Workload API to continuously receive updates for the [`X509Bundle`]. + /// The returned stream can be used to asynchronously yield new `X509Bundle` updates as they become available. + /// + /// # Returns + /// + /// Returns a stream of `Result`. Each item represents an updated [`X509Bundle`] or an error if + /// there was a problem processing an update from the stream. + /// + /// # Errors + /// + /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// + /// * There's an issue connecting to the Admin API. + /// * An error occurs while setting up the stream. + /// + /// Individual stream items might also be errors if there's an issue processing the response for a specific update. + pub async fn stream_x509_bundles( + &mut self, + ) -> Result>, ClientError> { + let request = SubscribeToX509BundlesRequest::default(); + + let response: tonic::Response> = + self.client.subscribe_to_x509_bundles(request).await?; + + let stream = response.into_inner().map(|message| { + message + .map_err(ClientError::from) + .and_then(DelegatedIdentityClient::parse_x509_bundle_set_from_grpc_response) + }); + + Ok(stream) + } + + /// Fetches a list of [`JwtSvid`] parsing the JWT token in the Workload API response, for the given audience and selectors. + /// + /// # Arguments + /// + /// * `audience` - A list of audiences to include in the JWT token. Cannot be empty nor contain only empty strings. + /// * `selectors` - A list of selectors to filter the list of [`JwtSvid`]. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// there is a problem processing the response. + pub async fn fetch_jwt_svids + ToString>( + &mut self, + audience: &[T], + selectors: Vec, + ) -> Result, ClientError> { + let request = FetchJwtsviDsRequest { + audience: audience.iter().map(|s| s.to_string()).collect(), + selectors: selectors.into_iter().map(|s| s.into()).collect(), + }; + + DelegatedIdentityClient::parse_jwt_svid_from_grpc_response( + self.client + .fetch_jwtsvi_ds(request) + .await? + .into_inner() + .svids, + ) + } + + + + /// Watches the stream of [`JwtBundleSet`] updates. + /// + /// This function establishes a stream with the Workload API to continuously receive updates for the [`JwtBundleSet`]. + /// The returned stream can be used to asynchronously yield new `JwtBundleSet` updates as they become available. + /// + /// # Returns + /// + /// Returns a stream of `Result`. Each item represents an updated [`JwtBundleSet`] or an error if + /// there was a problem processing an update from the stream. + /// + /// # Errors + /// + /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// + /// * There's an issue connecting to the Workload API. + /// * An error occurs while setting up the stream. + /// + /// Individual stream items might also be errors if there's an issue processing the response for a specific update. + pub async fn stream_jwt_bundles( + &mut self, + ) -> Result>, ClientError> { + let request = SubscribeToJwtBundlesRequest::default(); + let response = self.client.subscribe_to_jwt_bundles(request).await?; + Ok(response.into_inner().map(|message| { + message + .map_err(ClientError::from) + .and_then(DelegatedIdentityClient::parse_jwt_bundle_set_from_grpc_response) + })) + } + + /// Fetches [`JwtBundleSet`] that is a set of [`JwtBundle`] keyed by the trust domain to which they belong. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// there is a problem processing the response. + pub async fn fetch_jwt_bundles( + &mut self, + ) -> Result { + let request = SubscribeToJwtBundlesRequest::default(); + let response = self.client.subscribe_to_jwt_bundles(request).await?; + let initial = response.into_inner().message().await?; + DelegatedIdentityClient::parse_jwt_bundle_set_from_grpc_response(initial.ok_or(ClientError::EmptyResponse)?) + } +} + +impl DelegatedIdentityClient { + fn parse_x509_svid_from_grpc_response( + response: SubscribeToX509sviDsResponse, + ) -> Result { + let svid = response + .x509_svids + .get(DEFAULT_SVID) + .ok_or(ClientError::EmptyResponse)?; + + let x509_svid = svid.x509_svid.as_ref().ok_or(ClientError::EmptyResponse)?; + + let total_length = x509_svid.cert_chain.iter().map(|c| c.len()).sum(); + let mut cert_chain_bytes = Vec::with_capacity(total_length); + + for c in &x509_svid.cert_chain { + cert_chain_bytes.extend_from_slice(c); + } + + X509Svid::parse_from_der(&cert_chain_bytes, svid.x509_svid_key.as_ref()) + .map_err(|e| e.into()) + } + + fn parse_jwt_svid_from_grpc_response( + svids: Vec, + ) -> Result, ClientError> { + let result: Result, ClientError> = svids + .iter() + .map(|r| JwtSvid::from_str(&r.token).map_err(ClientError::InvalidJwtSvid)) + .collect(); + result + } + + fn parse_jwt_bundle_set_from_grpc_response( + response: SubscribeToJwtBundlesResponse, + ) -> Result { + let mut bundle_set = JwtBundleSet::new(); + + for (td, bundle_data) in response.bundles.into_iter() { + let trust_domain = TrustDomain::try_from(td)?; + let bundle = JwtBundle::from_jwt_authorities(trust_domain, &bundle_data) + .map_err(ClientError::from)?; + + bundle_set.add_bundle(bundle); + } + + Ok(bundle_set) + } + + fn parse_x509_bundle_set_from_grpc_response( + response: SubscribeToX509BundlesResponse, + ) -> Result { + let mut bundle_set = X509BundleSet::new(); + + for (td, bundle) in response.ca_certificates.into_iter() { + let trust_domain = TrustDomain::try_from(td)?; + + bundle_set.add_bundle( + X509Bundle::parse_from_der(trust_domain, &bundle) + .map_err(ClientError::InvalidX509Bundle)?, + ); + } + Ok(bundle_set) + } +} diff --git a/spire-api/src/agent/mod.rs b/spire-api/src/agent/mod.rs new file mode 100644 index 0000000..23dbc96 --- /dev/null +++ b/spire-api/src/agent/mod.rs @@ -0,0 +1,10 @@ +//! Agent API +//! +//! Consists of the following APIs: +//! - `delegated_identity`: For managing delegated identities. +//! - `debug`: (Not yet implemented). +//! +//! # Note +//! Access these APIs via the `admin_socket_path` in the [agent configuration file](https://spiffe.io/docs/latest/deploying/spire_agent/#agent-configuration-file). +//! +pub mod delegated_identity; diff --git a/spire-api/src/lib.rs b/spire-api/src/lib.rs index 95f1441..afb9a58 100644 --- a/spire-api/src/lib.rs +++ b/spire-api/src/lib.rs @@ -1 +1,10 @@ +#![deny(missing_docs)] +#![warn(missing_debug_implementations)] +// #![warn(rust_2018_idioms)] + +//! This library provides functions to interact with the SPIRE GRPC APIs as defined in the [SDK](https://github.com/spiffe/spire-api-sdk). + mod proto; + +pub mod agent; +pub mod selectors; diff --git a/spire-api/src/selectors.rs b/spire-api/src/selectors.rs new file mode 100644 index 0000000..ea023c8 --- /dev/null +++ b/spire-api/src/selectors.rs @@ -0,0 +1,129 @@ +//! Selectors conforming to SPIRE standards. +use crate::proto::spire::api::types::Selector as SpiffeSelector; + +const K8S_TYPE: &str = "k8s"; +const UNIX_TYPE: &str = "unix"; + +/// Converts user-defined selectors into SPIFFE selectors. +impl From for SpiffeSelector { + fn from(s: Selector) -> SpiffeSelector { + match s { + Selector::K8s(k8s_selector) => SpiffeSelector { + r#type: K8S_TYPE.to_string(), + value: k8s_selector.into(), + }, + Selector::Unix(unix_selector) => SpiffeSelector { + r#type: UNIX_TYPE.to_string(), + value: unix_selector.into(), + }, + Selector::Generic((k, v)) => SpiffeSelector { + r#type: k, + value: v, + }, + } + } +} + +#[derive(Debug, Clone)] +/// Represents various types of SPIFFE identity selectors. +pub enum Selector { + /// Represents a SPIFFE identity selector based on Kubernetes constructs. + K8s(K8s), + /// Represents a SPIFFE identity selector based on Unix system constructs such as PID, GID, and UID. + Unix(Unix), + /// Represents a generic SPIFFE identity selector defined by a key-value pair. + Generic((String, String)), +} + +const K8S_SA_TYPE: &str = "sa"; +const K8S_NS_TYPE: &str = "ns"; + +/// Converts Kubernetes selectors to their string representation. +impl From for String { + fn from(k: K8s) -> String { + match k { + K8s::ServiceAccount(s) => format!("{}:{}", K8S_SA_TYPE, s), + K8s::Namespace(s) => format!("{}:{}", K8S_NS_TYPE, s), + } + } +} + +#[derive(Debug, Clone)] +/// Represents a SPIFFE identity selector for Kubernetes. +pub enum K8s { + /// SPIFFE identity selector for a Kubernetes service account. + ServiceAccount(String), + /// SPIFFE identity selector for a Kubernetes namespace. + Namespace(String), +} + +const UNIX_PID_TYPE: &str = "pid"; +const UNIX_GID_TYPE: &str = "gid"; +const UNIX_UID_TYPE: &str = "uid"; + +/// Converts a Unix selector into a formatted string representation. +impl From for String { + fn from(value: Unix) -> Self { + match value { + Unix::Pid(s) => format!("{}:{}", UNIX_PID_TYPE, s), + Unix::Gid(s) => format!("{}:{}", UNIX_GID_TYPE, s), + Unix::Uid(s) => format!("{}:{}", UNIX_UID_TYPE, s), + } + } +} + +#[derive(Debug, Clone)] +/// Represents SPIFFE identity selectors based on Unix process-related attributes. +pub enum Unix { + /// Specifies a selector for a Unix process ID (PID). + Pid(u16), + /// Specifies a selector for a Unix group ID (GID). + Gid(u16), + /// Specifies a selector for a Unix user ID (UID). + Uid(u16), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_k8s_sa_selector() { + let selector = Selector::K8s(K8s::ServiceAccount("foo".to_string())); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, K8S_TYPE); + assert_eq!(spiffe_selector.value, "sa:foo"); + } + + #[test] + fn test_k8s_ns_selector() { + let selector = Selector::K8s(K8s::Namespace("foo".to_string())); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, K8S_TYPE); + assert_eq!(spiffe_selector.value, "ns:foo"); + } + + #[test] + fn test_unix_pid_selector() { + let selector = Selector::Unix(Unix::Pid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "pid:500"); + } + + #[test] + fn test_unix_gid_selector() { + let selector = Selector::Unix(Unix::Gid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "gid:500"); + } + + #[test] + fn test_unix_uid_selector() { + let selector = Selector::Unix(Unix::Uid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "uid:500"); + } +} diff --git a/spire-api/tests/delegated_identity_api_client_test.rs b/spire-api/tests/delegated_identity_api_client_test.rs index e1ed519..9173995 100644 --- a/spire-api/tests/delegated_identity_api_client_test.rs +++ b/spire-api/tests/delegated_identity_api_client_test.rs @@ -1,10 +1,179 @@ -// These tests requires a running SPIRE server and agent (see `scripts/run-spire.sh`). +// These tests requires a running SPIRE server and agent with workloads registered (see script `ci.sh`). +// In addition it requires the admin endpoint to be exposed, and the running user to registered +// as an authorized_delegate. #[cfg(feature = "integration-tests")] mod integration_tests { + use once_cell::sync::Lazy; + use spiffe::bundle::BundleRefSource; + use spiffe::bundle::jwt::JwtBundleSet; + use spiffe::spiffe_id::TrustDomain; + use spire_api::selectors; + use std::process::Command; + use tokio_stream::StreamExt; + use spire_api::agent::delegated_identity::DelegatedIdentityClient; - #[test] - fn dummy_test() { - assert!(true); + static TRUST_DOMAIN: Lazy = Lazy::new(|| TrustDomain::new("example.org").unwrap()); + + fn get_uid() -> u16 { + let mut uid = String::from_utf8( + Command::new("id") + .arg("-u") + .output() + .expect("could not get UID") + .stdout, + ) + .expect("could not parse to string"); + uid.truncate(uid.len() - 1); + uid.parse().expect("could not parse uid to number") + } + + async fn get_client() -> DelegatedIdentityClient { + DelegatedIdentityClient::default() + .await + .expect("failed to create client") + } + + #[tokio::test] + async fn fetch_delegate_jwt_svid() { + let mut client = get_client().await; + let svid = client + .fetch_jwt_svids( + &["my_audience"], + vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))], + ) + .await + .expect("Failed to fetch JWT SVID"); + assert_eq!(svid.len(), 1); + assert_eq!(svid[0].audience(), &["my_audience"]); + } + + #[tokio::test] + async fn fetch_delegate_x509_svid() { + let mut client = get_client().await; + let response: spiffe::svid::x509::X509Svid = client + .fetch_x509_svid(vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))]) + .await + .expect("Failed to fetch delegate SVID"); + // Not checking the chain as the root is generated by spire. + // In the future we could look in the downloaded spire directory for the keys. + assert_eq!(response.cert_chain().len(), 1); + assert_eq!( + response.spiffe_id().to_string(), + "spiffe://example.org/different-process" + ); + } + + #[tokio::test] + async fn stream_delegate_x509_svid() { + let test_duration = std::time::Duration::from_secs(60); + let mut client = get_client().await; + let mut stream = client + .stream_x509_svids(vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))]) + .await + .expect("Failed to fetch delegate SVID"); + + let result = tokio::time::timeout(test_duration, stream.next()) + .await + .expect("Test did not complete in the expected duration"); + let response = result.expect("empty result").expect("error in stream"); + // Not checking the chain as the root is generated by spire. + // In the future we could look in the downloaded spire directory for the keys. + assert_eq!(response.cert_chain().len(), 1); + assert_eq!( + response.spiffe_id().to_string(), + "spiffe://example.org/different-process" + ); + } + + #[tokio::test] + async fn fetch_delegated_x509_trust_bundles() { + let mut client = get_client().await; + let response = client + .fetch_x509_bundles() + .await + .expect("Failed to fetch trust bundles"); + response + .get_bundle(&*TRUST_DOMAIN) + .expect("Failed to get bundle"); + } + + #[tokio::test] + async fn stream_delegated_x509_trust_bundles() { + let test_duration = std::time::Duration::from_secs(60); + let mut client = get_client().await; + let mut stream = client + .stream_x509_bundles() + .await + .expect("Failed to fetch trust bundles"); + + let result = tokio::time::timeout(test_duration, stream.next()) + .await + .expect("Test did not complete in the expected duration"); + let response = result.expect("empty result").expect("error in stream"); + response + .get_bundle(&*TRUST_DOMAIN) + .expect("Failed to get bundle"); + } + + async fn verify_jwt( + client: &mut DelegatedIdentityClient, + bundles: JwtBundleSet, + ) { + let svids = client + .fetch_jwt_svids( + &["my_audience"], + vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))], + ) + .await + .expect("Failed to fetch JWT SVID"); + let svid = svids.first().expect("no items in jwt bundle list"); + let key_id = svid.key_id(); + + let bundle = bundles.get_bundle_for_trust_domain(&*TRUST_DOMAIN); + let bundle = bundle + .expect("Bundle was None") + .expect("Failed to unwrap bundle"); + assert_eq!(bundle.trust_domain(), &*TRUST_DOMAIN); + assert_eq!( + bundle.find_jwt_authority(key_id).unwrap().key_id, + Some(key_id.to_string()) + ); + } + + #[tokio::test] + async fn fetch_delegated_jwt_trust_bundles() { + let mut client = get_client().await; + let response = client + .fetch_jwt_bundles() + .await + .expect("Failed to fetch trust bundles"); + + + verify_jwt(&mut client, response).await; + } + + #[tokio::test] + async fn stream_delegated_jwt_trust_bundles() { + let mut client = get_client().await; + let test_duration = std::time::Duration::from_secs(60); + let mut stream = client + .stream_jwt_bundles() + .await + .expect("Failed to fetch trust bundles"); + + let result = tokio::time::timeout(test_duration, stream.next()) + .await + .expect("Test did not complete in the expected duration"); + + verify_jwt(&mut client, result.expect("empty result").expect("error in stream")).await; } }