diff --git a/common/src/backends/client/gateway.rs b/common/src/backends/client/gateway.rs index 79809a906..00b19e661 100644 --- a/common/src/backends/client/gateway.rs +++ b/common/src/backends/client/gateway.rs @@ -36,6 +36,16 @@ impl Client { /// Interact with all the data relating to projects #[allow(async_fn_in_trait)] pub trait ProjectsDal { + /// Get a user project + async fn get_user_project( + &self, + user_token: &str, + project_name: &str, + ) -> Result; + + /// Check the HEAD of a user project + async fn head_user_project(&self, user_token: &str, project_name: &str) -> Result; + /// Get the projects that belong to a user async fn get_user_projects( &self, @@ -56,22 +66,49 @@ pub trait ProjectsDal { } impl ProjectsDal for Client { + #[instrument(skip_all)] + async fn get_user_project( + &self, + user_token: &str, + project_name: &str, + ) -> Result { + self.public_client + .request( + Method::GET, + format!("projects/{}", project_name).as_str(), + None::<()>, + Some(Authorization::bearer(user_token).expect("to build an authorization bearer")), + ) + .await + } + + #[instrument(skip_all)] + async fn head_user_project(&self, user_token: &str, project_name: &str) -> Result { + self.public_client + .request_raw( + Method::HEAD, + format!("projects/{}", project_name).as_str(), + None::<()>, + Some(Authorization::bearer(user_token).expect("to build an authorization bearer")), + ) + .await?; + + Ok(true) + } + #[instrument(skip_all)] async fn get_user_projects( &self, user_token: &str, ) -> Result, Error> { - let projects = self - .public_client + self.public_client .request( Method::GET, "projects", None::<()>, Some(Authorization::bearer(user_token).expect("to build an authorization bearer")), ) - .await?; - - Ok(projects) + .await } } diff --git a/common/src/backends/client/mod.rs b/common/src/backends/client/mod.rs index 8d6ce0b67..5b7ca299a 100644 --- a/common/src/backends/client/mod.rs +++ b/common/src/backends/client/mod.rs @@ -1,3 +1,4 @@ +use bytes::Bytes; use headers::{ContentType, Header, HeaderMapExt}; use http::{Method, Request, StatusCode, Uri}; use hyper::{body, client::HttpConnector, Body, Client}; @@ -36,7 +37,6 @@ pub struct ServicesApiClient { } impl ServicesApiClient { - /// Make a new client that connects to the given endpoint fn new(base: Uri) -> Self { Self { client: Client::new(), @@ -44,7 +44,6 @@ impl ServicesApiClient { } } - /// Make a get request to a path on the service pub async fn request( &self, method: Method, @@ -52,6 +51,19 @@ impl ServicesApiClient { body: Option, extra_header: Option, ) -> Result { + let bytes = self.request_raw(method, path, body, extra_header).await?; + let json = serde_json::from_slice(&bytes)?; + + Ok(json) + } + + pub async fn request_raw( + &self, + method: Method, + path: &str, + body: Option, + extra_header: Option, + ) -> Result { let uri = format!("{}{path}", self.base); trace!(uri, "calling inner service"); @@ -59,15 +71,14 @@ impl ServicesApiClient { let headers = req .headers_mut() .expect("new request to have mutable headers"); - - headers.typed_insert(ContentType::json()); - if let Some(extra_header) = extra_header { headers.typed_insert(extra_header); } + if body.is_some() { + headers.typed_insert(ContentType::json()); + } let cx = Span::current().context(); - global::get_text_map_propagator(|propagator| { propagator.inject_context(&cx, &mut HeaderInjector(req.headers_mut().unwrap())) }); @@ -79,18 +90,15 @@ impl ServicesApiClient { }; let resp = self.client.request(req?).await?; - trace!(response = ?resp, "Load response"); if resp.status() != StatusCode::OK { return Err(Error::RequestError(resp.status())); } - let body = resp.into_body(); - let bytes = body::to_bytes(body).await?; - let json = serde_json::from_slice(&bytes)?; + let bytes = body::to_bytes(resp.into_body()).await?; - Ok(json) + Ok(bytes) } } diff --git a/common/src/backends/mod.rs b/common/src/backends/mod.rs index 62fdf62b7..f595fb5ac 100644 --- a/common/src/backends/mod.rs +++ b/common/src/backends/mod.rs @@ -1,6 +1,6 @@ use tracing::instrument; -use crate::claims::{Claim, Scope}; +use crate::claims::{AccountTier, Claim, Scope}; use self::client::{ProjectsDal, ResourceDal}; @@ -17,6 +17,7 @@ pub mod trace; pub trait ClaimExt { /// Verify that the [Claim] has the [Scope::Admin] scope. fn is_admin(&self) -> bool; + fn is_deployer(&self) -> bool; /// Verify that the user's current project count is lower than the account limit in [Claim::limits]. fn can_create_project(&self, current_count: u32) -> bool; /// Verify that the user has permission to provision RDS instances. @@ -31,12 +32,21 @@ pub trait ClaimExt { projects_dal: &G, project_name: &str, ) -> Result; + /// Verify if the claim subject has ownership of a project. + async fn owns_project_id( + &self, + projects_dal: &G, + project_id: &str, + ) -> Result; } impl ClaimExt for Claim { fn is_admin(&self) -> bool { self.scopes.contains(&Scope::Admin) } + fn is_deployer(&self) -> bool { + self.tier == AccountTier::Deployer + } fn can_create_project(&self, current_count: u32) -> bool { self.is_admin() || self.limits.project_limit() > current_count @@ -71,7 +81,17 @@ impl ClaimExt for Claim { project_name: &str, ) -> Result { let token = self.token.as_ref().expect("token to be set"); - let projects = projects_dal.get_user_projects(token).await?; - Ok(projects.iter().any(|project| project.name == project_name)) + projects_dal.head_user_project(token, project_name).await + } + + #[instrument(skip_all)] + async fn owns_project_id( + &self, + projects_dal: &G, + project_id: &str, + ) -> Result { + let token = self.token.as_ref().expect("token to be set"); + let projects = projects_dal.get_user_project_ids(token).await?; + Ok(projects.iter().any(|id| id == project_id)) } } diff --git a/deployer/src/persistence/mod.rs b/deployer/src/persistence/mod.rs index 6ecc868d9..e61ff5b2f 100644 --- a/deployer/src/persistence/mod.rs +++ b/deployer/src/persistence/mod.rs @@ -8,8 +8,8 @@ use shuttle_common::{claims::Claim, resource::Type}; use shuttle_proto::{ provisioner::{self, DatabaseRequest}, resource_recorder::{ - self, record_request, RecordRequest, ResourceIds, ResourceResponse, ResourcesResponse, - ResultResponse, ServiceResourcesRequest, + self, record_request, ProjectResourcesRequest, RecordRequest, ResourceIds, + ResourceResponse, ResourcesResponse, ResultResponse, }, }; use sqlx::{ @@ -364,17 +364,18 @@ impl ResourceManager for Persistence { service_id: &Ulid, claim: Claim, ) -> Result { - let mut req = tonic::Request::new(ServiceResourcesRequest { - service_id: service_id.to_string(), + let mut req = tonic::Request::new(ProjectResourcesRequest { + project_id: self.project_id.to_string(), }); + req.extensions_mut().insert(claim.clone()); - info!(%service_id, "Getting resources from resource-recorder"); + info!(%self.project_id, "Getting resources from resource-recorder"); let res = self .resource_recorder_client .as_mut() .expect("to have the resource recorder set up") - .get_service_resources(req) + .get_project_resources(req) .await .map_err(PersistenceError::ResourceRecorder) .map(|res| res.into_inner())?; @@ -384,8 +385,7 @@ impl ResourceManager for Persistence { info!("Got no resources from resource-recorder"); // Check if there are cached resources on the local persistence. let resources: std::result::Result, sqlx::Error> = - sqlx::query_as("SELECT * FROM resources WHERE service_id = ?") - .bind(service_id.to_string()) + sqlx::query_as("SELECT * FROM resources") .fetch_all(&self.pool) .await; @@ -410,9 +410,10 @@ impl ResourceManager for Persistence { self.insert_resources(local_resources, service_id, claim.clone()) .await?; - let mut req = tonic::Request::new(ServiceResourcesRequest { - service_id: service_id.to_string(), + let mut req = tonic::Request::new(ProjectResourcesRequest { + project_id: self.project_id.to_string(), }); + req.extensions_mut().insert(claim); info!("Getting resources from resource-recorder again"); @@ -420,7 +421,7 @@ impl ResourceManager for Persistence { .resource_recorder_client .as_mut() .expect("to have the resource recorder set up") - .get_service_resources(req) + .get_project_resources(req) .await .map_err(PersistenceError::ResourceRecorder) .map(|res| res.into_inner())?; @@ -433,8 +434,7 @@ impl ResourceManager for Persistence { info!("Deleting local resources"); // Now that we know that the resources are in resource-recorder, // we can safely delete them from here to prevent de-sync issues and to not hinder project deletion - sqlx::query("DELETE FROM resources WHERE service_id = ?") - .bind(service_id.to_string()) + sqlx::query("DELETE FROM resources") .execute(&self.pool) .await?; diff --git a/proto/resource-recorder.proto b/proto/resource-recorder.proto index 7d37dadf6..1ae89ea5f 100644 --- a/proto/resource-recorder.proto +++ b/proto/resource-recorder.proto @@ -10,7 +10,7 @@ service ResourceRecorder { // Get the resources belonging to a project rpc GetProjectResources(ProjectResourcesRequest) returns (ResourcesResponse); - // Get the resources belonging to a service + // Discontinued rpc GetServiceResources(ServiceResourcesRequest) returns (ResourcesResponse); // Get a resource diff --git a/proto/src/generated/resource_recorder.rs b/proto/src/generated/resource_recorder.rs index 4115b835f..9e206b9d9 100644 --- a/proto/src/generated/resource_recorder.rs +++ b/proto/src/generated/resource_recorder.rs @@ -219,7 +219,7 @@ pub mod resource_recorder_client { )); self.inner.unary(req, path, codec).await } - /// Get the resources belonging to a service + /// Discontinued pub async fn get_service_resources( &mut self, request: impl tonic::IntoRequest, @@ -304,7 +304,7 @@ pub mod resource_recorder_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; - /// Get the resources belonging to a service + /// Discontinued async fn get_service_resources( &self, request: tonic::Request, diff --git a/provisioner/src/lib.rs b/provisioner/src/lib.rs index e1369aad8..c73121e2d 100644 --- a/provisioner/src/lib.rs +++ b/provisioner/src/lib.rs @@ -14,7 +14,7 @@ use rand::Rng; use shuttle_common::backends::auth::VerifyClaim; use shuttle_common::backends::client::gateway; use shuttle_common::backends::ClaimExt; -use shuttle_common::claims::Scope; +use shuttle_common::claims::{Claim, Scope}; use shuttle_common::models::project::ProjectName; pub use shuttle_proto::provisioner::provisioner_server::ProvisionerServer; use shuttle_proto::provisioner::{ @@ -460,6 +460,21 @@ impl ShuttleProvisioner { Ok(DatabaseDeletionResponse {}) } + + async fn verify_ownership(&self, claim: &Claim, project_name: &str) -> Result<(), Status> { + if !claim.is_admin() + && !claim.is_deployer() + && !claim + .owns_project(&self.gateway_client, project_name) + .await + .map_err(|_| Status::internal("could not verify project ownership"))? + { + let status = Status::permission_denied("the request lacks the authorizations"); + error!(error = &status as &dyn std::error::Error); + return Err(status); + } + Ok(()) + } } #[tonic::async_trait] @@ -470,24 +485,12 @@ impl Provisioner for ShuttleProvisioner { request: Request, ) -> Result, Status> { request.verify(Scope::ResourcesWrite)?; - let claim = request.get_claim()?; - let request = request.into_inner(); if !ProjectName::is_valid(&request.project_name) { return Err(Status::invalid_argument("invalid project name")); } - - // Check project ownership. - if !claim - .owns_project(&self.gateway_client, &request.project_name) - .await - .map_err(|_| Status::internal("can not verify project ownership"))? - { - let status = Status::permission_denied("the request lacks the authorizations"); - error!(error = &status as &dyn std::error::Error); - return Err(status); - } + self.verify_ownership(&claim, &request.project_name).await?; let db_type = request.db_type.unwrap(); @@ -539,22 +542,11 @@ impl Provisioner for ShuttleProvisioner { ) -> Result, Status> { request.verify(Scope::ResourcesWrite)?; let claim = request.get_claim()?; - let request = request.into_inner(); if !ProjectName::is_valid(&request.project_name) { return Err(Status::invalid_argument("invalid project name")); } - - // Check project ownership. - if !claim - .owns_project(&self.gateway_client, &request.project_name) - .await - .map_err(|_| Status::internal("can not verify project ownership"))? - { - let status = Status::permission_denied("the request lacks the authorizations"); - error!(error = &status as &dyn std::error::Error); - return Err(status); - } + self.verify_ownership(&claim, &request.project_name).await?; let db_type = request.db_type.unwrap(); diff --git a/resource-recorder/Cargo.toml b/resource-recorder/Cargo.toml index 6f7853ec0..d94da8ee2 100644 --- a/resource-recorder/Cargo.toml +++ b/resource-recorder/Cargo.toml @@ -26,4 +26,5 @@ ulid = { workspace = true } portpicker = { workspace = true } pretty_assertions = { workspace = true } serde_json = { workspace = true } +shuttle-common = { workspace = true, features = ["test-utils"] } shuttle-common-tests = { workspace = true } diff --git a/resource-recorder/src/args.rs b/resource-recorder/src/args.rs index f246c54b2..319d00419 100644 --- a/resource-recorder/src/args.rs +++ b/resource-recorder/src/args.rs @@ -16,4 +16,8 @@ pub struct Args { /// Address to reach the authentication service at #[arg(long, default_value = "http://127.0.0.1:8008")] pub auth_uri: Uri, + + /// Address to reach gateway's control plane at + #[clap(long, default_value = "http://gateway:8001")] + pub gateway_uri: Uri, } diff --git a/resource-recorder/src/dal.rs b/resource-recorder/src/dal.rs index 4daa81539..9ad8aeb56 100644 --- a/resource-recorder/src/dal.rs +++ b/resource-recorder/src/dal.rs @@ -59,9 +59,6 @@ pub trait Dal { /// Get the resources that belong to a project async fn get_project_resources(&self, project_id: Ulid) -> Result, DalError>; - /// Get the resources that belong to a service - async fn get_service_resources(&self, service_id: Ulid) -> Result, DalError>; - /// Get a resource async fn get_resource( &self, @@ -181,15 +178,6 @@ impl Dal for Sqlite { Ok(result) } - async fn get_service_resources(&self, service_id: Ulid) -> Result, DalError> { - let result = sqlx::query_as(r#"SELECT * FROM resources WHERE service_id = ?"#) - .bind(service_id.to_string()) - .fetch_all(&self.pool) - .await?; - - Ok(result) - } - async fn get_resource( &self, resource: resource_recorder::ResourceIds, diff --git a/resource-recorder/src/lib.rs b/resource-recorder/src/lib.rs index 512fe15cd..072f897f8 100644 --- a/resource-recorder/src/lib.rs +++ b/resource-recorder/src/lib.rs @@ -1,7 +1,10 @@ use async_trait::async_trait; use dal::{Dal, DalError, Resource}; use prost_types::TimestampError; -use shuttle_common::{backends::auth::VerifyClaim, claims::Scope}; +use shuttle_common::{ + backends::{auth::VerifyClaim, client::gateway, ClaimExt}, + claims::{Claim, Scope}, +}; use shuttle_proto::resource_recorder::{ self, resource_recorder_server::ResourceRecorder, ProjectResourcesRequest, RecordRequest, ResourceIds, ResourceResponse, ResourcesResponse, ResultResponse, ServiceResourcesRequest, @@ -42,14 +45,18 @@ impl From for Error { pub struct Service { dal: D, + gateway_client: gateway::Client, } impl Service where D: Dal + Send + Sync + 'static, { - pub fn new(dal: D) -> Self { - Self { dal } + pub fn new(dal: D, gateway_client: gateway::Client) -> Self { + Self { + dal, + gateway_client, + } } /// Record the addition of a new resource @@ -87,18 +94,6 @@ where Ok(resources.into_iter().map(Into::into).collect()) } - /// Get the resources that belong to a service - async fn service_resources( - &self, - service_id: String, - ) -> Result, Error> { - tracing::info!("fetching resources for service"); - - let resources = self.dal.get_service_resources(service_id.parse()?).await?; - - Ok(resources.into_iter().map(Into::into).collect()) - } - /// Get a resource async fn get_resource( &self, @@ -120,6 +115,21 @@ where Ok(()) } + + async fn verify_ownership(&self, claim: &Claim, project_id: &str) -> Result<(), Status> { + if !claim.is_admin() + && !claim.is_deployer() + && !claim + .owns_project_id(&self.gateway_client, project_id) + .await + .map_err(|_| Status::internal("could not verify project ownership"))? + { + let status = Status::permission_denied("the request lacks the authorizations"); + error!(error = &status as &dyn std::error::Error); + return Err(status); + } + Ok(()) + } } #[async_trait] @@ -133,8 +143,9 @@ where request: Request, ) -> Result, Status> { request.verify(Scope::ResourcesWrite)?; - + let claim = request.get_claim()?; let request = request.into_inner(); + self.verify_ownership(&claim, &request.project_id).await?; let result = match self.add(request).await { Ok(()) => ResultResponse { @@ -156,8 +167,10 @@ where request: Request, ) -> Result, Status> { request.verify(Scope::Resources)?; - + let claim = request.get_claim()?; let request = request.into_inner(); + self.verify_ownership(&claim, &request.project_id).await?; + let result = match self.project_resources(request.project_id).await { Ok(resources) => ResourcesResponse { success: true, @@ -177,25 +190,11 @@ where #[tracing::instrument(skip(self))] async fn get_service_resources( &self, - request: Request, + _request: Request, ) -> Result, Status> { - request.verify(Scope::Resources)?; - - let request = request.into_inner(); - let result = match self.service_resources(request.service_id).await { - Ok(resources) => ResourcesResponse { - success: true, - message: Default::default(), - resources, - }, - Err(e) => ResourcesResponse { - success: false, - message: e.to_string(), - resources: Vec::new(), - }, - }; - - Ok(Response::new(result)) + Err(Status::not_found( + "This resource endpoint is discontinued. Please restart your project.", + )) } #[tracing::instrument(skip(self))] @@ -204,8 +203,10 @@ where request: tonic::Request, ) -> Result, Status> { request.verify(Scope::Resources)?; - + let claim = request.get_claim()?; let request = request.into_inner(); + self.verify_ownership(&claim, &request.project_id).await?; + let result = match self.get_resource(request).await { Ok(resource) => ResourceResponse { success: true, @@ -215,7 +216,7 @@ where Err(e) => ResourceResponse { success: false, message: e.to_string(), - resource: Default::default(), + resource: None, }, }; @@ -228,8 +229,10 @@ where request: tonic::Request, ) -> Result, Status> { request.verify(Scope::ResourcesWrite)?; - + let claim = request.get_claim()?; let request = request.into_inner(); + self.verify_ownership(&claim, &request.project_id).await?; + let result = match self.delete_resource(request).await { Ok(()) => ResultResponse { success: true, diff --git a/resource-recorder/src/main.rs b/resource-recorder/src/main.rs index f2989d5b4..066c66060 100644 --- a/resource-recorder/src/main.rs +++ b/resource-recorder/src/main.rs @@ -4,6 +4,7 @@ use clap::Parser; use shuttle_common::{ backends::{ auth::{AuthPublicKey, JwtAuthenticationLayer}, + client::gateway, trace::setup_tracing, }, extract_propagation::ExtractPropagationLayer, @@ -12,27 +13,32 @@ use shuttle_common::{ use shuttle_proto::resource_recorder::resource_recorder_server::ResourceRecorderServer; use shuttle_resource_recorder::{args::Args, Service, Sqlite}; use tonic::transport::Server; -use tracing::trace; #[tokio::main] async fn main() { - let args = Args::parse(); + let Args { + address, + state, + auth_uri, + gateway_uri, + } = Args::parse(); setup_tracing(tracing_subscriber::registry(), Backend::ResourceRecorder); - trace!(args = ?args, "parsed args"); - let mut server_builder = Server::builder() .http2_keepalive_interval(Some(Duration::from_secs(60))) - .layer(JwtAuthenticationLayer::new(AuthPublicKey::new( - args.auth_uri, - ))) + .layer(JwtAuthenticationLayer::new(AuthPublicKey::new(auth_uri))) .layer(ExtractPropagationLayer); - let db_path = args.state.join("resource-recorder.sqlite"); - let svc = Service::new(Sqlite::new(db_path.display().to_string().as_str()).await); + let gateway_client = gateway::Client::new(gateway_uri.clone(), gateway_uri); + + let db_path = state.join("resource-recorder.sqlite"); + let svc = Service::new( + Sqlite::new(db_path.display().to_string().as_str()).await, + gateway_client, + ); let svc = ResourceRecorderServer::new(svc); let router = server_builder.add_service(svc); - router.serve(args.address).await.unwrap(); + router.serve(address).await.unwrap(); } diff --git a/resource-recorder/tests/integration.rs b/resource-recorder/tests/integration.rs index aaaf54e9e..61af390cc 100644 --- a/resource-recorder/tests/integration.rs +++ b/resource-recorder/tests/integration.rs @@ -3,12 +3,14 @@ use std::net::{Ipv4Addr, SocketAddr}; use portpicker::pick_unused_port; use pretty_assertions::{assert_eq, assert_ne}; use serde_json::json; -use shuttle_common::claims::Scope; +use shuttle_common::{ + backends::client::gateway::Client, claims::Scope, test_utils::get_mocked_gateway_server, +}; use shuttle_common_tests::JwtScopesLayer; use shuttle_proto::resource_recorder::{ record_request, resource_recorder_client::ResourceRecorderClient, resource_recorder_server::ResourceRecorderServer, ProjectResourcesRequest, RecordRequest, - Resource, ResourceIds, ResourcesResponse, ResultResponse, ServiceResourcesRequest, + Resource, ResourceIds, ResourcesResponse, ResultResponse, }; use shuttle_resource_recorder::{Service, Sqlite}; use tokio::select; @@ -20,6 +22,9 @@ async fn manage_resources() { let port = pick_unused_port().unwrap(); let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port); + let server = get_mocked_gateway_server().await; + let client = Client::new(server.uri().parse().unwrap(), server.uri().parse().unwrap()); + let server_future = async { Server::builder() .layer(JwtScopesLayer::new(vec![ @@ -28,6 +33,7 @@ async fn manage_resources() { ])) .add_service(ResourceRecorderServer::new(Service::new( Sqlite::new_in_memory().await, + client, ))) .serve(addr) .await @@ -43,29 +49,33 @@ async fn manage_resources() { .unwrap(); let project_id = Ulid::new().to_string(); - let service_id = Ulid::new().to_string(); + let service_id = "id1".to_string(); + + let mut req = Request::new(RecordRequest { + project_id: project_id.clone(), + service_id: service_id.clone(), + resources: vec![ + record_request::Resource { + r#type: "database::shared::postgres".to_string(), + config: serde_json::to_vec(&json!({"public": true})).unwrap(), + data: serde_json::to_vec(&json!({"username": "test"})).unwrap(), + }, + record_request::Resource { + r#type: "secrets".to_string(), + config: serde_json::to_vec(&json!({})).unwrap(), + data: serde_json::to_vec(&json!({"password": "brrrr"})).unwrap(), + }, + ], + }); + req.metadata_mut().insert( + "authorization", + format!("Bearer user-1") + .parse() + .expect("to construct a bearer token"), + ); // Add resources for on service - let response = client - .record_resources(Request::new(RecordRequest { - project_id: project_id.clone(), - service_id: service_id.clone(), - resources: vec![ - record_request::Resource { - r#type: "database::shared::postgres".to_string(), - config: serde_json::to_vec(&json!({"public": true})).unwrap(), - data: serde_json::to_vec(&json!({"username": "test"})).unwrap(), - }, - record_request::Resource { - r#type: "secrets".to_string(), - config: serde_json::to_vec(&json!({})).unwrap(), - data: serde_json::to_vec(&json!({"password": "brrrr"})).unwrap(), - }, - ], - })) - .await - .unwrap() - .into_inner(); + let response = client.record_resources(req).await.unwrap().into_inner(); let expected = ResultResponse { success: true, @@ -75,7 +85,7 @@ async fn manage_resources() { assert_eq!(response, expected); // Add resources for another service on same project - let service_id2 = Ulid::new().to_string(); + let service_id2 = "id2".to_string(); let response = client .record_resources(Request::new(RecordRequest { @@ -95,7 +105,7 @@ async fn manage_resources() { // Add resources to a new project let project_id2 = Ulid::new().to_string(); - let service_id3 = Ulid::new().to_string(); + let service_id3 = "id3".to_string(); let response = client .record_resources(Request::new(RecordRequest { @@ -115,8 +125,8 @@ async fn manage_resources() { // Fetching resources for a service let response = client - .get_service_resources(Request::new(ServiceResourcesRequest { - service_id: service_id.clone(), + .get_project_resources(Request::new(ProjectResourcesRequest { + project_id: project_id.clone(), })) .await .unwrap()