Limit community to single-user workspace
Deploy / deploy (push) Successful in 2m33s
CI / Rust Checks (push) Successful in 5m35s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 2s
CI / Frontend E2E (push) Successful in 4m19s

This commit is contained in:
github-ops
2026-06-19 17:41:50 +00:00
parent ba29ac7b94
commit 58cfd86d90
23 changed files with 134 additions and 1696 deletions
+64 -125
View File
@@ -8,20 +8,14 @@ use crate::{
rate_limit::apply_api_rate_limit,
request_context::apply_request_context,
routes::{
access::{
create_invitation, delete_invitation, delete_membership, delete_workspace,
export_workspace, list_invitations, list_memberships, update_membership,
},
access::export_workspace,
agents::{
archive_agent, create_agent, create_agent_platform_api_key, delete_agent,
delete_agent_platform_api_key, get_agent, get_agent_version,
list_agent_platform_api_keys, list_agents, publish_agent,
revoke_agent_platform_api_key, save_agent_bindings, unpublish_agent, update_agent,
},
auth::{
change_password, get_profile, get_session, login, logout, update_current_workspace,
update_profile,
},
auth::{change_password, get_profile, get_session, login, logout, update_profile},
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
capabilities::get_capabilities,
machine_auth::{issue_agent_token, issue_one_time_agent_token},
@@ -35,7 +29,7 @@ use crate::{
secrets::{create_secret, delete_secret, get_secret, list_secrets, rotate_secret},
streaming::list_protocol_capabilities,
upstreams::{create_upstream, list_upstreams, update_upstream},
workspaces::{create_workspace, get_workspace, list_workspaces, update_workspace},
workspaces::{get_workspace, list_workspaces, update_workspace},
},
state::AppState,
};
@@ -118,16 +112,6 @@ pub fn build_app(state: AppState) -> Router {
get(get_secret).delete(delete_secret),
)
.route("/secrets/{secret_id}/rotate", post(rotate_secret))
.route("/members", get(list_memberships))
.route(
"/members/{user_id}",
axum::routing::patch(update_membership).delete(delete_membership),
)
.route(
"/invitations",
get(list_invitations).post(create_invitation),
)
.route("/invitations/{invitation_id}", delete(delete_invitation))
.route("/export", get(export_workspace))
.route("/logs", get(list_logs))
.route("/logs/{log_id}", get(get_log))
@@ -138,7 +122,7 @@ pub fn build_app(state: AppState) -> Router {
let workspace_root_router = Router::new()
.route("/capabilities", get(get_capabilities))
.route("/workspaces", get(list_workspaces).post(create_workspace))
.route("/workspaces", get(list_workspaces))
.layer(middleware::from_fn_with_state(
state.clone(),
require_session,
@@ -147,9 +131,7 @@ pub fn build_app(state: AppState) -> Router {
let workspace_scoped_router = Router::new()
.route(
"/workspaces/{workspace_id}",
get(get_workspace)
.patch(update_workspace)
.delete(delete_workspace),
get(get_workspace).patch(update_workspace),
)
.nest("/workspaces/{workspace_id}", workspace_router)
.layer(middleware::from_fn_with_state(
@@ -163,7 +145,6 @@ pub fn build_app(state: AppState) -> Router {
.route("/logout", post(logout))
.route("/session", get(get_session))
.route("/profile", get(get_profile).patch(update_profile))
.route("/current-workspace", post(update_current_workspace))
.route("/password", post(change_password))
.layer(middleware::from_fn_with_state(
state.clone(),
@@ -1002,21 +983,19 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn manages_platform_access_resources() {
async fn rejects_workspace_access_management_in_community() {
let registry = test_registry().await;
let storage_root = test_storage_root("platform_access");
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let members = client
let members_status = client
.get(format!("{base_url}/members"))
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
let created_invitation = client
.status();
let create_invitation_status = client
.post(format!("{base_url}/invitations"))
.json(&json!({
"email": "operator@example.com",
@@ -1025,44 +1004,10 @@ mod tests {
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
let invitation_id = created_invitation["invitation"]["invitation"]["id"]
.as_str()
.unwrap()
.to_owned();
let invitations = client
.get(format!("{base_url}/invitations"))
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
let delete_invitation_status = client
.delete(format!("{base_url}/invitations/{invitation_id}"))
.send()
.await
.unwrap()
.status();
assert_eq!(members["items"][0]["role"], "owner");
assert_eq!(
created_invitation["invitation"]["invitation"]["status"],
"pending"
);
assert!(
created_invitation["invite_token"]
.as_str()
.unwrap()
.starts_with("invite_")
);
assert_eq!(
invitations["items"][0]["invitation"]["email"],
"operator@example.com"
);
assert_eq!(delete_invitation_status, reqwest::StatusCode::NO_CONTENT);
assert_eq!(members_status, reqwest::StatusCode::NOT_FOUND);
assert_eq!(create_invitation_status, reqwest::StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread")]
@@ -1178,7 +1123,7 @@ mod tests {
);
assert_eq!(response["limits"]["max_workspaces"], 1);
assert_eq!(response["limits"]["max_users_per_workspace"], 1);
assert_eq!(response["limits"]["max_agents_per_workspace"], 1);
assert_eq!(response["limits"]["max_agents_per_workspace"], Value::Null);
}
#[tokio::test(flavor = "multi_thread")]
@@ -1568,7 +1513,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn manages_workspace_access_lifecycle() {
async fn exports_single_workspace_but_rejects_access_lifecycle() {
let registry = test_registry().await;
let storage_root = test_storage_root("workspace_access");
let base_url = spawn_admin_api(build_test_app(registry.clone(), storage_root)).await;
@@ -1587,22 +1532,14 @@ mod tests {
.await
.unwrap();
let updated_members = assert_success_json(
client
.patch(format!("{base_url}/members/{}", second_user_id.as_str()))
.json(&json!({ "role": "admin" }))
.send()
.await
.unwrap(),
)
.await;
let updated_member = updated_members["items"]
.as_array()
let update_member_status = client
.patch(format!("{base_url}/members/{}", second_user_id.as_str()))
.json(&json!({ "role": "admin" }))
.send()
.await
.unwrap()
.iter()
.find(|item| item["user"]["id"] == second_user_id.as_str())
.unwrap();
assert_eq!(updated_member["role"], "admin");
.status();
assert_eq!(update_member_status, reqwest::StatusCode::NOT_FOUND);
let exported = assert_success_json(
client
@@ -1616,7 +1553,8 @@ mod tests {
exported["workspace"]["workspace"]["id"],
DEFAULT_WORKSPACE_ID
);
assert_eq!(exported["memberships"].as_array().unwrap().len(), 2);
assert!(exported.get("memberships").is_none());
assert!(exported.get("invitations").is_none());
let delete_member_status = client
.delete(format!("{base_url}/members/{}", second_user_id.as_str()))
@@ -1624,7 +1562,7 @@ mod tests {
.await
.unwrap()
.status();
assert_eq!(delete_member_status, reqwest::StatusCode::NO_CONTENT);
assert_eq!(delete_member_status, reqwest::StatusCode::NOT_FOUND);
let delete_workspace_status = client
.delete(base_url.as_ref())
@@ -1632,21 +1570,27 @@ mod tests {
.await
.unwrap()
.status();
assert_eq!(delete_workspace_status, reqwest::StatusCode::NO_CONTENT);
assert_eq!(
delete_workspace_status,
reqwest::StatusCode::METHOD_NOT_ALLOWED
);
let root_url = base_url
.as_ref()
.split("/api/admin/workspaces/")
.next()
.unwrap();
let missing_workspace = client
.get(format!(
"{root_url}/api/admin/workspaces/{DEFAULT_WORKSPACE_ID}"
))
.send()
.await
.unwrap();
assert_eq!(missing_workspace.status(), reqwest::StatusCode::FORBIDDEN);
let still_available = assert_success_json(
client
.get(format!(
"{root_url}/api/admin/workspaces/{DEFAULT_WORKSPACE_ID}"
))
.send()
.await
.unwrap(),
)
.await;
assert_eq!(still_available["workspace"]["id"], DEFAULT_WORKSPACE_ID);
}
#[tokio::test]
@@ -1679,11 +1623,7 @@ mod tests {
.iter()
.any(|item| item.workspace.slug == "default")
);
assert!(
workspaces
.iter()
.any(|item| item.workspace.slug == "growth-lab")
);
assert_eq!(workspaces.len(), 1);
let default_workspace_id = WorkspaceId::new(DEFAULT_WORKSPACE_ID);
let operations = service
@@ -1788,7 +1728,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn switches_current_workspace_in_session() {
async fn rejects_multi_workspace_session_switching_in_community() {
let registry = test_registry().await;
let storage_root = test_storage_root("session_workspace");
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
@@ -1800,31 +1740,30 @@ mod tests {
.to_owned();
let client = authorized_client(&base_url).await;
let created_workspace = assert_success_json(
client
.post(format!("{root_url}/api/admin/workspaces"))
.json(&json!({
"slug": "growth-lab",
"display_name": "Growth Lab",
"settings": {}
}))
.send()
.await
.unwrap(),
)
.await;
let workspace_id = created_workspace["workspace"]["id"].as_str().unwrap();
let create_workspace_status = client
.post(format!("{root_url}/api/admin/workspaces"))
.json(&json!({
"slug": "growth-lab",
"display_name": "Growth Lab",
"settings": {}
}))
.send()
.await
.unwrap()
.status();
assert_eq!(
create_workspace_status,
reqwest::StatusCode::METHOD_NOT_ALLOWED
);
let switched = assert_success_json(
client
.post(format!("{root_url}/api/auth/current-workspace"))
.json(&json!({ "workspace_id": workspace_id }))
.send()
.await
.unwrap(),
)
.await;
assert_eq!(switched["current_workspace_id"], workspace_id);
let switch_status = client
.post(format!("{root_url}/api/auth/current-workspace"))
.json(&json!({ "workspace_id": DEFAULT_WORKSPACE_ID }))
.send()
.await
.unwrap()
.status();
assert_eq!(switch_status, reqwest::StatusCode::NOT_FOUND);
let session = assert_success_json(
client
@@ -1834,7 +1773,7 @@ mod tests {
.unwrap(),
)
.await;
assert_eq!(session["current_workspace_id"], workspace_id);
assert_eq!(session["current_workspace_id"], DEFAULT_WORKSPACE_ID);
}
#[tokio::test(flavor = "multi_thread")]