Limit community to single-user workspace
This commit is contained in:
+64
-125
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user