同步 MNote 授权目录到 OpenHub
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
use crate::error::WebError;
|
use crate::error::WebError;
|
||||||
use control_plane::UserRecord;
|
use control_plane::{ControlPlaneStore, DirectoryGrantRecord, UserRecord};
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
@@ -96,6 +96,7 @@ fn sync_disabled() -> bool {
|
|||||||
pub async fn sync_provider_identities(
|
pub async fn sync_provider_identities(
|
||||||
user: &UserRecord,
|
user: &UserRecord,
|
||||||
password: &str,
|
password: &str,
|
||||||
|
directory_grants: &[DirectoryGrantRecord],
|
||||||
) -> Vec<ProviderIdentitySyncResult> {
|
) -> Vec<ProviderIdentitySyncResult> {
|
||||||
if sync_disabled() {
|
if sync_disabled() {
|
||||||
return vec![ProviderIdentitySyncResult {
|
return vec![ProviderIdentitySyncResult {
|
||||||
@@ -112,12 +113,12 @@ pub async fn sync_provider_identities(
|
|||||||
match (sync_openhub, sync_weknora) {
|
match (sync_openhub, sync_weknora) {
|
||||||
(true, true) => {
|
(true, true) => {
|
||||||
let (openhub, weknora) = tokio::join!(
|
let (openhub, weknora) = tokio::join!(
|
||||||
sync_openhub_identity(user, password),
|
sync_openhub_identity(user, password, directory_grants),
|
||||||
sync_weknora_identity(user, password)
|
sync_weknora_identity(user, password)
|
||||||
);
|
);
|
||||||
vec![openhub, weknora]
|
vec![openhub, weknora]
|
||||||
}
|
}
|
||||||
(true, false) => vec![sync_openhub_identity(user, password).await],
|
(true, false) => vec![sync_openhub_identity(user, password, directory_grants).await],
|
||||||
(false, true) => vec![sync_weknora_identity(user, password).await],
|
(false, true) => vec![sync_weknora_identity(user, password).await],
|
||||||
(false, false) => vec![ProviderIdentitySyncResult {
|
(false, false) => vec![ProviderIdentitySyncResult {
|
||||||
provider: "all",
|
provider: "all",
|
||||||
@@ -128,6 +129,59 @@ pub async fn sync_provider_identities(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn sync_openhub_directory_permissions_for_user_id(
|
||||||
|
control_plane: &dyn ControlPlaneStore,
|
||||||
|
user_id: &str,
|
||||||
|
) -> ProviderIdentitySyncResult {
|
||||||
|
if sync_disabled() || !env_flag("MNOTE_PROVIDER_IDENTITY_SYNC_OPENHUB", true) {
|
||||||
|
return ProviderIdentitySyncResult {
|
||||||
|
provider: "openhub",
|
||||||
|
ok: true,
|
||||||
|
message: "provider identity sync disabled".to_string(),
|
||||||
|
provider_user_id: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let user_id = user_id.trim();
|
||||||
|
let provider_user_id = stable_openhub_user_id(user_id);
|
||||||
|
let directory_grants = match control_plane.list_directory_grants_for_actor(user_id) {
|
||||||
|
Ok(grants) => grants,
|
||||||
|
Err(error) => {
|
||||||
|
return ProviderIdentitySyncResult {
|
||||||
|
provider: "openhub",
|
||||||
|
ok: false,
|
||||||
|
message: format!("mnote directory grants read failed: {error}"),
|
||||||
|
provider_user_id: Some(provider_user_id.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let payload = json!({
|
||||||
|
"mnote_user_id": user_id,
|
||||||
|
"allowedRoots": openhub_allowed_roots_payload(&directory_grants),
|
||||||
|
});
|
||||||
|
match post_json(
|
||||||
|
join_url(
|
||||||
|
&openhub_base_url(),
|
||||||
|
&format!("/api/internal/mnote/users/{provider_user_id}/directory-permissions/sync"),
|
||||||
|
),
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => ProviderIdentitySyncResult {
|
||||||
|
provider: "openhub",
|
||||||
|
ok: true,
|
||||||
|
message: "directory permissions synced".to_string(),
|
||||||
|
provider_user_id: Some(provider_user_id.to_string()),
|
||||||
|
},
|
||||||
|
Err(error) => ProviderIdentitySyncResult {
|
||||||
|
provider: "openhub",
|
||||||
|
ok: false,
|
||||||
|
message: error.message().to_string(),
|
||||||
|
provider_user_id: Some(provider_user_id.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn post_json(url: String, body: Value) -> Result<Value, WebError> {
|
async fn post_json(url: String, body: Value) -> Result<Value, WebError> {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_millis(PROVIDER_IDENTITY_SYNC_TIMEOUT_MS))
|
.timeout(Duration::from_millis(PROVIDER_IDENTITY_SYNC_TIMEOUT_MS))
|
||||||
@@ -156,7 +210,11 @@ async fn post_json(url: String, body: Value) -> Result<Value, WebError> {
|
|||||||
Ok(payload)
|
Ok(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_openhub_identity(user: &UserRecord, password: &str) -> ProviderIdentitySyncResult {
|
async fn sync_openhub_identity(
|
||||||
|
user: &UserRecord,
|
||||||
|
password: &str,
|
||||||
|
directory_grants: &[DirectoryGrantRecord],
|
||||||
|
) -> ProviderIdentitySyncResult {
|
||||||
let provider_user_id = stable_openhub_user_id(&user.id);
|
let provider_user_id = stable_openhub_user_id(&user.id);
|
||||||
let payload = json!({
|
let payload = json!({
|
||||||
"provider_user_id": provider_user_id,
|
"provider_user_id": provider_user_id,
|
||||||
@@ -165,6 +223,7 @@ async fn sync_openhub_identity(user: &UserRecord, password: &str) -> ProviderIde
|
|||||||
"email": fallback_email(user),
|
"email": fallback_email(user),
|
||||||
"password": password,
|
"password": password,
|
||||||
"workspace_path": default_workspace_path(user),
|
"workspace_path": default_workspace_path(user),
|
||||||
|
"allowedRoots": openhub_allowed_roots_payload(directory_grants),
|
||||||
"disabled": user.status != "active",
|
"disabled": user.status != "active",
|
||||||
"is_admin": false,
|
"is_admin": false,
|
||||||
});
|
});
|
||||||
@@ -189,6 +248,36 @@ async fn sync_openhub_identity(user: &UserRecord, password: &str) -> ProviderIde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn openhub_allowed_roots_payload(directory_grants: &[DirectoryGrantRecord]) -> Vec<Value> {
|
||||||
|
directory_grants
|
||||||
|
.iter()
|
||||||
|
.filter(|grant| !is_default_workspace_auto_grant(grant))
|
||||||
|
.filter(|grant| grant.status.trim() == "active")
|
||||||
|
.filter(|grant| !grant.root_path.trim().is_empty())
|
||||||
|
.map(|grant| {
|
||||||
|
json!({
|
||||||
|
"grantId": grant.id,
|
||||||
|
"rootUri": grant.root_uri,
|
||||||
|
"rootPath": grant.root_path,
|
||||||
|
"permission": grant.permission,
|
||||||
|
"recursive": grant.recursive,
|
||||||
|
"capabilities": serde_json::from_str::<Vec<String>>(&grant.capabilities_json).unwrap_or_default(),
|
||||||
|
"source": grant.source,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_default_workspace_auto_grant(grant: &DirectoryGrantRecord) -> bool {
|
||||||
|
grant.source.trim() == "auto"
|
||||||
|
&& grant.permission.trim() == "write"
|
||||||
|
&& grant.recursive
|
||||||
|
&& grant.created_by.as_deref().map(str::trim) == Some(grant.user_id.as_str())
|
||||||
|
&& grant.workspace_id.is_some()
|
||||||
|
&& grant.root_uri.starts_with("local://users/")
|
||||||
|
&& grant.root_uri.ends_with("/workspaces/my-space")
|
||||||
|
}
|
||||||
|
|
||||||
async fn sync_weknora_identity(user: &UserRecord, password: &str) -> ProviderIdentitySyncResult {
|
async fn sync_weknora_identity(user: &UserRecord, password: &str) -> ProviderIdentitySyncResult {
|
||||||
let payload = json!({
|
let payload = json!({
|
||||||
"mnote_user_id": user.id,
|
"mnote_user_id": user.id,
|
||||||
|
|||||||
@@ -2203,7 +2203,16 @@ async fn handle_sqlite_auth_action(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let provider_sync_results = if flow == "signUp" {
|
let provider_sync_results = if flow == "signUp" {
|
||||||
let results = sync_provider_identities(&resolved.user, &password_for_provider_sync).await;
|
let directory_grants = state
|
||||||
|
.control_plane()
|
||||||
|
.list_directory_grants_for_actor(&resolved.user.id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let results = sync_provider_identities(
|
||||||
|
&resolved.user,
|
||||||
|
&password_for_provider_sync,
|
||||||
|
&directory_grants,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||||
actor_user_id: Some(resolved.user.id.clone()),
|
actor_user_id: Some(resolved.user.id.clone()),
|
||||||
action: "control.auth.provider_identities_synced".to_string(),
|
action: "control.auth.provider_identities_synced".to_string(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::page_aggregate::{
|
|||||||
PageAggregate, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions,
|
PageAggregate, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions,
|
||||||
PagePermissions, PageStats, PageTree,
|
PagePermissions, PageStats, PageTree,
|
||||||
};
|
};
|
||||||
|
use crate::provider_identity_sync::sync_openhub_directory_permissions_for_user_id;
|
||||||
use crate::routes::local_markdown_parser::{
|
use crate::routes::local_markdown_parser::{
|
||||||
file_stem_title, parse_markdown_attachment_refs, parse_markdown_page, split_frontmatter,
|
file_stem_title, parse_markdown_attachment_refs, parse_markdown_page, split_frontmatter,
|
||||||
};
|
};
|
||||||
@@ -2124,6 +2125,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
|||||||
"默认空间的系统授权不能撤销",
|
"默认空间的系统授权不能撤销",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let revoked_user_id = grants.first().map(|grant| grant.user_id.clone());
|
||||||
state
|
state
|
||||||
.control_plane()
|
.control_plane()
|
||||||
.revoke_directory_grant(grant_id, None)
|
.revoke_directory_grant(grant_id, None)
|
||||||
@@ -2154,6 +2156,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
|||||||
"controlPlane": "sqlite",
|
"controlPlane": "sqlite",
|
||||||
"policyPath": local_access_policy_path().display().to_string(),
|
"policyPath": local_access_policy_path().display().to_string(),
|
||||||
"deletedGrantId": grant_id,
|
"deletedGrantId": grant_id,
|
||||||
|
"revokedUserId": revoked_user_id,
|
||||||
"policy": {
|
"policy": {
|
||||||
"grants": state
|
"grants": state
|
||||||
.control_plane()
|
.control_plane()
|
||||||
@@ -2371,6 +2374,14 @@ pub async fn create_local_access_grant(
|
|||||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||||
let payload = add_sqlite_local_access_grant_for_context(&state, &context, request)
|
let payload = add_sqlite_local_access_grant_for_context(&state, &context, request)
|
||||||
.map_err(|error| error.with_context(&context))?;
|
.map_err(|error| error.with_context(&context))?;
|
||||||
|
if let Some(user_id) = payload
|
||||||
|
.pointer("/grant/userId")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await;
|
||||||
|
}
|
||||||
Ok((StatusCode::OK, Json(payload)))
|
Ok((StatusCode::OK, Json(payload)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2381,6 +2392,14 @@ pub async fn create_user_access_grant(
|
|||||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||||
let payload = add_sqlite_user_access_grant_for_context(&state, &context, request)
|
let payload = add_sqlite_user_access_grant_for_context(&state, &context, request)
|
||||||
.map_err(|error| error.with_context(&context))?;
|
.map_err(|error| error.with_context(&context))?;
|
||||||
|
if let Some(user_id) = payload
|
||||||
|
.pointer("/grant/userId")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await;
|
||||||
|
}
|
||||||
Ok((StatusCode::OK, Json(payload)))
|
Ok((StatusCode::OK, Json(payload)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2391,6 +2410,14 @@ pub async fn delete_local_access_grant(
|
|||||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||||
let payload = delete_sqlite_local_access_grant_for_context(&state, &context, &grant_id)
|
let payload = delete_sqlite_local_access_grant_for_context(&state, &context, &grant_id)
|
||||||
.map_err(|error| error.with_context(&context))?;
|
.map_err(|error| error.with_context(&context))?;
|
||||||
|
if let Some(user_id) = payload
|
||||||
|
.get("revokedUserId")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await;
|
||||||
|
}
|
||||||
Ok((StatusCode::OK, Json(payload)))
|
Ok((StatusCode::OK, Json(payload)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2401,6 +2428,14 @@ pub async fn delete_user_access_grant(
|
|||||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||||
let payload = delete_sqlite_user_access_grant_for_context(&state, &context, &grant_id)
|
let payload = delete_sqlite_user_access_grant_for_context(&state, &context, &grant_id)
|
||||||
.map_err(|error| error.with_context(&context))?;
|
.map_err(|error| error.with_context(&context))?;
|
||||||
|
if let Some(user_id) = payload
|
||||||
|
.get("revokedUserId")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await;
|
||||||
|
}
|
||||||
Ok((StatusCode::OK, Json(payload)))
|
Ok((StatusCode::OK, Json(payload)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11244,6 +11279,7 @@ fn main() {}
|
|||||||
.await
|
.await
|
||||||
.expect("owner can revoke self-created grant");
|
.expect("owner can revoke self-created grant");
|
||||||
assert_eq!(deleted["deletedGrantId"], grant_id);
|
assert_eq!(deleted["deletedGrantId"], grant_id);
|
||||||
|
assert_eq!(deleted["revokedUserId"], "user_target");
|
||||||
let revoked_error = ensure_local_workspace_write_access_with_state(
|
let revoked_error = ensure_local_workspace_write_access_with_state(
|
||||||
&state,
|
&state,
|
||||||
&request_context("user_target", "user"),
|
&request_context("user_target", "user"),
|
||||||
|
|||||||
Reference in New Issue
Block a user