feat(admin): complete site replication support (#2346)

This commit is contained in:
weisd
2026-03-31 13:01:38 +08:00
committed by GitHub
parent 6bf0c542a1
commit 15995aae14
22 changed files with 5429 additions and 101 deletions
+1
View File
@@ -131,6 +131,7 @@ astral-tokio-tar = { workspace = true }
atoi = { workspace = true }
atomic_enum = { workspace = true }
base64 = { workspace = true }
sha2 = { workspace = true }
base64-simd.workspace = true
clap = { workspace = true }
const-str = { workspace = true }
+82 -23
View File
@@ -15,6 +15,7 @@
use crate::{
admin::{
auth::validate_admin_request,
handlers::site_replication::site_replication_iam_change_hook,
router::{AdminOperation, Operation, S3Router},
utils::has_space_be,
},
@@ -27,7 +28,7 @@ use matchit::Params;
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
use rustfs_credentials::get_global_action_cred;
use rustfs_iam::error::{is_err_no_such_group, is_err_no_such_user};
use rustfs_madmin::GroupAddRemove;
use rustfs_madmin::{GroupAddRemove, GroupStatus, SITE_REPL_API_VERSION, SRGroupInfo, SRIAMItem};
use rustfs_policy::policy::action::{Action, AdminAction};
use s3s::{
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
@@ -222,7 +223,7 @@ impl Operation for DeleteGroup {
let Ok(iam_store) = rustfs_iam::get() else { return Err(s3_error!(InternalError, "iam not init")) };
iam_store.remove_users_from_group(group, vec![]).await.map_err(|e| {
let updated_at = iam_store.remove_users_from_group(group, vec![]).await.map_err(|e| {
warn!("delete group failed, e: {:?}", e);
match e {
rustfs_iam::error::Error::GroupNotEmpty => {
@@ -241,6 +242,26 @@ impl Operation for DeleteGroup {
}
})?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "group-info".to_string(),
group_info: Some(SRGroupInfo {
update_req: GroupAddRemove {
group: group.to_string(),
members: vec![],
status: GroupStatus::Enabled,
is_remove: true,
},
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!("site replication group delete hook failed, err: {err}");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
@@ -287,26 +308,46 @@ impl Operation for SetGroupStatus {
let Ok(iam_store) = rustfs_iam::get() else { return Err(s3_error!(InternalError, "iam not init")) };
if let Some(status) = query.status {
match status.as_str() {
"enabled" => {
iam_store.set_group_status(&query.group, true).await.map_err(|e| {
warn!("enable group failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
}
"disabled" => {
iam_store.set_group_status(&query.group, false).await.map_err(|e| {
warn!("enable group failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
}
let updated_at = if let Some(status) = query.status.as_deref() {
match status {
"enabled" => iam_store.set_group_status(&query.group, true).await.map_err(|e| {
warn!("enable group failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?,
"disabled" => iam_store.set_group_status(&query.group, false).await.map_err(|e| {
warn!("enable group failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?,
_ => {
return Err(s3_error!(InvalidArgument, "invalid status"));
}
}
} else {
return Err(s3_error!(InvalidArgument, "status is required"));
};
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "group-info".to_string(),
group_info: Some(SRGroupInfo {
update_req: GroupAddRemove {
group: query.group.clone(),
members: vec![],
status: if matches!(query.status.as_deref(), Some("disabled")) {
GroupStatus::Disabled
} else {
GroupStatus::Enabled
},
is_remove: false,
},
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!("site replication group status hook failed, err: {err}");
}
let mut header = HeaderMap::new();
@@ -387,15 +428,15 @@ impl Operation for UpdateGroupMembers {
}
}
if args.is_remove {
let updated_at = if args.is_remove {
warn!("remove group members");
iam_store
.remove_users_from_group(&args.group, args.members)
.remove_users_from_group(&args.group, args.members.clone())
.await
.map_err(|e| {
warn!("remove group members failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
})?
} else {
warn!("add group members");
@@ -406,10 +447,28 @@ impl Operation for UpdateGroupMembers {
return Err(s3_error!(InvalidArgument, "not such group"));
}
iam_store.add_users_to_group(&args.group, args.members).await.map_err(|e| {
warn!("add group members failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
iam_store
.add_users_to_group(&args.group, args.members.clone())
.await
.map_err(|e| {
warn!("add group members failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?
};
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "group-info".to_string(),
group_info: Some(SRGroupInfo {
update_req: args,
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!("site replication group membership hook failed, err: {err}");
}
let mut header = HeaderMap::new();
+4
View File
@@ -33,6 +33,7 @@ pub mod quota;
pub mod rebalance;
pub mod replication;
pub mod service_account;
pub mod site_replication;
pub mod sts;
pub mod system;
pub mod tier;
@@ -64,6 +65,9 @@ mod tests {
let _set_remote_target_handler = replication::SetRemoteTargetHandler {};
let _list_remote_target_handler = replication::ListRemoteTargetHandler {};
let _remove_remote_target_handler = replication::RemoveRemoteTargetHandler {};
let _site_replication_add_handler = site_replication::SiteReplicationAddHandler {};
let _site_replication_info_handler = site_replication::SiteReplicationInfoHandler {};
let _site_replication_status_handler = site_replication::SiteReplicationStatusHandler {};
// Just verify they can be created without panicking
// Test passes if we reach this point without panicking
+74 -3
View File
@@ -15,6 +15,7 @@
use crate::{
admin::{
auth::validate_admin_request,
handlers::site_replication::site_replication_iam_change_hook,
router::{AdminOperation, Operation, S3Router},
utils::{encode_compatible_admin_payload, has_space_be, read_compatible_admin_body},
},
@@ -28,7 +29,10 @@ use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
use rustfs_credentials::get_global_action_cred;
use rustfs_iam::error::is_err_no_such_user;
use rustfs_iam::store::MappedPolicy;
use rustfs_madmin::{GroupPolicyEntities, PolicyEntities, PolicyEntitiesResult, UserPolicyEntities};
use rustfs_madmin::{
GroupPolicyEntities, PolicyEntities, PolicyEntitiesResult, SITE_REPL_API_VERSION, SRIAMItem, SRPolicyMapping,
UserPolicyEntities,
};
use rustfs_policy::policy::{
Policy,
action::{Action, AdminAction},
@@ -223,11 +227,26 @@ impl Operation for AddCannedPolicy {
}
let Ok(iam_store) = rustfs_iam::get() else { return Err(s3_error!(InternalError, "iam not init")) };
iam_store.set_policy(&query.name, policy).await.map_err(|e| {
let updated_at = iam_store.set_policy(&query.name, policy.clone()).await.map_err(|e| {
warn!("set policy failed, e: {:?}", e);
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "policy".to_string(),
name: query.name.clone(),
policy: Some(
serde_json::to_value(&policy).map_err(|e| s3_error!(InternalError, "marshal policy failed, e: {:?}", e))?,
),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(policy = %query.name, error = ?err, "site replication policy add hook failed");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
@@ -337,6 +356,18 @@ impl Operation for RemoveCannedPolicy {
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "policy".to_string(),
name: query.name.clone(),
updated_at: Some(OffsetDateTime::now_utc()),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(policy = %query.name, error = ?err, "site replication policy delete hook failed");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
@@ -428,7 +459,7 @@ impl Operation for SetPolicyForUserOrGroup {
})?;
}
iam_store
let updated_at = iam_store
.policy_db_set(&query.user_or_group, rustfs_iam::store::UserType::Reg, query.is_group, &query.policy_name)
.await
.map_err(|e| {
@@ -436,6 +467,26 @@ impl Operation for SetPolicyForUserOrGroup {
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "policy-mapping".to_string(),
policy_mapping: Some(SRPolicyMapping {
user_or_group: query.user_or_group.clone(),
user_type: rustfs_iam::store::UserType::Reg.to_u64(),
is_group: query.is_group,
policy: query.policy_name.clone(),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(target = %query.user_or_group, error = ?err, "site replication policy mapping hook failed");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
@@ -841,6 +892,26 @@ async fn handle_builtin_policy_association(req: S3Request<Body>, is_attach: bool
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
})?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "policy-mapping".to_string(),
policy_mapping: Some(SRPolicyMapping {
user_or_group: target_name.clone(),
user_type: rustfs_iam::store::UserType::Reg.to_u64(),
is_group,
policy: updated_policies.join(","),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(target = %target_name, error = ?err, "site replication policy association hook failed");
}
let policies_attached = if is_attach { changed_policies.clone() } else { Vec::new() };
let policies_detached = if is_attach { Vec::new() } else { changed_policies };
+115 -8
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::admin::handlers::site_replication::site_replication_iam_change_hook;
use crate::admin::utils::{encode_compatible_admin_payload, has_space_be, is_compat_admin_request, read_compatible_admin_body};
use crate::auth::{constant_time_eq, get_condition_values, get_session_token};
use crate::server::{ADMIN_PREFIX, RemoteAddr};
@@ -30,7 +31,8 @@ use rustfs_iam::sys::{NewServiceAccountOpts, UpdateServiceAccountOpts};
use rustfs_madmin::{
ACCESS_KEY_LIST_ALL, ACCESS_KEY_LIST_STS_ONLY, ACCESS_KEY_LIST_SVCACC_ONLY, ACCESS_KEY_LIST_USERS_ONLY, AddServiceAccountReq,
AddServiceAccountResp, Credentials, InfoAccessKeyResp, InfoServiceAccountResp, LDAPSpecificAccessKeyInfo, ListAccessKeysResp,
ListServiceAccountsResp, OpenIDSpecificAccessKeyInfo, ServiceAccountInfo, TemporaryAccountInfoResp, UpdateServiceAccountReq,
ListServiceAccountsResp, OpenIDSpecificAccessKeyInfo, SITE_REPL_API_VERSION, SRIAMItem, SRSessionPolicy, SRSvcAccChange,
SRSvcAccCreate, SRSvcAccDelete, SRSvcAccUpdate, ServiceAccountInfo, TemporaryAccountInfoResp, UpdateServiceAccountReq,
};
use rustfs_policy::policy::action::{Action, AdminAction};
use rustfs_policy::policy::{Args, Policy};
@@ -44,6 +46,15 @@ use time::OffsetDateTime;
use tracing::{debug, warn};
use url::form_urlencoded;
fn sr_session_policy_from_value(value: Option<&serde_json::Value>) -> S3Result<SRSessionPolicy> {
let Some(value) = value else {
return Ok(SRSessionPolicy::default());
};
let raw = serde_json::to_string(value).map_err(|e| s3_error!(InvalidArgument, "marshal policy failed: {:?}", e))?;
SRSessionPolicy::from_json(&raw).map_err(|e| s3_error!(InvalidArgument, "marshal policy failed: {:?}", e))
}
fn compat_time_sentinel() -> OffsetDateTime {
OffsetDateTime::UNIX_EPOCH
}
@@ -294,6 +305,18 @@ impl Operation for AddServiceAccount {
}
}
let replication_claims = opts.claims.clone().unwrap_or_default();
let replication_policy = create_req
.policy
.as_ref()
.map(serde_json::to_string)
.transpose()
.map_err(|e| s3_error!(InvalidArgument, "marshal policy failed: {:?}", e))?;
let replication_groups = target_groups.clone().unwrap_or_default();
let replication_name = opts.name.clone().unwrap_or_default();
let replication_description = opts.description.clone().unwrap_or_default();
let replication_expiration = opts.expiration;
let (new_cred, _) = iam_store
.new_service_account(&target_user, target_groups, opts)
.await
@@ -302,6 +325,39 @@ impl Operation for AddServiceAccount {
s3_error!(InternalError, "create service account failed, e: {:?}", e)
})?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "service-account".to_string(),
svc_acc_change: Some(SRSvcAccChange {
create: Some(SRSvcAccCreate {
parent: target_user.clone(),
access_key: new_cred.access_key.clone(),
secret_key: new_cred.secret_key.clone(),
groups: replication_groups,
claims: replication_claims,
session_policy: replication_policy
.as_deref()
.map(SRSessionPolicy::from_json)
.transpose()
.map_err(|e| s3_error!(InvalidArgument, "marshal policy failed: {:?}", e))?
.unwrap_or_default(),
status: String::new(),
name: replication_name,
description: replication_description,
expiration: replication_expiration,
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
}),
updated_at: Some(OffsetDateTime::now_utc()),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(access_key = %new_cred.access_key, error = ?err, "site replication add service account hook failed");
}
let resp = AddServiceAccountResp {
credentials: Credentials {
access_key: &new_cred.access_key,
@@ -502,22 +558,54 @@ impl Operation for UpdateServiceAccount {
return Err(s3_error!(AccessDenied, "access denied"));
}
let sp = parse_update_service_account_policy(update_req.new_policy)?;
let new_secret_key = update_req.new_secret_key.clone();
let new_status = update_req.new_status.clone();
let new_name = update_req.new_name.clone();
let new_description = update_req.new_description.clone();
let new_expiration = update_req.new_expiration;
let new_policy = update_req.new_policy.clone();
let sp = parse_update_service_account_policy(new_policy.clone())?;
let opts = UpdateServiceAccountOpts {
secret_key: update_req.new_secret_key,
status: update_req.new_status,
name: update_req.new_name,
description: update_req.new_description,
expiration: update_req.new_expiration,
secret_key: new_secret_key.clone(),
status: new_status.clone(),
name: new_name.clone(),
description: new_description.clone(),
expiration: new_expiration,
session_policy: sp,
};
let _ = iam_store
let updated_at = iam_store
.update_service_account(&access_key, opts)
.await
.map_err(|e| map_service_account_lookup_error(e, "update service account failed"))?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "service-account".to_string(),
svc_acc_change: Some(SRSvcAccChange {
update: Some(SRSvcAccUpdate {
access_key: access_key.clone(),
secret_key: new_secret_key.unwrap_or_default(),
status: new_status.unwrap_or_default(),
name: new_name.unwrap_or_default(),
description: new_description.unwrap_or_default(),
session_policy: sr_session_policy_from_value(new_policy.as_ref())?,
expiration: new_expiration,
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(access_key = %access_key, error = ?err, "site replication update service account hook failed");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
@@ -1205,6 +1293,25 @@ impl Operation for DeleteServiceAccount {
s3_error!(InternalError, "delete service account failed")
})?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "service-account".to_string(),
svc_acc_change: Some(SRSvcAccChange {
delete: Some(SRSvcAccDelete {
access_key: query.access_key.clone(),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
}),
updated_at: Some(OffsetDateTime::now_utc()),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(access_key = %query.access_key, error = ?err, "site replication delete service account hook failed");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
File diff suppressed because it is too large Load Diff
+50 -6
View File
@@ -14,7 +14,10 @@
use super::is_admin::IsAdminHandler;
use crate::{
admin::router::{AdminOperation, Operation, S3Router},
admin::{
handlers::site_replication::site_replication_iam_change_hook,
router::{AdminOperation, Operation, S3Router},
},
auth::{check_key_valid, get_session_token},
server::ADMIN_PREFIX,
};
@@ -23,8 +26,10 @@ use http::header::HeaderValue;
use hyper::Method;
use matchit::Params;
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
use rustfs_credentials::get_global_action_cred;
use rustfs_ecstore::bucket::utils::serialize;
use rustfs_iam::{manager::get_token_signing_key, oidc::OidcClaims, sys::SESSION_POLICY_NAME};
use rustfs_madmin::{SITE_REPL_API_VERSION, SRIAMItem, SRSTSCredential};
use rustfs_policy::{auth::get_new_credentials_with_metadata, policy::Policy};
use s3s::{
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
@@ -164,11 +169,31 @@ async fn handle_assume_role(
debug!("AssumeRole get new_cred {:?}", &new_cred);
if let Err(_err) = iam_store.set_temp_user(&new_cred.access_key, &new_cred, None).await {
return Err(s3_error!(InternalError, "set_temp_user failed"));
}
let updated_at = iam_store
.set_temp_user(&new_cred.access_key, &new_cred, None)
.await
.map_err(|_| s3_error!(InternalError, "set_temp_user failed"))?;
// TODO: globalSiteReplicationSys
let root_access_key = get_global_action_cred().map(|cred| cred.access_key);
if root_access_key.as_deref() != Some(new_cred.parent_user.as_str())
&& let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "sts-credential".to_string(),
sts_credential: Some(SRSTSCredential {
access_key: new_cred.access_key.clone(),
secret_key: new_cred.secret_key.clone(),
session_token: new_cred.session_token.clone(),
parent_user: new_cred.parent_user.clone(),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!("site replication STS hook failed, err: {err}");
}
let resp = AssumeRoleOutput {
credentials: Some(Credentials {
@@ -358,11 +383,30 @@ pub async fn create_oidc_sts_credentials(
// Store temp user in IAM
let iam_store = rustfs_iam::get().map_err(|_| s3_error!(InternalError, "IAM not initialized"))?;
iam_store
let updated_at = iam_store
.set_temp_user(&new_cred.access_key, &new_cred, None)
.await
.map_err(|_| s3_error!(InternalError, "failed to store temp user"))?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "sts-credential".to_string(),
sts_credential: Some(SRSTSCredential {
access_key: new_cred.access_key.clone(),
secret_key: new_cred.secret_key.clone(),
session_token: new_cred.session_token.clone(),
parent_user: new_cred.parent_user.clone(),
parent_policy_mapping: policies.join(","),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!("site replication OIDC STS hook failed, err: {err}");
}
Ok(new_cred)
}
+37 -3
View File
@@ -16,6 +16,7 @@ use super::{account_info, group, service_account, user_iam, user_lifecycle, user
use crate::{
admin::{
auth::validate_admin_request,
handlers::site_replication::site_replication_iam_change_hook,
router::{AdminOperation, Operation, S3Router},
utils::{encode_compatible_admin_payload, has_space_be, read_compatible_admin_body},
},
@@ -31,7 +32,8 @@ use rustfs_iam::{
sys::{NewServiceAccountOpts, UpdateServiceAccountOpts},
};
use rustfs_madmin::{
AccountStatus, AddOrUpdateUserReq, IAMEntities, IAMErrEntities, IAMErrEntity, IAMErrPolicyEntity,
AccountStatus, AddOrUpdateUserReq, IAMEntities, IAMErrEntities, IAMErrEntity, IAMErrPolicyEntity, SITE_REPL_API_VERSION,
SRIAMItem, SRIAMUser,
user::{ImportIAMResult, SRSessionPolicy, SRSvcAccCreate},
};
use rustfs_policy::policy::action::{Action, AdminAction};
@@ -225,11 +227,28 @@ impl Operation for AddUser {
)
.await?;
iam_store
let updated_at = iam_store
.create_user(ak, &args)
.await
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("create_user err {e}")))?;
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "iam-user".to_string(),
iam_user: Some(SRIAMUser {
access_key: ak.to_string(),
is_delete_req: false,
user_req: Some(args.clone()),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
updated_at: Some(updated_at),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(access_key = %ak, error = ?err, "site replication create user hook failed");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
@@ -432,7 +451,22 @@ impl Operation for RemoveUser {
.await
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("delete_user err {e}")))?;
// TODO: IAMChangeHook
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
r#type: "iam-user".to_string(),
iam_user: Some(SRIAMUser {
access_key: ak.to_string(),
is_delete_req: true,
user_req: None,
api_version: Some(SITE_REPL_API_VERSION.to_string()),
}),
updated_at: Some(time::OffsetDateTime::now_utc()),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
})
.await
{
warn!(access_key = %ak, error = ?err, "site replication delete user hook failed");
}
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
+4 -1
View File
@@ -16,6 +16,7 @@ mod auth;
pub mod console;
pub mod handlers;
pub mod router;
pub mod service;
pub mod utils;
#[cfg(test)]
@@ -24,7 +25,8 @@ mod console_test;
mod route_registration_test;
use handlers::{
bucket_meta, heal, health, kms, oidc, pools, profile_admin, quota, rebalance, replication, sts, system, tier, user,
bucket_meta, heal, health, kms, oidc, pools, profile_admin, quota, rebalance, replication, site_replication, sts, system,
tier, user,
};
use router::{AdminOperation, S3Router};
use s3s::route::S3Route;
@@ -55,6 +57,7 @@ pub fn make_admin_route(console_enabled: bool) -> std::io::Result<impl S3Route>
bucket_meta::register_bucket_meta_route(&mut r)?;
replication::register_replication_route(&mut r)?;
site_replication::register_site_replication_route(&mut r)?;
profile_admin::register_profiling_route(&mut r)?;
kms::register_kms_route(&mut r)?;
oidc::register_oidc_route(&mut r)?;
+24 -2
View File
@@ -14,7 +14,8 @@
use crate::admin::{
handlers::{
bucket_meta, heal, health, kms, oidc, pools, profile_admin, quota, rebalance, replication, sts, system, tier, user,
bucket_meta, heal, health, kms, oidc, pools, profile_admin, quota, rebalance, replication, site_replication, sts, system,
tier, user,
},
router::{AdminOperation, S3Router},
};
@@ -50,6 +51,7 @@ fn register_admin_routes(router: &mut S3Router<AdminOperation>) {
quota::register_quota_route(router).expect("register quota route");
bucket_meta::register_bucket_meta_route(router).expect("register bucket meta route");
replication::register_replication_route(router).expect("register replication route");
site_replication::register_site_replication_route(router).expect("register site replication route");
profile_admin::register_profiling_route(router).expect("register profile route");
kms::register_kms_route(router).expect("register kms route");
oidc::register_oidc_route(router).expect("register oidc route");
@@ -60,7 +62,6 @@ fn test_register_routes_cover_representative_admin_paths() {
let mut router: S3Router<AdminOperation> = S3Router::new(false);
register_admin_routes(&mut router);
assert_route(&router, Method::GET, HEALTH_PREFIX);
assert_route(&router, Method::HEAD, HEALTH_PREFIX);
assert_route(&router, Method::GET, HEALTH_READY_PATH);
@@ -119,6 +120,23 @@ fn test_register_routes_cover_representative_admin_paths() {
assert_route(&router, Method::PUT, &admin_path("/v3/import-bucket-metadata"));
assert_route(&router, Method::GET, &admin_path("/v3/list-remote-targets"));
assert_route(&router, Method::PUT, &admin_path("/v3/set-remote-target"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/add"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/remove"));
assert_route(&router, Method::GET, &admin_path("/v3/site-replication/info"));
assert_route(&router, Method::GET, &admin_path("/v3/site-replication/metainfo"));
assert_route(&router, Method::GET, &admin_path("/v3/site-replication/status"));
assert_route(&router, Method::POST, &admin_path("/v3/site-replication/devnull"));
assert_route(&router, Method::POST, &admin_path("/v3/site-replication/netperf"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/join"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/bucket-ops"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/iam-item"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/bucket-meta"));
assert_route(&router, Method::GET, &admin_path("/v3/site-replication/peer/idp-settings"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/edit"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/edit"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/remove"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/resync/op"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/state/edit"));
assert_route(&router, Method::GET, &admin_path("/debug/pprof/profile"));
assert_route(&router, Method::POST, &admin_path("/v3/kms/create-key"));
@@ -178,6 +196,10 @@ fn test_admin_alias_paths_match_existing_admin_routes() {
(Method::GET, compat_admin_alias_path("/v3/oidc/callback/default")),
(Method::GET, compat_admin_alias_path("/v3/oidc/config")),
(Method::PUT, compat_admin_alias_path("/v3/oidc/config/default")),
(Method::PUT, compat_admin_alias_path("/v3/site-replication/add")),
(Method::GET, compat_admin_alias_path("/v3/site-replication/info")),
(Method::GET, compat_admin_alias_path("/v3/site-replication/status")),
(Method::PUT, compat_admin_alias_path("/v3/site-replication/peer/join")),
(Method::GET, compat_admin_alias_path("/export-bucket-metadata")),
(Method::GET, compat_admin_alias_path("/v3/export-bucket-metadata")),
(Method::PUT, compat_admin_alias_path("/import-bucket-metadata")),
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod site_replication;
@@ -0,0 +1,43 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use rustfs_ecstore::config::com::read_config;
use rustfs_ecstore::error::Error as StorageError;
use rustfs_ecstore::new_object_layer_fn;
use s3s::{S3Error, S3ErrorCode, S3Result};
const SITE_REPLICATION_STATE_PATH: &str = "config/site-replication/state.json";
/// Reload persisted site-replication state.
///
/// RustFS does not currently keep a separate in-memory cache for this state,
/// so "reload" means validating that the persisted JSON is readable.
pub async fn reload_site_replication_runtime_state() -> S3Result<()> {
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
match read_config(store, SITE_REPLICATION_STATE_PATH).await {
Ok(data) => {
let _: serde_json::Value = serde_json::from_slice(&data)
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("invalid site replication state: {e}")))?;
Ok(())
}
Err(StorageError::ConfigNotFound) => Ok(()),
Err(err) => Err(S3Error::with_message(
S3ErrorCode::InternalError,
format!("failed to load site replication state: {err}"),
)),
}
}
+106 -1
View File
@@ -14,6 +14,9 @@
//! Bucket application use-case contracts.
use crate::admin::handlers::site_replication::{
site_replication_bucket_meta_hook, site_replication_delete_bucket_hook, site_replication_make_bucket_hook,
};
use crate::app::context::{AppContext, default_notify_interface, get_global_app_context};
use crate::auth::get_condition_values;
use crate::error::ApiError;
@@ -49,6 +52,7 @@ use rustfs_ecstore::store_api::{
BucketOperations, BucketOptions, DeleteBucketOptions, ListObjectVersionsInfo, ListObjectsV2Info, ListOperations,
MakeBucketOptions, ObjectInfo,
};
use rustfs_madmin::{SITE_REPL_API_VERSION, SRBucketMeta};
use rustfs_policy::policy::{
action::{Action, S3Action},
{BucketPolicy, BucketPolicyArgs, Effect, Validator},
@@ -81,6 +85,16 @@ fn to_internal_error(err: impl Display) -> S3Error {
S3Error::with_message(S3ErrorCode::InternalError, format!("{err}"))
}
fn sr_bucket_meta_item(bucket: String, item_type: &str) -> SRBucketMeta {
SRBucketMeta {
bucket,
r#type: item_type.to_string(),
updated_at: Some(time::OffsetDateTime::now_utc()),
api_version: Some(SITE_REPL_API_VERSION.to_string()),
..Default::default()
}
}
fn versioning_configuration_has_object_lock_incompatible_settings(config: &VersioningConfiguration) -> bool {
config.suspended()
|| config.exclude_folders.unwrap_or(false)
@@ -538,6 +552,7 @@ impl DefaultBucketUsecase {
object_lock_enabled_for_bucket,
..
} = req.input;
let lock_enabled = object_lock_enabled_for_bucket.is_some_and(|v| v);
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
@@ -548,7 +563,7 @@ impl DefaultBucketUsecase {
&bucket,
&MakeBucketOptions {
force_create: false,
lock_enabled: object_lock_enabled_for_bucket.is_some_and(|v| v),
lock_enabled,
..Default::default()
},
)
@@ -566,6 +581,10 @@ impl DefaultBucketUsecase {
Err(e) => return Err(ApiError::from(e).into()),
}
if let Err(err) = site_replication_make_bucket_hook(&bucket, lock_enabled).await {
warn!(bucket = %bucket, error = ?err, "site replication make bucket hook failed");
}
let output = CreateBucketOutput::default();
counter!("rustfs_create_bucket_total").increment(1);
let result = Ok(S3Response::new(output));
@@ -637,6 +656,10 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
if let Err(err) = site_replication_delete_bucket_hook(&input.bucket, force).await {
warn!(bucket = %input.bucket, error = ?err, "site replication delete bucket hook failed");
}
let result = Ok(S3Response::new(DeleteBucketOutput {}));
let _ = helper.complete(&result);
result
@@ -791,6 +814,11 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let item = sr_bucket_meta_item(bucket.clone(), "sse-config");
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket encryption delete hook failed");
}
Ok(S3Response::with_status(DeleteBucketEncryptionOutput::default(), StatusCode::NO_CONTENT))
}
@@ -818,6 +846,11 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let item = sr_bucket_meta_item(bucket.clone(), "cors-config");
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket cors delete hook failed");
}
Ok(S3Response::new(DeleteBucketCorsOutput {}))
}
@@ -845,6 +878,11 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let item = sr_bucket_meta_item(bucket.clone(), "lc-config");
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket lifecycle delete hook failed");
}
Ok(S3Response::new(DeleteBucketLifecycleOutput::default()))
}
@@ -871,6 +909,11 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let item = sr_bucket_meta_item(bucket.clone(), "policy");
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket policy delete hook failed");
}
Ok(S3Response::new(DeleteBucketPolicyOutput {}))
}
@@ -896,6 +939,11 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let item = sr_bucket_meta_item(bucket.clone(), "replication-config");
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket replication-config delete hook failed");
}
// TODO: remove targets
info!(bucket = %bucket, "deleted bucket replication config");
@@ -917,6 +965,11 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let item = sr_bucket_meta_item(bucket.clone(), "tags");
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket tagging delete hook failed");
}
Ok(S3Response::new(tagging::build_delete_bucket_tagging_output()))
}
@@ -1374,6 +1427,15 @@ impl DefaultBucketUsecase {
metadata_sys::update(&bucket, BUCKET_SSECONFIG, data)
.await
.map_err(ApiError::from)?;
let mut item = sr_bucket_meta_item(bucket.clone(), "sse-config");
item.sse_config = Some(
serialize_config(&server_side_encryption_configuration)
.and_then(|bytes| String::from_utf8(bytes).map_err(to_internal_error))?,
);
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket encryption hook failed");
}
Ok(S3Response::new(encryption::build_put_bucket_encryption_output()))
}
@@ -1420,6 +1482,14 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let mut item = sr_bucket_meta_item(bucket.clone(), "lc-config");
item.expiry_lc_config =
Some(serialize_config(&input_cfg).and_then(|bytes| String::from_utf8(bytes).map_err(to_internal_error))?);
item.expiry_updated_at = item.updated_at;
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket lifecycle hook failed");
}
if lifecycle_has_transition_rules(&input_cfg)
&& let Some(store) = new_object_layer_fn()
{
@@ -1564,6 +1634,12 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let mut item = sr_bucket_meta_item(bucket.clone(), "policy");
item.policy = Some(serde_json::from_str(&policy).map_err(|e| s3_error!(InvalidArgument, "parse policy failed {:?}", e))?);
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket policy hook failed");
}
Ok(S3Response::new(PutBucketPolicyOutput {}))
}
@@ -1593,6 +1669,13 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let mut item = sr_bucket_meta_item(bucket.clone(), "cors-config");
item.cors =
Some(serialize_config(&cors_configuration).and_then(|bytes| String::from_utf8(bytes).map_err(to_internal_error))?);
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket cors hook failed");
}
Ok(S3Response::new(PutBucketCorsOutput::default()))
}
@@ -1626,6 +1709,14 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let mut item = sr_bucket_meta_item(bucket.clone(), "replication-config");
item.replication_config = Some(
serialize_config(&replication_configuration).and_then(|bytes| String::from_utf8(bytes).map_err(to_internal_error))?,
);
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket replication-config hook failed");
}
Ok(S3Response::new(replication::build_put_bucket_replication_output()))
}
@@ -1687,6 +1778,12 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let mut item = sr_bucket_meta_item(bucket.clone(), "tags");
item.tags = Some(serialize_config(&tagging).and_then(|bytes| String::from_utf8(bytes).map_err(to_internal_error))?);
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket tagging hook failed");
}
Ok(S3Response::new(tagging::build_put_bucket_tagging_output()))
}
@@ -1713,6 +1810,14 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let mut item = sr_bucket_meta_item(bucket.clone(), "version-config");
item.versioning = Some(
serialize_config(&versioning_configuration).and_then(|bytes| String::from_utf8(bytes).map_err(to_internal_error))?,
);
if let Err(err) = site_replication_bucket_meta_hook(item).await {
warn!(bucket = %bucket, error = ?err, "site replication bucket versioning hook failed");
}
Ok(S3Response::new(PutBucketVersioningOutput {}))
}
+11 -1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::admin::service::site_replication::reload_site_replication_runtime_state;
use bytes::Bytes;
use futures::Stream;
use futures_util::future::join_all;
@@ -757,7 +758,16 @@ impl Node for NodeService {
error_info: Some("errServerNotInitialized".to_string()),
}));
};
todo!()
match reload_site_replication_runtime_state().await {
Ok(()) => Ok(Response::new(ReloadSiteReplicationConfigResponse {
success: true,
error_info: None,
})),
Err(err) => Ok(Response::new(ReloadSiteReplicationConfigResponse {
success: false,
error_info: Some(err.to_string()),
})),
}
}
async fn signal_service(&self, request: Request<SignalServiceRequest>) -> Result<Response<SignalServiceResponse>, Status> {