mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 16:46:55 +00:00
17f0bd2637
* fix(iam): report duplicate access keys clearly * fix(iam): narrow duplicate access key handling
1804 lines
68 KiB
Rust
1804 lines
68 KiB
Rust
// 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 super::iam_error::iam_error_to_s3_error;
|
|
use crate::admin::access_key_identity;
|
|
use crate::admin::handlers::site_replication::site_replication_iam_change_hook;
|
|
use crate::admin::runtime_sources::current_action_credentials;
|
|
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};
|
|
use crate::{
|
|
admin::router::{AdminOperation, Operation, S3Router},
|
|
auth::check_key_valid,
|
|
};
|
|
use http::HeaderMap;
|
|
use hyper::{Method, StatusCode};
|
|
use matchit::Params;
|
|
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
|
|
use rustfs_credentials::Credentials as StoredCredentials;
|
|
use rustfs_iam::error::{is_err_no_such_service_account, is_err_no_such_temp_account};
|
|
use rustfs_iam::store::Store as IamStore;
|
|
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, InfoServiceAccountResp, ListAccessKeysResp, ListServiceAccountsResp,
|
|
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};
|
|
use s3s::S3ErrorCode::InvalidRequest;
|
|
use s3s::header::CONTENT_LENGTH;
|
|
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
|
|
use serde::Deserialize;
|
|
use serde_urlencoded::from_bytes;
|
|
use std::collections::HashMap;
|
|
use time::OffsetDateTime;
|
|
use tracing::{debug, warn};
|
|
use url::form_urlencoded;
|
|
|
|
const LOG_COMPONENT_ADMIN: &str = "admin";
|
|
const LOG_SUBSYSTEM_SERVICE_ACCOUNT: &str = "service_account";
|
|
const EVENT_ADMIN_SERVICE_ACCOUNT_STATE: &str = "admin_service_account_state";
|
|
|
|
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
|
|
}
|
|
|
|
fn list_expiration_or_sentinel(expiration: Option<OffsetDateTime>) -> Option<OffsetDateTime> {
|
|
Some(expiration.unwrap_or_else(compat_time_sentinel))
|
|
}
|
|
|
|
fn expiration_for_admin_path(path: &str, expiration: Option<OffsetDateTime>) -> Option<OffsetDateTime> {
|
|
if is_compat_admin_request(path) {
|
|
list_expiration_or_sentinel(expiration)
|
|
} else {
|
|
expiration
|
|
}
|
|
}
|
|
|
|
fn delete_service_account_success_status(path: &str) -> StatusCode {
|
|
if is_compat_admin_request(path) {
|
|
StatusCode::NO_CONTENT
|
|
} else {
|
|
StatusCode::OK
|
|
}
|
|
}
|
|
|
|
fn merge_derived_service_account_claims(
|
|
target_claims: &mut HashMap<String, serde_json::Value>,
|
|
source_claims: &HashMap<String, serde_json::Value>,
|
|
) {
|
|
for (key, value) in source_claims {
|
|
if key == "exp" {
|
|
continue;
|
|
}
|
|
target_claims.insert(key.clone(), value.clone());
|
|
}
|
|
}
|
|
|
|
fn is_service_account_owner_of(caller: &StoredCredentials, target_parent_user: &str) -> bool {
|
|
let caller_parent = if caller.parent_user.is_empty() {
|
|
caller.access_key.as_str()
|
|
} else {
|
|
caller.parent_user.as_str()
|
|
};
|
|
|
|
caller_parent == target_parent_user
|
|
}
|
|
|
|
/// Fail-closed ownership check used by DeleteServiceAccount for a caller that
|
|
/// lacks the RemoveServiceAccount admin action.
|
|
///
|
|
/// Returns `true` only when the target service account was successfully loaded
|
|
/// AND is owned by the caller. A `None` target — meaning `get_service_account`
|
|
/// failed with a non-not-found error (e.g. a stored-credential decode error) so
|
|
/// ownership could not be verified — denies, as does an owner mismatch. The
|
|
/// previous `svc_account.is_some_and(|v| v.parent_user != user)` form returned
|
|
/// `false` for a `None` target and thus fell through to the delete, an authz
|
|
/// bypass (backlog#806).
|
|
fn non_admin_may_delete_service_account(caller_user: &str, target_parent_user: Option<&str>) -> bool {
|
|
matches!(target_parent_user, Some(parent) if parent == caller_user)
|
|
}
|
|
|
|
fn map_service_account_lookup_error(err: rustfs_iam::error::Error, action: &str) -> S3Error {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
action,
|
|
result = "lookup_failed",
|
|
error = ?err,
|
|
"admin service account state"
|
|
);
|
|
if is_err_no_such_service_account(&err) {
|
|
iam_error_to_s3_error(err)
|
|
} else {
|
|
s3_error!(InternalError, "{action}")
|
|
}
|
|
}
|
|
|
|
fn map_temp_account_lookup_error(err: rustfs_iam::error::Error, action: &str) -> S3Error {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
action,
|
|
result = "temporary_lookup_failed",
|
|
error = ?err,
|
|
"admin service account state"
|
|
);
|
|
if is_err_no_such_temp_account(&err) {
|
|
iam_error_to_s3_error(err)
|
|
} else {
|
|
s3_error!(InternalError, "{action}")
|
|
}
|
|
}
|
|
|
|
fn parse_service_account_policy(policy: &serde_json::Value) -> S3Result<Policy> {
|
|
let policy_bytes = serde_json::to_vec(policy).map_err(|e| s3_error!(InvalidArgument, "marshal policy failed: {:?}", e))?;
|
|
Policy::parse_config(&policy_bytes).map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
result = "policy_parse_failed",
|
|
error = ?e,
|
|
"admin service account state"
|
|
);
|
|
match e {
|
|
rustfs_policy::error::Error::PolicyError(rustfs_policy::policy::Error::NonResource) => {
|
|
s3_error!(InvalidArgument, "invalid service account policy: Resource is empty")
|
|
}
|
|
rustfs_policy::error::Error::PolicyError(err) => {
|
|
s3_error!(InvalidArgument, "invalid service account policy: {}", err)
|
|
}
|
|
rustfs_policy::error::Error::Io(err) if err.get_ref().is_some_and(|source| source.is::<serde_json::Error>()) => {
|
|
s3_error!(
|
|
InvalidArgument,
|
|
"Policy format is invalid. Please check the JSON structure and ensure it follows the IAM policy format."
|
|
)
|
|
}
|
|
err => s3_error!(InvalidArgument, "invalid service account policy: {}", err),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn parse_update_service_account_policy(new_policy: Option<serde_json::Value>) -> S3Result<Option<Policy>> {
|
|
let Some(policy) = new_policy else {
|
|
return Ok(None);
|
|
};
|
|
|
|
Ok(Some(parse_service_account_policy(&policy)?))
|
|
}
|
|
|
|
pub fn register_service_account_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
|
r.insert(
|
|
Method::POST,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/update-service-account").as_str(),
|
|
AdminOperation(&UpdateServiceAccount {}),
|
|
)?;
|
|
|
|
r.insert(
|
|
Method::GET,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/info-service-account").as_str(),
|
|
AdminOperation(&InfoServiceAccount {}),
|
|
)?;
|
|
r.insert(
|
|
Method::GET,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/temporary-account-info").as_str(),
|
|
AdminOperation(&TemporaryAccountInfo {}),
|
|
)?;
|
|
r.insert(
|
|
Method::GET,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/info-access-key").as_str(),
|
|
AdminOperation(&InfoAccessKey {}),
|
|
)?;
|
|
|
|
r.insert(
|
|
Method::GET,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/list-service-accounts").as_str(),
|
|
AdminOperation(&ListServiceAccount {}),
|
|
)?;
|
|
r.insert(
|
|
Method::GET,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/list-access-keys-bulk").as_str(),
|
|
AdminOperation(&ListAccessKeysBulk {}),
|
|
)?;
|
|
|
|
r.insert(
|
|
Method::DELETE,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/delete-service-accounts").as_str(),
|
|
AdminOperation(&DeleteServiceAccount {}),
|
|
)?;
|
|
r.insert(
|
|
Method::DELETE,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/delete-service-account").as_str(),
|
|
AdminOperation(&DeleteServiceAccount {}),
|
|
)?;
|
|
|
|
r.insert(
|
|
Method::PUT,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/add-service-accounts").as_str(),
|
|
AdminOperation(&AddServiceAccount {}),
|
|
)?;
|
|
r.insert(
|
|
Method::PUT,
|
|
format!("{}{}", ADMIN_PREFIX, "/v3/add-service-account").as_str(),
|
|
AdminOperation(&AddServiceAccount {}),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub struct AddServiceAccount {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for AddServiceAccount {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let Some(req_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &req_cred.access_key).await?;
|
|
|
|
let body = read_compatible_admin_body(req.input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri.path(), &cred.secret_key).await?;
|
|
|
|
let create_req: AddServiceAccountReq =
|
|
serde_json::from_slice(&body[..]).map_err(|e| s3_error!(InvalidRequest, "unmarshal body failed, e: {:?}", e))?;
|
|
|
|
// create_req.expiration = create_req.expiration.and_then(|expire| expire.replace_millisecond(0).ok());
|
|
|
|
if has_space_be(&create_req.access_key) {
|
|
return Err(s3_error!(InvalidRequest, "access key has spaces"));
|
|
}
|
|
|
|
create_req.validate().map_err(|e| S3Error::with_message(InvalidRequest, e))?;
|
|
|
|
let session_policy = if let Some(policy) = &create_req.policy {
|
|
Some(parse_service_account_policy(policy)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let Some(sys_cred) = current_action_credentials() else {
|
|
return Err(s3_error!(InvalidRequest, "get sys cred failed"));
|
|
};
|
|
|
|
if constant_time_eq(&sys_cred.access_key, &create_req.access_key) {
|
|
return Err(s3_error!(InvalidArgument, "can't create user with system access key"));
|
|
}
|
|
|
|
let mut target_user = if let Some(u) = create_req.target_user {
|
|
u
|
|
} else {
|
|
cred.access_key.clone()
|
|
};
|
|
|
|
let req_user = cred.access_key.clone();
|
|
let mut req_parent_user = cred.access_key.clone();
|
|
let req_groups = cred.groups.clone();
|
|
let mut req_is_derived_cred = false;
|
|
|
|
if cred.is_service_account() || cred.is_temp() {
|
|
req_parent_user = cred.parent_user.clone();
|
|
req_is_derived_cred = true;
|
|
}
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::CreateServiceAccountAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: false, // Always require explicit Allow permission
|
|
})
|
|
.await
|
|
{
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
if target_user != cred.access_key {
|
|
let has_user = iam_store.get_user(&target_user).await;
|
|
if has_user.is_none() && target_user != sys_cred.access_key {
|
|
return Err(s3_error!(InvalidRequest, "target user not exist"));
|
|
}
|
|
}
|
|
|
|
let is_svc_acc = target_user == req_user || target_user == req_parent_user;
|
|
|
|
let mut target_groups = None;
|
|
let mut opts = NewServiceAccountOpts {
|
|
access_key: create_req.access_key,
|
|
secret_key: create_req.secret_key,
|
|
name: create_req.name,
|
|
description: create_req.description.or(create_req.comment),
|
|
expiration: create_req.expiration,
|
|
session_policy,
|
|
..Default::default()
|
|
};
|
|
|
|
if is_svc_acc {
|
|
if req_is_derived_cred {
|
|
if req_parent_user.is_empty() {
|
|
return Err(s3_error!(AccessDenied, "only derived cred can create service account"));
|
|
}
|
|
target_user = req_parent_user;
|
|
}
|
|
|
|
target_groups = req_groups;
|
|
|
|
if let Some(claims) = cred.claims {
|
|
if opts.claims.is_none() {
|
|
opts.claims = Some(HashMap::new());
|
|
}
|
|
|
|
merge_derived_service_account_claims(opts.claims.as_mut().unwrap(), &claims);
|
|
}
|
|
}
|
|
|
|
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
|
|
.map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
target_user = %target_user,
|
|
result = "create_failed",
|
|
error = ?e,
|
|
"admin service account state"
|
|
);
|
|
match e {
|
|
rustfs_iam::error::Error::InvalidAccessKeyLength
|
|
| rustfs_iam::error::Error::InvalidSecretKeyLength
|
|
| rustfs_iam::error::Error::AccessKeyAlreadyExists => iam_error_to_s3_error(e),
|
|
err => s3_error!(InternalError, "create service account failed, e: {:?}", err),
|
|
}
|
|
})?;
|
|
|
|
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!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
access_key = %new_cred.access_key,
|
|
action = "create",
|
|
result = "site_replication_hook_failed",
|
|
error = ?err,
|
|
"admin service account state"
|
|
);
|
|
}
|
|
|
|
let resp = AddServiceAccountResp {
|
|
credentials: Credentials {
|
|
access_key: &new_cred.access_key,
|
|
secret_key: &new_cred.secret_key,
|
|
session_token: None,
|
|
expiration: new_cred.expiration,
|
|
},
|
|
};
|
|
|
|
let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?;
|
|
let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?;
|
|
|
|
let mut header = HeaderMap::new();
|
|
header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value"));
|
|
|
|
Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
struct AccessKeyQuery {
|
|
#[serde(rename = "accessKey", alias = "access-key")]
|
|
pub access_key: String,
|
|
}
|
|
|
|
fn request_user_name(cred: &StoredCredentials) -> &str {
|
|
if cred.parent_user.is_empty() {
|
|
&cred.access_key
|
|
} else {
|
|
&cred.parent_user
|
|
}
|
|
}
|
|
|
|
fn can_fallback_view_access_key_info(requester: &StoredCredentials, target: &StoredCredentials) -> bool {
|
|
if requester.is_service_account() {
|
|
return false;
|
|
}
|
|
|
|
if target.is_temp() || target.is_service_account() {
|
|
return request_user_name(requester) == target.parent_user;
|
|
}
|
|
|
|
request_user_name(requester) == target.access_key
|
|
}
|
|
|
|
fn can_fallback_view_access_key_info_after_admin_check_failed(
|
|
requester: &StoredCredentials,
|
|
target: &StoredCredentials,
|
|
no_explicit_deny: bool,
|
|
) -> bool {
|
|
no_explicit_deny && can_fallback_view_access_key_info(requester, target)
|
|
}
|
|
|
|
async fn build_info_service_account_resp<T: IamStore>(
|
|
iam_store: &rustfs_iam::sys::IamSys<T>,
|
|
account: &StoredCredentials,
|
|
session_policy: Option<rustfs_policy::policy::Policy>,
|
|
) -> S3Result<InfoServiceAccountResp> {
|
|
access_key_identity::build_info_service_account_resp(iam_store, account, session_policy).await
|
|
}
|
|
|
|
pub struct UpdateServiceAccount {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for UpdateServiceAccount {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let query = {
|
|
if let Some(query) = req.uri.query() {
|
|
let input: AccessKeyQuery =
|
|
from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?;
|
|
input
|
|
} else {
|
|
AccessKeyQuery::default()
|
|
}
|
|
};
|
|
|
|
if query.access_key.is_empty() {
|
|
return Err(s3_error!(InvalidArgument, "access key is empty"));
|
|
}
|
|
|
|
let access_key = query.access_key;
|
|
|
|
let Some(input_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
// let svc_account = iam_store.get_service_account(&access_key).await.map_err(|e| {
|
|
// debug!("get service account failed, e: {:?}", e);
|
|
// s3_error!(InternalError, "get service account failed")
|
|
// })?;
|
|
|
|
let body =
|
|
read_compatible_admin_body(req.input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri.path(), input_cred.secret_key.expose())
|
|
.await?;
|
|
|
|
let update_req: UpdateServiceAccountReq =
|
|
serde_json::from_slice(&body[..]).map_err(|e| s3_error!(InvalidRequest, "unmarshal body failed, e: {:?}", e))?;
|
|
|
|
update_req.validate().map_err(|e| S3Error::with_message(InvalidRequest, e))?;
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
|
|
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: false,
|
|
})
|
|
.await
|
|
{
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
let (svc_account, _) = iam_store
|
|
.get_service_account(&access_key)
|
|
.await
|
|
.map_err(|e| map_service_account_lookup_error(e, "get service account failed"))?;
|
|
|
|
if !is_service_account_owner_of(&cred, &svc_account.parent_user) {
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
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: new_secret_key.clone(),
|
|
status: new_status.clone(),
|
|
name: new_name.clone(),
|
|
description: new_description.clone(),
|
|
expiration: new_expiration,
|
|
session_policy: sp,
|
|
allow_site_replicator_account: false,
|
|
};
|
|
|
|
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().expect("valid header value"));
|
|
header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value"));
|
|
Ok(S3Response::with_headers((StatusCode::NO_CONTENT, Body::empty()), header))
|
|
}
|
|
}
|
|
|
|
pub struct InfoServiceAccount {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for InfoServiceAccount {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let query = {
|
|
if let Some(query) = req.uri.query() {
|
|
let input: AccessKeyQuery =
|
|
from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?;
|
|
input
|
|
} else {
|
|
AccessKeyQuery::default()
|
|
}
|
|
};
|
|
|
|
if query.access_key.is_empty() {
|
|
return Err(s3_error!(InvalidArgument, "access key is empty"));
|
|
}
|
|
|
|
let access_key = query.access_key;
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
let (svc_account, session_policy) = iam_store
|
|
.get_service_account(&access_key)
|
|
.await
|
|
.map_err(|e| map_service_account_lookup_error(e, "get service account failed"))?;
|
|
|
|
let Some(input_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
|
|
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: false,
|
|
})
|
|
.await
|
|
&& request_user_name(&cred) != svc_account.parent_user
|
|
{
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
let resp = build_info_service_account_resp(&iam_store, &svc_account, session_policy).await?;
|
|
|
|
let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?;
|
|
let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?;
|
|
|
|
let mut header = HeaderMap::new();
|
|
header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value"));
|
|
|
|
Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header))
|
|
}
|
|
}
|
|
|
|
pub struct TemporaryAccountInfo {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for TemporaryAccountInfo {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let query = {
|
|
if let Some(query) = req.uri.query() {
|
|
let input: AccessKeyQuery =
|
|
from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?;
|
|
input
|
|
} else {
|
|
AccessKeyQuery::default()
|
|
}
|
|
};
|
|
|
|
if query.access_key.is_empty() {
|
|
return Err(s3_error!(InvalidArgument, "access key is empty"));
|
|
}
|
|
|
|
let Some(input_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::ListTemporaryAccountsAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: false,
|
|
})
|
|
.await
|
|
{
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
let (temp_account, session_policy) = iam_store
|
|
.get_temporary_account(&query.access_key)
|
|
.await
|
|
.map_err(|e| map_temp_account_lookup_error(e, "get temporary account failed"))?;
|
|
|
|
let resp: TemporaryAccountInfoResp = build_info_service_account_resp(&iam_store, &temp_account, session_policy).await?;
|
|
let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?;
|
|
let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?;
|
|
|
|
let mut header = HeaderMap::new();
|
|
header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value"));
|
|
|
|
Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header))
|
|
}
|
|
}
|
|
|
|
pub struct InfoAccessKey {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for InfoAccessKey {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let query = {
|
|
if let Some(query) = req.uri.query() {
|
|
let input: AccessKeyQuery =
|
|
from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed1"))?;
|
|
input
|
|
} else {
|
|
AccessKeyQuery::default()
|
|
}
|
|
};
|
|
|
|
let Some(input_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
|
|
|
|
let access_key = if query.access_key.is_empty() {
|
|
cred.access_key.clone()
|
|
} else {
|
|
query.access_key
|
|
};
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
let target_cred = iam_store.get_user(&access_key).await.map(|identity| identity.credentials);
|
|
|
|
let conditions = get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
);
|
|
let claims = cred.claims_or_empty();
|
|
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),
|
|
bucket: "",
|
|
conditions: &conditions,
|
|
is_owner: owner,
|
|
object: "",
|
|
claims,
|
|
deny_only: false,
|
|
})
|
|
.await
|
|
{
|
|
let no_explicit_deny = iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),
|
|
bucket: "",
|
|
conditions: &conditions,
|
|
is_owner: owner,
|
|
object: "",
|
|
claims,
|
|
deny_only: true,
|
|
})
|
|
.await;
|
|
if !no_explicit_deny {
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
let Some(target_cred) = target_cred.as_ref() else {
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
};
|
|
|
|
if !can_fallback_view_access_key_info_after_admin_check_failed(&cred, target_cred, no_explicit_deny) {
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
}
|
|
|
|
let Some(target_cred) = target_cred else {
|
|
return Err(s3_error!(InvalidRequest, "access key not exist"));
|
|
};
|
|
|
|
let resp = access_key_identity::resolve_info_access_key_resp(&iam_store, access_key, target_cred).await?;
|
|
|
|
let body = serde_json::to_vec(&resp).map_err(|e| s3_error!(InternalError, "marshal body failed, e: {:?}", e))?;
|
|
let (body, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, body)?;
|
|
|
|
let mut header = HeaderMap::new();
|
|
header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value"));
|
|
|
|
Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, serde::Deserialize)]
|
|
pub struct ListServiceAccountQuery {
|
|
pub user: Option<String>,
|
|
}
|
|
|
|
pub struct ListServiceAccount {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for ListServiceAccount {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let query = {
|
|
if let Some(query) = req.uri.query() {
|
|
let input: ListServiceAccountQuery = from_bytes(query.as_bytes())
|
|
.map_err(|_e| s3_error!(InvalidArgument, "invalid service account query parameters"))?;
|
|
input
|
|
} else {
|
|
ListServiceAccountQuery::default()
|
|
}
|
|
};
|
|
|
|
let Some(input_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key)
|
|
.await
|
|
.map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
result = "check_key_failed",
|
|
error = ?e,
|
|
"admin service account state"
|
|
);
|
|
s3_error!(InternalError, "check key failed")
|
|
})?;
|
|
|
|
// let target_account = if let Some(user) = query.user {
|
|
// if user != input_cred.access_key {
|
|
// user
|
|
// } else if cred.parent_user.is_empty() {
|
|
// input_cred.access_key
|
|
// } else {
|
|
// cred.parent_user
|
|
// }
|
|
// } else if cred.parent_user.is_empty() {
|
|
// input_cred.access_key
|
|
// } else {
|
|
// cred.parent_user
|
|
// };
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
let target_account = if query.user.as_ref().is_some_and(|v| v != &cred.access_key) {
|
|
// Cross-user listing must be authorized by ListServiceAccounts, matching the
|
|
// sibling InfoServiceAccount/InfoAccessKey/ListAccessKeysBulk handlers.
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: false,
|
|
})
|
|
.await
|
|
{
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
query.user.unwrap_or_default()
|
|
} else if cred.parent_user.is_empty() {
|
|
cred.access_key
|
|
} else {
|
|
cred.parent_user
|
|
};
|
|
|
|
let service_accounts = iam_store.list_service_accounts(&target_account).await.map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
target_user = %target_account,
|
|
result = "list_service_accounts_failed",
|
|
error = ?e,
|
|
"admin service account state"
|
|
);
|
|
s3_error!(InternalError, "list service account failed")
|
|
})?;
|
|
|
|
let accounts: Vec<ServiceAccountInfo> = service_accounts
|
|
.into_iter()
|
|
.map(|sa| ServiceAccountInfo {
|
|
parent_user: sa.parent_user.clone(),
|
|
account_status: sa.status.clone(),
|
|
implied_policy: sa.is_implied_policy(), // or set according to your logic
|
|
access_key: sa.access_key,
|
|
name: sa.name,
|
|
description: sa.description,
|
|
expiration: expiration_for_admin_path(req.uri.path(), sa.expiration),
|
|
})
|
|
.collect();
|
|
|
|
let data = serde_json::to_vec(&ListServiceAccountsResp { accounts })
|
|
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal users err {e}")))?;
|
|
let (data, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, data)?;
|
|
|
|
let mut header = HeaderMap::new();
|
|
header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value"));
|
|
|
|
Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ListAccessKeysQuery {
|
|
users: Vec<String>,
|
|
all: bool,
|
|
list_type: String,
|
|
}
|
|
|
|
fn default_access_key_list_type() -> String {
|
|
ACCESS_KEY_LIST_ALL.to_string()
|
|
}
|
|
|
|
impl Default for ListAccessKeysQuery {
|
|
fn default() -> Self {
|
|
Self {
|
|
users: Vec::new(),
|
|
all: false,
|
|
list_type: default_access_key_list_type(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_bool_param(value: &str) -> bool {
|
|
matches!(value, "true" | "1" | "on" | "yes")
|
|
}
|
|
|
|
fn parse_list_access_keys_query(query: Option<&str>) -> ListAccessKeysQuery {
|
|
let mut parsed = ListAccessKeysQuery::default();
|
|
|
|
let Some(query) = query else {
|
|
return parsed;
|
|
};
|
|
|
|
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
|
match key.as_ref() {
|
|
"users" if !value.is_empty() => {
|
|
parsed.users.push(value.into_owned());
|
|
}
|
|
"all" => parsed.all = parse_bool_param(value.as_ref()),
|
|
"listType" => parsed.list_type = value.into_owned(),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
parsed
|
|
}
|
|
|
|
pub struct ListAccessKeysBulk {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for ListAccessKeysBulk {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let query = { parse_list_access_keys_query(req.uri.query()) };
|
|
|
|
if query.all && !query.users.is_empty() {
|
|
return Err(s3_error!(InvalidRequest, "either specify users or all, not both"));
|
|
}
|
|
|
|
let Some(input_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
let mut requested_users = query.users;
|
|
let mut self_only = !query.all && requested_users.is_empty();
|
|
if !query.all && requested_users.len() == 1 {
|
|
let requested_user = &requested_users[0];
|
|
if requested_user == &cred.access_key || requested_user == &cred.parent_user {
|
|
self_only = true;
|
|
}
|
|
}
|
|
|
|
if query.all
|
|
&& !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::ListUsersAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: false,
|
|
})
|
|
.await
|
|
{
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: self_only,
|
|
})
|
|
.await
|
|
{
|
|
return Err(s3_error!(AccessDenied, "access denied"));
|
|
}
|
|
|
|
if self_only && requested_users.is_empty() {
|
|
requested_users.push(request_user_name(&cred).to_string());
|
|
}
|
|
|
|
let checked_user_list = if query.all {
|
|
let mut users = iam_store
|
|
.list_users()
|
|
.await
|
|
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("list users err {e}")))?
|
|
.into_keys()
|
|
.collect::<Vec<_>>();
|
|
|
|
if let Some(sys_cred) = current_action_credentials() {
|
|
users.push(sys_cred.access_key);
|
|
}
|
|
users
|
|
} else {
|
|
// Keep requested identities as-is. Some valid parent users (for example external
|
|
// identities) may not be persisted as regular IAM users, but can still own keys.
|
|
requested_users
|
|
};
|
|
|
|
let (list_sts_keys, list_service_accounts) = match query.list_type.as_str() {
|
|
ACCESS_KEY_LIST_USERS_ONLY => (false, false),
|
|
ACCESS_KEY_LIST_STS_ONLY => (true, false),
|
|
ACCESS_KEY_LIST_SVCACC_ONLY => (false, true),
|
|
ACCESS_KEY_LIST_ALL => (true, true),
|
|
_ => return Err(s3_error!(InvalidRequest, "invalid list type")),
|
|
};
|
|
|
|
let mut access_key_map = HashMap::new();
|
|
for user in checked_user_list {
|
|
let mut access_keys = ListAccessKeysResp::default();
|
|
|
|
if list_sts_keys {
|
|
let sts_keys = iam_store.list_sts_accounts(&user).await.map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
target_user = %user,
|
|
result = "list_sts_accounts_failed",
|
|
error = ?e,
|
|
"admin service account state"
|
|
);
|
|
s3_error!(InternalError, "list sts account failed")
|
|
})?;
|
|
|
|
access_keys.sts_keys = sts_keys
|
|
.into_iter()
|
|
.map(|sts| {
|
|
access_key_identity::list_entry_from_credentials(
|
|
&sts,
|
|
expiration_for_admin_path(req.uri.path(), sts.expiration),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
if !list_service_accounts && access_keys.sts_keys.is_empty() {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if list_service_accounts {
|
|
let service_accounts = iam_store.list_service_accounts(&user).await.map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
target_user = %user,
|
|
result = "list_service_accounts_failed",
|
|
error = ?e,
|
|
"admin service account state"
|
|
);
|
|
s3_error!(InternalError, "list service account failed")
|
|
})?;
|
|
|
|
access_keys.service_accounts = service_accounts
|
|
.into_iter()
|
|
.map(|svc| {
|
|
access_key_identity::list_entry_from_credentials(
|
|
&svc,
|
|
expiration_for_admin_path(req.uri.path(), svc.expiration),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
if !list_sts_keys && access_keys.service_accounts.is_empty() {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
access_key_map.insert(user, access_keys);
|
|
}
|
|
|
|
let data = serde_json::to_vec(&access_key_map)
|
|
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal access keys err {e}")))?;
|
|
let (data, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, data)?;
|
|
|
|
let mut header = HeaderMap::new();
|
|
header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value"));
|
|
|
|
Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header))
|
|
}
|
|
}
|
|
|
|
pub struct DeleteServiceAccount {}
|
|
#[async_trait::async_trait]
|
|
impl Operation for DeleteServiceAccount {
|
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
|
let Some(input_cred) = req.credentials else {
|
|
return Err(s3_error!(InvalidRequest, "get cred failed"));
|
|
};
|
|
|
|
let (cred, owner) =
|
|
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key)
|
|
.await
|
|
.map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = "service_account_auth_failed",
|
|
action = "delete",
|
|
error = ?e,
|
|
"Service account authentication failed"
|
|
);
|
|
s3_error!(InternalError, "check key failed")
|
|
})?;
|
|
|
|
let query = {
|
|
if let Some(query) = req.uri.query() {
|
|
let input: AccessKeyQuery = from_bytes(query.as_bytes())
|
|
.map_err(|_e| s3_error!(InvalidArgument, "invalid access key query parameters"))?;
|
|
input
|
|
} else {
|
|
AccessKeyQuery::default()
|
|
}
|
|
};
|
|
|
|
if query.access_key.is_empty() {
|
|
return Err(s3_error!(InvalidArgument, "access key is empty"));
|
|
}
|
|
|
|
let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
|
|
return Err(s3_error!(InvalidRequest, "iam not init"));
|
|
};
|
|
|
|
let svc_account = match iam_store.get_service_account(&query.access_key).await {
|
|
Ok((res, _)) => Some(res),
|
|
Err(err) => {
|
|
if is_err_no_such_service_account(&err) {
|
|
return Err(s3_error!(InvalidRequest, "service account not exist"));
|
|
}
|
|
|
|
None
|
|
}
|
|
};
|
|
|
|
if !iam_store
|
|
.is_allowed(&Args {
|
|
account: &cred.access_key,
|
|
groups: &cred.groups,
|
|
action: Action::AdminAction(AdminAction::RemoveServiceAccountAdminAction),
|
|
bucket: "",
|
|
conditions: &get_condition_values(
|
|
&req.headers,
|
|
&cred,
|
|
None,
|
|
None,
|
|
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
|
|
),
|
|
is_owner: owner,
|
|
object: "",
|
|
claims: cred.claims_or_empty(),
|
|
deny_only: false,
|
|
})
|
|
.await
|
|
{
|
|
let user = if cred.parent_user.is_empty() {
|
|
cred.access_key.as_str()
|
|
} else {
|
|
cred.parent_user.as_str()
|
|
};
|
|
|
|
// Fail closed: a caller without the RemoveServiceAccount admin action
|
|
// may only delete a service account it OWNS. If the target could not be
|
|
// loaded (svc_account is None because get_service_account failed with a
|
|
// non-not-found error — e.g. a credential decode error), ownership cannot
|
|
// be verified, so deny rather than fall through to the delete. The
|
|
// previous `is_some_and(parent != user)` form skipped the guard on a
|
|
// None target, an authz bypass (backlog#806).
|
|
let target_parent = svc_account.as_ref().map(|v| v.parent_user.as_str());
|
|
if !non_admin_may_delete_service_account(user, target_parent) {
|
|
return Err(s3_error!(InvalidRequest, "service account not exist"));
|
|
}
|
|
}
|
|
|
|
iam_store.delete_service_account(&query.access_key, true).await.map_err(|e| {
|
|
debug!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = "service_account_delete_failed",
|
|
access_key = %query.access_key,
|
|
error = ?e,
|
|
"Failed to delete service account"
|
|
);
|
|
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!(
|
|
component = LOG_COMPONENT_ADMIN,
|
|
subsystem = LOG_SUBSYSTEM_SERVICE_ACCOUNT,
|
|
event = EVENT_ADMIN_SERVICE_ACCOUNT_STATE,
|
|
access_key = %query.access_key,
|
|
action = "delete",
|
|
result = "site_replication_hook_failed",
|
|
error = ?err,
|
|
"admin service account state"
|
|
);
|
|
}
|
|
|
|
let mut header = HeaderMap::new();
|
|
header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));
|
|
header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value"));
|
|
Ok(S3Response::with_headers(
|
|
(delete_service_account_success_status(req.uri.path()), Body::empty()),
|
|
header,
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use rustfs_credentials::IAM_POLICY_CLAIM_NAME_SA;
|
|
use serde_json::json;
|
|
use serde_urlencoded::from_bytes;
|
|
|
|
#[test]
|
|
fn access_key_query_supports_external_alias() {
|
|
let query: AccessKeyQuery = from_bytes(b"access-key=test-access-key").expect("parse query");
|
|
assert_eq!(query.access_key, "test-access-key");
|
|
}
|
|
|
|
#[test]
|
|
fn non_admin_delete_is_fail_closed_on_unloadable_target() {
|
|
// Regression (backlog#806): a non-admin caller may delete ONLY a service
|
|
// account it owns; a None target (get_service_account failed to load /
|
|
// decode) must DENY, not fall through to the delete.
|
|
assert!(
|
|
non_admin_may_delete_service_account("alice", Some("alice")),
|
|
"owner may delete own service account"
|
|
);
|
|
assert!(!non_admin_may_delete_service_account("alice", Some("bob")), "non-owner must be denied");
|
|
assert!(
|
|
!non_admin_may_delete_service_account("alice", None),
|
|
"unloadable target (None) must be denied (fail-closed), not bypass the owner check"
|
|
);
|
|
assert!(
|
|
!non_admin_may_delete_service_account("", None),
|
|
"empty caller + unloadable target must still deny"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn guess_user_provider_detects_builtin_accounts() {
|
|
let credentials = StoredCredentials {
|
|
access_key: "builtin-user".to_string(),
|
|
secret_key: "secret-key".to_string(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(access_key_identity::guess_user_provider(&credentials), "builtin");
|
|
}
|
|
|
|
#[test]
|
|
fn guess_user_provider_detects_ldap_accounts() {
|
|
let credentials = StoredCredentials {
|
|
access_key: "svc".to_string(),
|
|
secret_key: "secret-key".to_string(),
|
|
parent_user: "parent".to_string(),
|
|
claims: Some(HashMap::from([
|
|
(IAM_POLICY_CLAIM_NAME_SA.to_string(), json!("embedded")),
|
|
("ldap:user".to_string(), json!("uid=rustfs,ou=people,dc=example,dc=com")),
|
|
])),
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(access_key_identity::guess_user_provider(&credentials), "ldap");
|
|
}
|
|
|
|
#[test]
|
|
fn guess_user_provider_detects_openid_accounts() {
|
|
let credentials = StoredCredentials {
|
|
access_key: "sts".to_string(),
|
|
secret_key: "secret-key".to_string(),
|
|
session_token: "session-token".to_string(),
|
|
parent_user: "parent".to_string(),
|
|
claims: Some(HashMap::from([("sub".to_string(), json!("subject-123"))])),
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(access_key_identity::guess_user_provider(&credentials), "openid");
|
|
}
|
|
|
|
#[test]
|
|
fn provider_specific_info_uses_known_claims() {
|
|
let claims = HashMap::from([
|
|
("ldap:user".to_string(), json!("uid=rustfs,ou=people,dc=example,dc=com")),
|
|
("sub".to_string(), json!("subject-123")),
|
|
("name".to_string(), json!("RustFS User")),
|
|
]);
|
|
|
|
let ldap_info = access_key_identity::ldap_specific_info(Some(&claims));
|
|
let openid_info = access_key_identity::openid_specific_info(Some(&claims));
|
|
|
|
assert_eq!(ldap_info.username.as_deref(), Some("uid=rustfs,ou=people,dc=example,dc=com"));
|
|
assert_eq!(openid_info.user_id.as_deref(), Some("subject-123"));
|
|
assert_eq!(openid_info.user_id_claim.as_deref(), Some("sub"));
|
|
assert_eq!(openid_info.display_name.as_deref(), Some("RustFS User"));
|
|
assert_eq!(openid_info.display_name_claim.as_deref(), Some("name"));
|
|
}
|
|
|
|
#[test]
|
|
fn list_access_keys_query_parses_external_parameters() {
|
|
let query = parse_list_access_keys_query(Some("users=alice&users=bob&all=true&listType=svcacc-only"));
|
|
|
|
assert_eq!(query.users, vec!["alice".to_string(), "bob".to_string()]);
|
|
assert!(query.all);
|
|
assert_eq!(query.list_type, ACCESS_KEY_LIST_SVCACC_ONLY);
|
|
}
|
|
|
|
#[test]
|
|
fn list_access_keys_query_ignores_empty_users_values() {
|
|
let query = parse_list_access_keys_query(Some("users=&users=alice&users=&listType=all"));
|
|
|
|
assert_eq!(query.users, vec!["alice".to_string()]);
|
|
assert!(!query.all);
|
|
assert_eq!(query.list_type, ACCESS_KEY_LIST_ALL);
|
|
}
|
|
|
|
#[test]
|
|
fn list_access_keys_query_all_with_empty_users_does_not_conflict() {
|
|
let query = parse_list_access_keys_query(Some("users=&all=true&listType=all"));
|
|
|
|
assert!(query.users.is_empty());
|
|
assert!(query.all);
|
|
assert_eq!(query.list_type, ACCESS_KEY_LIST_ALL);
|
|
assert!(!query.all || query.users.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn list_access_keys_query_defaults_to_all_list_type() {
|
|
let query = ListAccessKeysQuery::default();
|
|
assert_eq!(query.list_type, ACCESS_KEY_LIST_ALL);
|
|
}
|
|
|
|
#[test]
|
|
fn list_service_account_cross_user_uses_list_service_accounts_action() {
|
|
let src = include_str!("service_account.rs");
|
|
let list_start = src
|
|
.find("impl Operation for ListServiceAccount")
|
|
.expect("ListServiceAccount operation should exist");
|
|
let list_block = &src[list_start..];
|
|
let list_end = list_block
|
|
.find("struct ListAccessKeysQuery")
|
|
.expect("ListAccessKeysQuery marker should exist");
|
|
let list_block = &list_block[..list_end];
|
|
|
|
assert!(
|
|
list_block.contains("query.user.as_ref().is_some_and(") && list_block.contains("v != &cred.access_key"),
|
|
"cross-user ListServiceAccount path should stay explicitly guarded"
|
|
);
|
|
assert!(
|
|
list_block.contains("ListServiceAccountsAdminAction"),
|
|
"cross-user ListServiceAccount should authorize with ListServiceAccountsAdminAction"
|
|
);
|
|
assert!(
|
|
!list_block.contains("UpdateServiceAccountAdminAction"),
|
|
"cross-user ListServiceAccount must not require UpdateServiceAccountAdminAction"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_service_account_uses_external_success_status() {
|
|
assert_eq!(
|
|
delete_service_account_success_status("/minio/admin/v3/delete-service-account"),
|
|
StatusCode::NO_CONTENT
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_service_account_keeps_rustfs_console_success_status() {
|
|
assert_eq!(
|
|
delete_service_account_success_status("/rustfs/admin/v3/delete-service-account"),
|
|
StatusCode::OK
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn expiration_for_external_admin_path_uses_sentinel() {
|
|
assert_eq!(
|
|
expiration_for_admin_path("/minio/admin/v3/list-service-accounts", None),
|
|
Some(OffsetDateTime::UNIX_EPOCH)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn expiration_for_rustfs_admin_path_preserves_none() {
|
|
assert_eq!(expiration_for_admin_path("/rustfs/admin/v3/list-service-accounts", None), None);
|
|
}
|
|
|
|
#[test]
|
|
fn map_service_account_lookup_error_reports_missing_accounts_explicitly() {
|
|
let err = map_service_account_lookup_error(
|
|
rustfs_iam::error::Error::NoSuchServiceAccount("missing".to_string()),
|
|
"get service account failed",
|
|
);
|
|
|
|
assert_eq!(*err.code(), S3ErrorCode::NoSuchResource);
|
|
assert_eq!(err.message(), Some("service account 'missing' does not exist"));
|
|
}
|
|
|
|
#[test]
|
|
fn add_service_account_maps_iam_create_errors_to_s3_errors() {
|
|
let src = include_str!("service_account.rs");
|
|
let create_start = src
|
|
.find("impl Operation for AddServiceAccount")
|
|
.expect("AddServiceAccount operation should exist");
|
|
let create_block = &src[create_start..];
|
|
let create_end = create_block
|
|
.find("if let Err(err) = site_replication_iam_change_hook")
|
|
.expect("site replication hook marker should exist");
|
|
let create_block = &create_block[..create_end];
|
|
|
|
assert!(
|
|
create_block.contains("AccessKeyAlreadyExists") && create_block.contains("iam_error_to_s3_error(e)"),
|
|
"duplicate access keys must map to client S3 errors"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn map_temp_account_lookup_error_reports_missing_access_keys_explicitly() {
|
|
let err = map_temp_account_lookup_error(
|
|
rustfs_iam::error::Error::NoSuchTempAccount("missing".to_string()),
|
|
"get temporary account failed",
|
|
);
|
|
|
|
assert_eq!(*err.code(), S3ErrorCode::NoSuchResource);
|
|
assert_eq!(err.message(), Some("temp account 'missing' does not exist"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_update_service_account_policy_keeps_explicit_empty_policy() {
|
|
let policy = parse_update_service_account_policy(Some(json!({}))).expect("empty policy should parse");
|
|
|
|
assert!(policy.is_some());
|
|
let policy = policy.unwrap();
|
|
assert!(policy.version.is_empty());
|
|
assert!(policy.statements.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_service_account_policy_reports_missing_resource() {
|
|
let err = parse_service_account_policy(&json!({
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:GetObject"]
|
|
}
|
|
]
|
|
}))
|
|
.expect_err("policy without Resource should be rejected");
|
|
|
|
assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);
|
|
assert_eq!(err.message(), Some("invalid service account policy: Resource is empty"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_service_account_policy_preserves_policy_validation_details() {
|
|
let err = parse_service_account_policy(&json!({
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:GetObject", "admin:ServerInfo"],
|
|
"Resource": ["arn:aws:s3:::bucket/*"]
|
|
}
|
|
]
|
|
}))
|
|
.expect_err("policy with mixed action families should be rejected");
|
|
|
|
assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);
|
|
assert_eq!(
|
|
err.message(),
|
|
Some("invalid service account policy: 'Action' contains mixed action families in the same statement")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn update_service_account_requires_requester_parent_match() {
|
|
let parent_owner = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
parent_user: String::new(),
|
|
..Default::default()
|
|
};
|
|
let derived_owner = StoredCredentials {
|
|
access_key: "sa-user".to_string(),
|
|
parent_user: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
let foreign_user = StoredCredentials {
|
|
access_key: "other".to_string(),
|
|
parent_user: String::new(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(is_service_account_owner_of(&parent_owner, "owner-user"));
|
|
assert!(is_service_account_owner_of(&derived_owner, "owner-user"));
|
|
assert!(!is_service_account_owner_of(&foreign_user, "owner-user"));
|
|
}
|
|
|
|
#[test]
|
|
fn merge_derived_service_account_claims_skips_only_expiration() {
|
|
let mut merged = HashMap::new();
|
|
let source = HashMap::from([
|
|
("exp".to_string(), json!(123456)),
|
|
("parent".to_string(), json!("owner-user")),
|
|
("custom".to_string(), json!("value")),
|
|
]);
|
|
|
|
merge_derived_service_account_claims(&mut merged, &source);
|
|
|
|
assert!(!merged.contains_key("exp"));
|
|
assert_eq!(merged.get("parent"), Some(&json!("owner-user")));
|
|
assert_eq!(merged.get("custom"), Some(&json!("value")));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_access_key_info_allows_same_regular_user() {
|
|
let requester = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
let target = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(can_fallback_view_access_key_info(&requester, &target));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_access_key_info_allows_parent_for_derived_credentials() {
|
|
let requester = StoredCredentials {
|
|
access_key: "sts-user".to_string(),
|
|
session_token: "session-token".to_string(),
|
|
parent_user: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
let target = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(can_fallback_view_access_key_info(&requester, &target));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_access_key_info_denies_service_account_for_parent_regular_user() {
|
|
let requester = StoredCredentials {
|
|
access_key: "svc-user".to_string(),
|
|
session_token: "session-token".to_string(),
|
|
parent_user: "owner-user".to_string(),
|
|
claims: Some(HashMap::from([("sa-policy".to_string(), json!("inherited-policy"))])),
|
|
..Default::default()
|
|
};
|
|
let target = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(!can_fallback_view_access_key_info(&requester, &target));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_access_key_info_denies_other_regular_user() {
|
|
let requester = StoredCredentials {
|
|
access_key: "alice".to_string(),
|
|
..Default::default()
|
|
};
|
|
let target = StoredCredentials {
|
|
access_key: "bob".to_string(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(!can_fallback_view_access_key_info(&requester, &target));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_access_key_info_requires_no_explicit_deny() {
|
|
let requester = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
let target = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(can_fallback_view_access_key_info_after_admin_check_failed(&requester, &target, true));
|
|
assert!(!can_fallback_view_access_key_info_after_admin_check_failed(&requester, &target, false));
|
|
}
|
|
|
|
#[test]
|
|
fn fallback_access_key_info_requires_no_explicit_deny_for_parent_owned_derived_credentials() {
|
|
let requester = StoredCredentials {
|
|
access_key: "sts-user".to_string(),
|
|
session_token: "session-token".to_string(),
|
|
parent_user: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
let target = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(can_fallback_view_access_key_info_after_admin_check_failed(&requester, &target, true));
|
|
assert!(!can_fallback_view_access_key_info_after_admin_check_failed(&requester, &target, false));
|
|
}
|
|
|
|
#[test]
|
|
fn build_info_regular_user_resp_maps_user_metadata() {
|
|
let account = StoredCredentials {
|
|
access_key: "owner-user".to_string(),
|
|
name: Some("Owner".to_string()),
|
|
description: Some("Primary user".to_string()),
|
|
..Default::default()
|
|
};
|
|
let user_info = rustfs_madmin::UserInfo {
|
|
policy_name: Some("readwrite".to_string()),
|
|
status: rustfs_madmin::AccountStatus::Enabled,
|
|
..Default::default()
|
|
};
|
|
|
|
let resp = access_key_identity::build_info_regular_user_resp(&account, &user_info);
|
|
|
|
assert_eq!(resp.parent_user, "");
|
|
assert_eq!(resp.account_status, "enabled");
|
|
assert!(!resp.implied_policy);
|
|
assert_eq!(resp.policy.as_deref(), Some("readwrite"));
|
|
assert_eq!(resp.name.as_deref(), Some("Owner"));
|
|
assert_eq!(resp.description.as_deref(), Some("Primary user"));
|
|
assert_eq!(resp.expiration, None);
|
|
}
|
|
}
|