Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion capsules/services/browser/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ function renderServices(services) {
available: availableRemoteOffers,
source: "others",
selectedTitle: "Subscribed",
availableTitle: "Available from People",
availableTitle: "Services from People",
emptySelected: "No Services from others are subscribed.",
emptyAvailable: `No ${SERVICE_KIND_LIST_COPY} services are available from People you are connected with.`,
});
Expand Down Expand Up @@ -484,6 +484,10 @@ function serviceCopy(offer, source, selected) {
if (selected && requestStatus === "denied") {
return `${name} denied the request. Remove it and ask again if needed.`;
}
if (readText(offer?.service_kind) === BROWSER_ENGINE_SERVICE_KIND) {
const sharing = "The owner must share their Browser Engine in Services and approve in Inbox before Browser can use it.";
return selected ? `Request sent. ${sharing}` : `Ask to use ${name}. ${sharing}`;
}
return selected
? `${name} is waiting for approval.`
: `Ask to use ${name}. You need to be connected in People first.`;
Expand Down Expand Up @@ -520,6 +524,9 @@ function serviceStatus(offer, source, selected) {
return "Subscribed";
}
const status = readText(offer?.status);
if (status === "requestable" && readText(offer?.service_kind) === BROWSER_ENGINE_SERVICE_KIND) {
return "Ask owner";
}
return status === "requestable" ? "Available" : status || "Available";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ pub(super) fn home_service_offers_for_people_contact(
display_name: format!("{}'s Browser Engine", contact.display_name),
provider_uri: Some("elastos://browser-engine/*".to_string()),
provider_label: "Remote Engine".to_string(),
policy_summary: "Ask this person to run Browser pages with a profile stored on their Runtime. Your Runtime keeps control of the page and its selected Exit. Existing profiles require an approved transfer.".to_string(),
policy_summary: "Ask this person to run Browser pages with a profile stored on their Runtime. They must share their Browser Engine in Services and approve in Inbox before Browser can use it. Your Runtime keeps control of the page and its selected Exit. Existing profiles require an approved transfer.".to_string(),
status: "requestable".to_string(), enabled: false, grant_required: true,
grant_scope: crate::carrier::browser_engine_binding::EXECUTION_SCOPE.to_string(),
capsule_contract: "browser -> Runtime service grant -> owner-bound Engine page".to_string(),
Expand Down
46 changes: 30 additions & 16 deletions elastos/crates/elastos-server/src/api/gateway_home_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4199,13 +4199,8 @@ pub(super) fn home_services_sync_access_requests(
let _guard = mutation_lock
.lock()
.map_err(|_| anyhow::anyhow!("Services state is unavailable"))?;
if !home_services_local_exit_shared(data_dir, context)?
&& !home_services_local_engine_shared(data_dir, context)?
&& !home_services_local_model_shared(data_dir, context)?
&& !crate::api::any_hosted_model_shared(data_dir)
{
return Ok(());
}
let model_shared = home_services_local_model_shared(data_dir, context)?
|| crate::api::any_hosted_model_shared(data_dir);
let contacts = home_services_peer_contacts_state(data_dir, context, discovery_service)?;
let known_peers = contacts
.contacts
Expand Down Expand Up @@ -4331,13 +4326,14 @@ pub(super) fn home_services_sync_access_requests(
.pending_model_catalogs
.iter()
.filter(|(peer_id, _)| {
now.saturating_sub(
state
.catalog_last_response_at
.get(*peer_id)
.copied()
.unwrap_or_default(),
) >= HOME_MODEL_CATALOG_RESPONSE_COOLDOWN_SECS
model_shared
&& now.saturating_sub(
state
.catalog_last_response_at
.get(*peer_id)
.copied()
.unwrap_or_default(),
) >= HOME_MODEL_CATALOG_RESPONSE_COOLDOWN_SECS
})
.map(|(peer_id, request)| {
(
Expand Down Expand Up @@ -4534,6 +4530,9 @@ fn home_services_request_notification_copy(kind: &str, uri: &str) -> (&'static s
}
}

const PRIVATE_ENGINE_APPROVAL_COPY: &str =
"Your Browser Engine is private. Share it in Services before approving this request.";

pub(super) fn append_home_service_access_notifications(
data_dir: &std::path::Path,
context: &HomeLaunchTokenContext,
Expand Down Expand Up @@ -4599,6 +4598,14 @@ pub(super) fn append_home_service_access_notifications(
}
None => approval_effect.to_string(),
}
} else if request.service_kind == crate::carrier::ENGINE_SERVICE_KIND
&& request.service_uri == crate::carrier::ENGINE_SERVICE_URI
{
match home_services_local_engine_shared(data_dir, context) {
Ok(false) => PRIVATE_ENGINE_APPROVAL_COPY.to_string(),
Ok(true) => approval_effect.to_string(),
Err(_) => "Check Browser Engine sharing in Services before approval.".to_string(),
}
} else {
approval_effect.to_string()
};
Expand Down Expand Up @@ -4770,8 +4777,15 @@ fn home_services_record_access_decision(
let contacts = home_services_peer_contacts_state(data_dir, context, discovery_service)?;
if !contacts.contacts.values().any(|contact| {
contact.peer_id == request.requester_peer_id && contact.did == request.requester_did
}) || !home_services_request_shared(data_dir, context, &request)?
{
}) {
anyhow::bail!("service request person or shared offer is no longer available");
}
if !home_services_request_shared(data_dir, context, &request)? {
if request.service_kind == crate::carrier::ENGINE_SERVICE_KIND
&& request.service_uri == crate::carrier::ENGINE_SERVICE_URI
{
anyhow::bail!(PRIVATE_ENGINE_APPROVAL_COPY);
}
anyhow::bail!("service request person or shared offer is no longer available");
}
}
Expand Down
170 changes: 163 additions & 7 deletions elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8682,6 +8682,160 @@ fn services_engine_offer(fixture: &ServicesContactFixture) -> String {
)
}

#[tokio::test]
async fn test_private_engine_request_requires_share_before_inbox_approval() {
for model_shared in [false, true] {
let left = tempfile::tempdir().unwrap();
let right = tempfile::tempdir().unwrap();
let bus = Arc::new(TokioMutex::new(FakePeerBus::default()));
let (trusted_key, _) = generate_keypair();
let network =
configured_discovery_network_profile_for_test(&trusted_key, "services-contacts");
let alice =
services_contact_fixture(left.path(), "Alice", bus.clone(), network.clone()).await;
let bob = services_contact_fixture(right.path(), "Bob", bus, network).await;
accept_services_contact_pair(&alice, &bob);
std::fs::create_dir_all(right.path().join("config")).unwrap();
std::fs::write(
right.path().join("config/browser-engine-adapter.json"),
"{}",
)
.unwrap();
std::fs::create_dir_all(right.path().join("bin")).unwrap();
std::fs::write(right.path().join("bin/model-provider"), b"").unwrap();
let services_token =
app_token_for_authority(right.path(), SERVICES_CAPSULE_ID, &bob.authority);
if model_shared {
let (status, _) = services_contact_post(
&bob.app,
&services_token,
"/api/apps/services/offers",
json!({"offer_id":MODEL_LOCAL_OFFER,"section":"mine","selected":true}),
)
.await;
assert_eq!(status, StatusCode::OK);
}

let alice_token =
app_token_for_authority(left.path(), SERVICES_CAPSULE_ID, &alice.authority);
let (status, _) = services_contact_post(
&alice.app,
&alice_token,
"/api/apps/services/offers",
json!({"offer_id":services_engine_offer(&bob),"section":"others","selected":true}),
)
.await;
assert_eq!(status, StatusCode::OK);
let (status, _) = home_test_post_json(
&bob.app,
"/api/apps/home/launch",
&bob.authority.home_token,
"http://localhost:61180",
json!({"target":INBOX_CAPSULE_ID}),
)
.await;
assert_eq!(status, StatusCode::OK);
let inbox_token = app_token_for_authority(right.path(), INBOX_CAPSULE_ID, &bob.authority);
let (_, inbox) =
home_test_get_json(&bob.app, "/api/apps/inbox/summary", &inbox_token, "null").await;
let request = inbox["notifications"]["entries"]
.as_array()
.unwrap()
.iter()
.find(|entry| entry["kind"] == "service_access_request")
.unwrap();
assert!(request["body"].as_str().unwrap().contains(
"Your Browser Engine is private. Share it in Services before approving this request."
));
let action = request["action_ref"]["action_id"]
.as_str()
.unwrap()
.to_string();
let (status, message) = services_contact_post(
&bob.app,
&inbox_token,
"/api/apps/inbox/actions",
json!({"action_id":action}),
)
.await;
assert!(!status.is_success());
assert!(
message.contains("Your Browser Engine is private. Share it in Services"),
"{message}"
);
let (_, still_pending) =
home_test_get_json(&bob.app, "/api/apps/inbox/summary", &inbox_token, "null").await;
assert!(still_pending["notifications"]["entries"]
.as_array()
.unwrap()
.iter()
.any(|entry| entry["action_ref"]["action_id"] == action));

let (status, _) = services_contact_post(
&bob.app,
&services_token,
"/api/apps/services/offers",
json!({"offer_id":"local:provider:browser-engine","section":"mine","selected":true}),
)
.await;
assert_eq!(status, StatusCode::OK);
let (_, services) = home_test_get_json(
&bob.app,
"/api/apps/services/summary",
&services_token,
"null",
)
.await;
assert!(services["local_offers"]
.as_array()
.unwrap()
.iter()
.any(|offer| offer["offer_id"] == "local:provider:browser-engine"));
assert_eq!(
services["local_offers"]
.as_array()
.unwrap()
.iter()
.any(|offer| offer["offer_id"] == MODEL_LOCAL_OFFER),
model_shared
);
let (_, ready_inbox) =
home_test_get_json(&bob.app, "/api/apps/inbox/summary", &inbox_token, "null").await;
assert!(!ready_inbox["notifications"]["entries"]
.as_array()
.unwrap()
.iter()
.find(|entry| entry["action_ref"]["action_id"] == action)
.unwrap()["body"]
.as_str()
.unwrap()
.contains("is private"));
let (status, message) = services_contact_post(
&bob.app,
&inbox_token,
"/api/apps/inbox/actions",
json!({"action_id":action}),
)
.await;
assert_eq!(status, StatusCode::OK, "{message}");
let (_, services) = home_test_get_json(
&bob.app,
"/api/apps/services/summary",
&services_token,
"null",
)
.await;
assert_eq!(
services["local_offers"]
.as_array()
.unwrap()
.iter()
.any(|offer| offer["offer_id"] == MODEL_LOCAL_OFFER),
model_shared
);
}
}

#[tokio::test]
async fn test_services_inbox_request_copy_matches_engine_or_exit_and_exact_approval() {
for (engine, service_name, offer_name, approval_text) in [
Expand Down Expand Up @@ -10122,13 +10276,15 @@ async fn test_services_runtime_mailbox_requires_current_sharing_contact_and_sign
std::fs::write(right.path().join("config/exit-provider.json"), "{}").unwrap();
let service = bob.discovery_service.clone();
service.sync_services_mailboxes_once(right.path(), 0).await;
assert!(bob
.peer_provider
.state
.provider_requests
.lock()
.await
.is_empty());
let initial_peer_calls = bob.peer_provider.state.provider_requests.lock().await;
assert!(
initial_peer_calls.iter().all(|call| matches!(
call["op"].as_str(),
Some("get_ticket" | "gossip_join" | "gossip_join_peers" | "gossip_recv")
)),
"an empty mailbox may poll but must not send: {initial_peer_calls:?}"
);
drop(initial_peer_calls);
assert!(
services_mailbox_saved_state(right.path(), &bob.authority, "services-requests.json")
["requests"]
Expand Down
17 changes: 17 additions & 0 deletions scripts/services-uiux-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,18 @@ async function assertVerifiedLaunch(page, server, diagnostics) {

async function assertSelectAvailableOffer(page, server) {
const frame = await bootLaunchedPage(page, server);
const requestable = await frame.evaluate(() => {
const card = document.querySelector('[data-service-offer-id="request-browser-engine"]')?.closest(".service-card");
return {
heading: document.getElementById("other-services")?.textContent || "",
status: card?.querySelector(".status-badge")?.textContent?.trim(),
copy: card?.querySelector(".service-copy")?.textContent?.trim(),
};
});
assert(requestable.heading.includes("Services from People")
&& requestable.status === "Ask owner"
&& requestable.copy?.includes("The owner must share their Browser Engine in Services and approve in Inbox before Browser can use it."),
"an unapproved contact Engine must show the Share and approval steps", requestable);
await frame.click('[data-service-offer-id="request-browser-engine"]');
await frame.waitForFunction(
() =>
Expand All @@ -502,6 +514,11 @@ async function assertSelectAvailableOffer(page, server) {
),
);
assert(server.trace.offerPosts.length === 1, "selecting an available offer must send one POST", server.trace.offerPosts);
const requestedCopy = await frame.evaluate(() => [...document.querySelectorAll(".service-card")]
.find((card) => card.textContent?.includes("Lin's Browser Engine"))
?.querySelector(".service-copy")?.textContent?.trim());
assert(requestedCopy?.includes("Request sent. The owner must share their Browser Engine"),
"a sent request must not claim that the private Engine is ready", requestedCopy);
assert(
JSON.stringify(server.trace.offerPosts[0]) === JSON.stringify({
homeToken: TOKEN,
Expand Down
Loading