mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 08:49:26 +00:00
feat(iam,admin): prepared IAM auth, ExistingObjectTag, admin permission checks (#2315)
Signed-off-by: GatewayJ <835269233@qq.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: GatewayJ <8352692332qq.com>
This commit is contained in:
@@ -463,18 +463,18 @@ impl Drop for RustFSTestEnvironment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utility function to execute awscurl commands
|
async fn execute_awscurl_with_service(
|
||||||
pub async fn execute_awscurl(
|
|
||||||
url: &str,
|
url: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
body: Option<&str>,
|
body: Option<&str>,
|
||||||
access_key: &str,
|
access_key: &str,
|
||||||
secret_key: &str,
|
secret_key: &str,
|
||||||
|
service: &str,
|
||||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"--fail-with-body",
|
"--fail-with-body",
|
||||||
"--service",
|
"--service",
|
||||||
"s3",
|
service,
|
||||||
"--region",
|
"--region",
|
||||||
"us-east-1",
|
"us-east-1",
|
||||||
"--access_key",
|
"--access_key",
|
||||||
@@ -490,7 +490,64 @@ pub async fn execute_awscurl(
|
|||||||
args.extend(&["-d", body_content]);
|
args.extend(&["-d", body_content]);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Executing awscurl: {} {}", method, url);
|
info!("Executing awscurl: {} {} (service={})", method, url, service);
|
||||||
|
let awscurl_path = awscurl_binary_path();
|
||||||
|
let output = Command::new(&awscurl_path).args(&args).output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
return Err(format!("awscurl failed: stderr='{stderr}', stdout='{stdout}'").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility function to execute awscurl commands (SigV4 service `s3` for admin APIs).
|
||||||
|
pub async fn execute_awscurl(
|
||||||
|
url: &str,
|
||||||
|
method: &str,
|
||||||
|
body: Option<&str>,
|
||||||
|
access_key: &str,
|
||||||
|
secret_key: &str,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
execute_awscurl_with_service(url, method, body, access_key, secret_key, "s3").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `POST` with SigV4 `--service sts` and explicit `Content-Type: application/x-www-form-urlencoded`.
|
||||||
|
///
|
||||||
|
/// RustFS `AssumeRole` is handled on `POST /` by the admin router; `is_match` requires this
|
||||||
|
/// content type so s3s routes to the custom handler instead of `Unknown operation`.
|
||||||
|
pub async fn awscurl_post_sts_form_urlencoded(
|
||||||
|
url: &str,
|
||||||
|
body: &str,
|
||||||
|
access_key: &str,
|
||||||
|
secret_key: &str,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let args = vec![
|
||||||
|
"--fail-with-body",
|
||||||
|
"--service",
|
||||||
|
"sts",
|
||||||
|
"--region",
|
||||||
|
"us-east-1",
|
||||||
|
"--access_key",
|
||||||
|
access_key,
|
||||||
|
"--secret_key",
|
||||||
|
secret_key,
|
||||||
|
"-H",
|
||||||
|
"Content-Type: application/x-www-form-urlencoded",
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
"-d",
|
||||||
|
body,
|
||||||
|
];
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Executing awscurl: POST {} (service=sts, Content-Type=application/x-www-form-urlencoded)",
|
||||||
|
url
|
||||||
|
);
|
||||||
let awscurl_path = awscurl_binary_path();
|
let awscurl_path = awscurl_binary_path();
|
||||||
let output = Command::new(&awscurl_path).args(&args).output()?;
|
let output = Command::new(&awscurl_path).args(&args).output()?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//! E2E: `s3:ExistingObjectTag` with **IAM identity policy**, **bucket policy**, and **STS AssumeRole
|
||||||
|
//! session policy** (`Policy` parameter) via `awscurl --service sts` with explicit
|
||||||
|
//! `Content-Type: application/x-www-form-urlencoded` on `POST /`.
|
||||||
|
|
||||||
|
use crate::common::{
|
||||||
|
RustFSTestEnvironment, awscurl_available, awscurl_delete, awscurl_post_sts_form_urlencoded, awscurl_put, init_logging,
|
||||||
|
};
|
||||||
|
use aws_sdk_s3::config::{Credentials, Region};
|
||||||
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
|
use aws_sdk_s3::types::{Tag, Tagging};
|
||||||
|
use aws_sdk_s3::{Client, Config};
|
||||||
|
use serial_test::serial;
|
||||||
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn user_client(env: &RustFSTestEnvironment, access_key: &str, secret_key: &str) -> Client {
|
||||||
|
let credentials = Credentials::new(access_key, secret_key, None, None, "e2e-existing-tag");
|
||||||
|
let config = Config::builder()
|
||||||
|
.credentials_provider(credentials)
|
||||||
|
.region(Region::new("us-east-1"))
|
||||||
|
.endpoint_url(&env.url)
|
||||||
|
.force_path_style(true)
|
||||||
|
.behavior_version_latest()
|
||||||
|
.build();
|
||||||
|
Client::from_conf(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sts_session_client(env: &RustFSTestEnvironment, access_key: &str, secret_key: &str, session_token: &str) -> Client {
|
||||||
|
let credentials = Credentials::new(access_key, secret_key, Some(session_token.into()), None, "e2e-sts-session");
|
||||||
|
let config = Config::builder()
|
||||||
|
.credentials_provider(credentials)
|
||||||
|
.region(Region::new("us-east-1"))
|
||||||
|
.endpoint_url(&env.url)
|
||||||
|
.force_path_style(true)
|
||||||
|
.behavior_version_latest()
|
||||||
|
.build();
|
||||||
|
Client::from_conf(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_xml_tag(xml: &str, tag: &str) -> Option<String> {
|
||||||
|
let open = format!("<{tag}>");
|
||||||
|
let close = format!("</{tag}>");
|
||||||
|
let start = xml.find(&open)? + open.len();
|
||||||
|
let end = xml[start..].find(&close)? + start;
|
||||||
|
Some(xml[start..end].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_assume_role_credentials(xml: &str) -> Result<(String, String, String), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let ak = extract_xml_tag(xml, "AccessKeyId").ok_or("missing AccessKeyId in AssumeRole response")?;
|
||||||
|
let sk = extract_xml_tag(xml, "SecretAccessKey").ok_or("missing SecretAccessKey in AssumeRole response")?;
|
||||||
|
let token = extract_xml_tag(xml, "SessionToken").ok_or("missing SessionToken in AssumeRole response")?;
|
||||||
|
Ok((ak, sk, token))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assume_role_with_session_policy(
|
||||||
|
env: &RustFSTestEnvironment,
|
||||||
|
parent_ak: &str,
|
||||||
|
parent_sk: &str,
|
||||||
|
session_policy_json: &str,
|
||||||
|
) -> Result<(String, String, String), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let policy_enc = urlencoding::encode(session_policy_json);
|
||||||
|
let body = format!("Action=AssumeRole&Version=2011-06-15&DurationSeconds=3600&Policy={}", policy_enc);
|
||||||
|
let url = format!("{}/", env.url.trim_end_matches('/'));
|
||||||
|
let xml = awscurl_post_sts_form_urlencoded(&url, &body, parent_ak, parent_sk).await?;
|
||||||
|
parse_assume_role_credentials(&xml)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn admin_create_user(
|
||||||
|
env: &RustFSTestEnvironment,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let body = serde_json::json!({ "secretKey": password, "status": "enabled" }).to_string();
|
||||||
|
let url = format!("{}/rustfs/admin/v3/add-user?accessKey={}", env.url, username);
|
||||||
|
awscurl_put(&url, &body, &env.access_key, &env.secret_key).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn admin_add_canned_policy(
|
||||||
|
env: &RustFSTestEnvironment,
|
||||||
|
policy_name: &str,
|
||||||
|
policy_json: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let url = format!("{}/rustfs/admin/v3/add-canned-policy?name={}", env.url, policy_name);
|
||||||
|
awscurl_put(&url, policy_json, &env.access_key, &env.secret_key).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn admin_attach_policy_to_user(
|
||||||
|
env: &RustFSTestEnvironment,
|
||||||
|
policy_name: &str,
|
||||||
|
username: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/rustfs/admin/v3/set-user-or-group-policy?policyName={}&userOrGroup={}&isGroup=false",
|
||||||
|
env.url, policy_name, username
|
||||||
|
);
|
||||||
|
awscurl_put(&url, "", &env.access_key, &env.secret_key).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn admin_remove_user(env: &RustFSTestEnvironment, username: &str) {
|
||||||
|
let url = format!("{}/rustfs/admin/v3/remove-user?accessKey={}", env.url, username);
|
||||||
|
let _ = awscurl_delete(&url, &env.access_key, &env.secret_key).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn admin_remove_policy(env: &RustFSTestEnvironment, policy_name: &str) {
|
||||||
|
let url = format!("{}/rustfs/admin/v3/remove-canned-policy?name={}", env.url, policy_name);
|
||||||
|
let _ = awscurl_delete(&url, &env.access_key, &env.secret_key).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_object_with_tagging_str(
|
||||||
|
client: &Client,
|
||||||
|
bucket: &str,
|
||||||
|
key: &str,
|
||||||
|
data: &[u8],
|
||||||
|
tagging: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(ByteStream::from(data.to_vec()))
|
||||||
|
.tagging(tagging)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_object_tag_kv(
|
||||||
|
client: &Client,
|
||||||
|
bucket: &str,
|
||||||
|
key: &str,
|
||||||
|
tag_key: &str,
|
||||||
|
tag_value: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let tag = Tag::builder()
|
||||||
|
.key(tag_key)
|
||||||
|
.value(tag_value)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Tag build: {e}"))?;
|
||||||
|
let tagging = Tagging::builder()
|
||||||
|
.tag_set(tag)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Tagging build: {e}"))?;
|
||||||
|
client
|
||||||
|
.put_object_tagging()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.tagging(tagging)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cleanup_bucket_and_object(admin: &Client, bucket: &str, key: &str) {
|
||||||
|
let _ = admin.delete_object().bucket(bucket).key(key).send().await;
|
||||||
|
let _ = admin.delete_bucket().bucket(bucket).send().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IAM identity policy: GetObject allowed only when `s3:ExistingObjectTag/security` == `public`.
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_e2e_iam_policy_existing_object_tag_get_object() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
if !awscurl_available() {
|
||||||
|
info!("Skipping test_e2e_iam_policy_existing_object_tag_get_object: awscurl not available");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix = Uuid::new_v4();
|
||||||
|
let user = format!("e2eiamtag-{suffix}");
|
||||||
|
let user_secret = "longSecretKeyForTest123!";
|
||||||
|
let policy_name = format!("e2e-iam-tag-pol-{suffix}");
|
||||||
|
let bucket = format!("e2e-iam-tag-bkt-{suffix}");
|
||||||
|
let key = "tagged-object.txt";
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
env.start_rustfs_server(vec![]).await?;
|
||||||
|
|
||||||
|
let admin = env.create_s3_client();
|
||||||
|
admin_create_user(&env, &user, user_secret).await?;
|
||||||
|
|
||||||
|
let policy_doc = serde_json::json!({
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["s3:GetObject"],
|
||||||
|
"Resource": [format!("arn:aws:s3:::{}/*", bucket)],
|
||||||
|
"Condition": { "StringEquals": { "s3:ExistingObjectTag/security": "public" } }
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
admin_add_canned_policy(&env, &policy_name, &policy_doc).await?;
|
||||||
|
admin_attach_policy_to_user(&env, &policy_name, &user).await?;
|
||||||
|
|
||||||
|
admin.create_bucket().bucket(&bucket).send().await?;
|
||||||
|
put_object_with_tagging_str(&admin, &bucket, key, b"hello-iam-tag", "security=public").await?;
|
||||||
|
|
||||||
|
let uclient = user_client(&env, &user, user_secret);
|
||||||
|
let out = uclient.get_object().bucket(&bucket).key(key).send().await?;
|
||||||
|
let _ = out.body.collect().await?;
|
||||||
|
|
||||||
|
put_object_tag_kv(&admin, &bucket, key, "security", "private").await?;
|
||||||
|
let denied = uclient.get_object().bucket(&bucket).key(key).send().await;
|
||||||
|
assert!(
|
||||||
|
denied.is_err(),
|
||||||
|
"GetObject must be denied when ExistingObjectTag no longer matches IAM policy"
|
||||||
|
);
|
||||||
|
|
||||||
|
cleanup_bucket_and_object(&admin, &bucket, key).await;
|
||||||
|
admin_remove_user(&env, &user).await;
|
||||||
|
admin_remove_policy(&env, &policy_name).await;
|
||||||
|
|
||||||
|
info!("test_e2e_iam_policy_existing_object_tag_get_object passed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bucket policy: same `ExistingObjectTag` condition; user has no canned IAM policy attached.
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_e2e_bucket_policy_existing_object_tag_get_object() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
if !awscurl_available() {
|
||||||
|
info!("Skipping test_e2e_bucket_policy_existing_object_tag_get_object: awscurl not available");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix = Uuid::new_v4();
|
||||||
|
let user = format!("e2ebptag-{suffix}");
|
||||||
|
let user_secret = "longSecretKeyForTest456!";
|
||||||
|
let bucket = format!("e2e-bp-tag-bkt-{suffix}");
|
||||||
|
let key = "obj.txt";
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
env.start_rustfs_server(vec![]).await?;
|
||||||
|
|
||||||
|
let admin = env.create_s3_client();
|
||||||
|
admin_create_user(&env, &user, user_secret).await?;
|
||||||
|
|
||||||
|
admin.create_bucket().bucket(&bucket).send().await?;
|
||||||
|
|
||||||
|
let deny_before = user_client(&env, &user, user_secret)
|
||||||
|
.get_object()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert!(deny_before.is_err(), "without bucket policy, user must be denied");
|
||||||
|
|
||||||
|
let bp = serde_json::json!({
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": { "AWS": [user.clone()] },
|
||||||
|
"Action": ["s3:GetObject"],
|
||||||
|
"Resource": [format!("arn:aws:s3:::{}/*", bucket)],
|
||||||
|
"Condition": { "StringEquals": { "s3:ExistingObjectTag/security": "public" } }
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
admin.put_bucket_policy().bucket(&bucket).policy(&bp).send().await?;
|
||||||
|
put_object_with_tagging_str(&admin, &bucket, key, b"data", "security=public").await?;
|
||||||
|
|
||||||
|
let uclient = user_client(&env, &user, user_secret);
|
||||||
|
let ok = uclient.get_object().bucket(&bucket).key(key).send().await?;
|
||||||
|
let _ = ok.body.collect().await?;
|
||||||
|
|
||||||
|
put_object_tag_kv(&admin, &bucket, key, "security", "private").await?;
|
||||||
|
let denied = uclient.get_object().bucket(&bucket).key(key).send().await;
|
||||||
|
assert!(denied.is_err(), "GetObject must fail when tag no longer satisfies bucket policy");
|
||||||
|
|
||||||
|
cleanup_bucket_and_object(&admin, &bucket, key).await;
|
||||||
|
admin_remove_user(&env, &user).await;
|
||||||
|
|
||||||
|
info!("test_e2e_bucket_policy_existing_object_tag_get_object passed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// STS `AssumeRole` with inline `Policy` (session policy): GetObject only when `ExistingObjectTag/security` is `public`.
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_e2e_sts_assume_role_session_policy_existing_object_tag() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
if !awscurl_available() {
|
||||||
|
info!("Skipping test_e2e_sts_assume_role_session_policy_existing_object_tag: awscurl not available");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix = Uuid::new_v4();
|
||||||
|
let parent = format!("e2e-sts-par-{suffix}");
|
||||||
|
let parent_secret = "longSecretKeyForParentSts99!";
|
||||||
|
let policy_readwrite = format!("e2e-sts-rw-{suffix}");
|
||||||
|
let bucket = format!("e2e-sts-tag-bkt-{suffix}");
|
||||||
|
let key = "sts-obj.txt";
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
env.start_rustfs_server(vec![]).await?;
|
||||||
|
|
||||||
|
let admin = env.create_s3_client();
|
||||||
|
admin_create_user(&env, &parent, parent_secret).await?;
|
||||||
|
|
||||||
|
let rw = serde_json::to_string(&serde_json::json!({
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["s3:*"],
|
||||||
|
"Resource": ["arn:aws:s3:::*"]
|
||||||
|
}]
|
||||||
|
}))?;
|
||||||
|
admin_add_canned_policy(&env, &policy_readwrite, &rw).await?;
|
||||||
|
admin_attach_policy_to_user(&env, &policy_readwrite, &parent).await?;
|
||||||
|
|
||||||
|
let parent_client = user_client(&env, &parent, parent_secret);
|
||||||
|
parent_client.create_bucket().bucket(&bucket).send().await?;
|
||||||
|
put_object_with_tagging_str(&parent_client, &bucket, key, b"sts-e2e-data", "security=public").await?;
|
||||||
|
|
||||||
|
let session_policy = serde_json::json!({
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["s3:GetObject"],
|
||||||
|
"Resource": [format!("arn:aws:s3:::{}/*", bucket)],
|
||||||
|
"Condition": { "StringEquals": { "s3:ExistingObjectTag/security": "public" } }
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let (ak, sk, token) = assume_role_with_session_policy(&env, &parent, parent_secret, &session_policy).await?;
|
||||||
|
|
||||||
|
let session_client = sts_session_client(&env, &ak, &sk, &token);
|
||||||
|
let ok = session_client.get_object().bucket(&bucket).key(key).send().await?;
|
||||||
|
let _ = ok.body.collect().await?;
|
||||||
|
|
||||||
|
put_object_tag_kv(&parent_client, &bucket, key, "security", "private").await?;
|
||||||
|
let denied = session_client.get_object().bucket(&bucket).key(key).send().await;
|
||||||
|
assert!(
|
||||||
|
denied.is_err(),
|
||||||
|
"session policy must deny GetObject when ExistingObjectTag no longer matches"
|
||||||
|
);
|
||||||
|
|
||||||
|
cleanup_bucket_and_object(&admin, &bucket, key).await;
|
||||||
|
admin_remove_user(&env, &parent).await;
|
||||||
|
admin_remove_policy(&env, &policy_readwrite).await;
|
||||||
|
|
||||||
|
info!("test_e2e_sts_assume_role_session_policy_existing_object_tag passed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -40,6 +40,10 @@ mod quota_test;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod bucket_policy_check_test;
|
mod bucket_policy_check_test;
|
||||||
|
|
||||||
|
/// IAM / bucket / STS session policy with `s3:ExistingObjectTag` conditions (E2E).
|
||||||
|
#[cfg(test)]
|
||||||
|
mod existing_object_tag_policy_test;
|
||||||
|
|
||||||
// Regression tests for Issue #2036: anonymous access with PublicAccessBlock
|
// Regression tests for Issue #2036: anonymous access with PublicAccessBlock
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod anonymous_access_test;
|
mod anonymous_access_test;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
//! Tests for AWS IAM policy variables with single-value, multi-value, and nested scenarios
|
//! Tests for AWS IAM policy variables with single-value, multi-value, and nested scenarios
|
||||||
|
|
||||||
use crate::common::{awscurl_put, init_logging};
|
use crate::common::{awscurl_delete, awscurl_put, init_logging};
|
||||||
use crate::policy::test_env::PolicyTestEnvironment;
|
use crate::policy::test_env::PolicyTestEnvironment;
|
||||||
use aws_sdk_s3::primitives::ByteStream;
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
@@ -113,11 +113,11 @@ async fn cleanup_user_and_policy(env: &PolicyTestEnvironment, username: &str, po
|
|||||||
|
|
||||||
// Remove user
|
// Remove user
|
||||||
let remove_user_url = format!("{}/rustfs/admin/v3/remove-user?accessKey={}", env.url, username);
|
let remove_user_url = format!("{}/rustfs/admin/v3/remove-user?accessKey={}", env.url, username);
|
||||||
let _ = awscurl_put(&remove_user_url, "", &env.access_key, &env.secret_key).await;
|
let _ = awscurl_delete(&remove_user_url, &env.access_key, &env.secret_key).await;
|
||||||
|
|
||||||
// Remove policy
|
// Remove policy
|
||||||
let remove_policy_url = format!("{}/rustfs/admin/v3/remove-canned-policy?name={}", env.url, policy_name);
|
let remove_policy_url = format!("{}/rustfs/admin/v3/remove-canned-policy?name={}", env.url, policy_name);
|
||||||
let _ = awscurl_put(&remove_policy_url, "", &env.access_key, &env.secret_key).await;
|
let _ = awscurl_delete(&remove_policy_url, &env.access_key, &env.secret_key).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test AWS policy variables with single-value scenarios
|
/// Test AWS policy variables with single-value scenarios
|
||||||
|
|||||||
+621
-160
@@ -35,7 +35,7 @@ use rustfs_policy::auth::{
|
|||||||
};
|
};
|
||||||
use rustfs_policy::policy::Args;
|
use rustfs_policy::policy::Args;
|
||||||
use rustfs_policy::policy::opa;
|
use rustfs_policy::policy::opa;
|
||||||
use rustfs_policy::policy::{Policy, PolicyDoc, iam_policy_claim_name_sa};
|
use rustfs_policy::policy::{Policy, PolicyDoc, iam_policy_claim_name_sa, policy_needs_existing_object_tag_for_args};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -65,6 +65,78 @@ pub struct IamSys<T> {
|
|||||||
roles_map: HashMap<ARN, String>,
|
roles_map: HashMap<ARN, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum PreparedSessionPolicy {
|
||||||
|
None,
|
||||||
|
DenyAll,
|
||||||
|
Policy(Policy),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum PreparedServicePolicyMode {
|
||||||
|
Inherited,
|
||||||
|
SessionBound,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum PreparedIamMode {
|
||||||
|
Opa,
|
||||||
|
Owner,
|
||||||
|
Deny,
|
||||||
|
Regular {
|
||||||
|
combined_policy: Policy,
|
||||||
|
},
|
||||||
|
Sts {
|
||||||
|
is_owner: bool,
|
||||||
|
combined_policy: Policy,
|
||||||
|
session_policy: PreparedSessionPolicy,
|
||||||
|
},
|
||||||
|
ServiceAccount {
|
||||||
|
is_owner: bool,
|
||||||
|
parent_user: String,
|
||||||
|
combined_policy: Policy,
|
||||||
|
mode: PreparedServicePolicyMode,
|
||||||
|
session_policy: PreparedSessionPolicy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PreparedIamAuth {
|
||||||
|
pub needs_existing_object_tag: bool,
|
||||||
|
mode: PreparedIamMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreparedIamAuth {
|
||||||
|
/// Evaluate whether the already-prepared IAM context needs ExistingObjectTag
|
||||||
|
/// conditions for the provided request args.
|
||||||
|
pub async fn needs_existing_object_tag_for_args(&self, args: &Args<'_>) -> bool {
|
||||||
|
match &self.mode {
|
||||||
|
PreparedIamMode::Opa | PreparedIamMode::Owner | PreparedIamMode::Deny => false,
|
||||||
|
PreparedIamMode::Regular { combined_policy } => {
|
||||||
|
policy_needs_existing_object_tag_for_args(combined_policy, args).await
|
||||||
|
}
|
||||||
|
PreparedIamMode::Sts {
|
||||||
|
combined_policy,
|
||||||
|
session_policy,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
policy_needs_existing_object_tag_for_args(combined_policy, args).await
|
||||||
|
|| prepared_session_policy_needs_existing_object_tag_for_args(session_policy, args).await
|
||||||
|
}
|
||||||
|
PreparedIamMode::ServiceAccount {
|
||||||
|
combined_policy,
|
||||||
|
mode,
|
||||||
|
session_policy,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
policy_needs_existing_object_tag_for_args(combined_policy, args).await
|
||||||
|
|| matches!(mode, PreparedServicePolicyMode::SessionBound)
|
||||||
|
&& prepared_session_policy_needs_existing_object_tag_for_args(session_policy, args).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Store> IamSys<T> {
|
impl<T: Store> IamSys<T> {
|
||||||
/// Create a new IamSys instance with the given IamCache store
|
/// Create a new IamSys instance with the given IamCache store
|
||||||
///
|
///
|
||||||
@@ -740,14 +812,150 @@ impl<T: Store> IamSys<T> {
|
|||||||
self.store.policy_db_get(name, groups).await
|
self.store.policy_db_get(name, groups).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_allowed_sts(&self, args: &Args<'_>, parent_user: &str) -> bool {
|
fn is_safe_claim_policy_name(policy: &str) -> bool {
|
||||||
|
!policy.is_empty() && policy.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compatibility wrapper for service-account authorization entry points.
|
||||||
|
/// The canonical evaluation path is `prepare_service_account_auth + eval_prepared`.
|
||||||
|
pub async fn is_allowed_service_account(&self, args: &Args<'_>, parent_user: &str) -> bool {
|
||||||
|
let prepared = self.prepare_service_account_auth(args, parent_user).await;
|
||||||
|
self.eval_prepared(&prepared, args).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_combined_policy(&self, policies: &[String]) -> Policy {
|
||||||
|
self.store.merge_policies(&policies.join(",")).await.1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare IAM authorization context once so callers can:
|
||||||
|
/// 1) know whether policy evaluation may need `s3:ExistingObjectTag`, and
|
||||||
|
/// 2) evaluate with final conditions without re-merging identity policies.
|
||||||
|
pub async fn prepare_auth(&self, args: &Args<'_>) -> PreparedIamAuth {
|
||||||
|
if args.is_owner {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Owner,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if Self::get_policy_plugin_client().await.is_some() {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Opa,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok((is_temp, parent_user)) = self.is_temp_user(args.account).await else {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if is_temp {
|
||||||
|
return self.prepare_sts_auth(args, &parent_user).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok((is_svc, parent_user)) = self.is_service_account(args.account).await else {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if is_svc {
|
||||||
|
return self.prepare_service_account_auth(args, &parent_user).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prepare_regular_auth(args).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn eval_prepared(&self, prepared: &PreparedIamAuth, args: &Args<'_>) -> bool {
|
||||||
|
match &prepared.mode {
|
||||||
|
PreparedIamMode::Opa => {
|
||||||
|
let Some(opa_enable) = Self::get_policy_plugin_client().await else {
|
||||||
|
tracing::warn!("eval_prepared: OPA mode requested but plugin is unavailable");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
opa_enable.is_allowed(args).await
|
||||||
|
}
|
||||||
|
PreparedIamMode::Owner => true,
|
||||||
|
PreparedIamMode::Deny => false,
|
||||||
|
PreparedIamMode::Regular { combined_policy } => combined_policy.is_allowed(args).await,
|
||||||
|
PreparedIamMode::Sts {
|
||||||
|
is_owner,
|
||||||
|
combined_policy,
|
||||||
|
session_policy,
|
||||||
|
} => {
|
||||||
|
let session_ok = evaluate_prepared_session_policy(session_policy, args).await;
|
||||||
|
if let Some(ok) = session_ok {
|
||||||
|
return ok && (*is_owner || combined_policy.is_allowed(args).await);
|
||||||
|
}
|
||||||
|
*is_owner || combined_policy.is_allowed(args).await
|
||||||
|
}
|
||||||
|
PreparedIamMode::ServiceAccount {
|
||||||
|
is_owner,
|
||||||
|
parent_user,
|
||||||
|
combined_policy,
|
||||||
|
mode,
|
||||||
|
session_policy,
|
||||||
|
} => {
|
||||||
|
let mut parent_args = args.clone();
|
||||||
|
parent_args.account = parent_user;
|
||||||
|
|
||||||
|
let parent_allowed = *is_owner || combined_policy.is_allowed(&parent_args).await;
|
||||||
|
match mode {
|
||||||
|
PreparedServicePolicyMode::Inherited => parent_allowed,
|
||||||
|
PreparedServicePolicyMode::SessionBound => {
|
||||||
|
let session_ok = evaluate_prepared_session_policy(session_policy, args).await;
|
||||||
|
if let Some(ok) = session_ok {
|
||||||
|
return ok && parent_allowed;
|
||||||
|
}
|
||||||
|
parent_allowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare_regular_auth(&self, args: &Args<'_>) -> PreparedIamAuth {
|
||||||
|
let Ok(policies) = self.policy_db_get(args.account, args.groups).await else {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if policies.is_empty() {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let combined_policy = self.get_combined_policy(&policies).await;
|
||||||
|
PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: policy_needs_existing_object_tag_for_args(&combined_policy, args).await,
|
||||||
|
mode: PreparedIamMode::Regular { combined_policy },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn prepare_sts_auth(&self, args: &Args<'_>, parent_user: &str) -> PreparedIamAuth {
|
||||||
let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user);
|
let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user);
|
||||||
let role_arn = args.get_role_arn();
|
let role_arn = args.get_role_arn();
|
||||||
|
|
||||||
let (effective_groups, groups_source, policies) = if is_owner {
|
let (effective_groups, groups_source, policies) = if is_owner {
|
||||||
(None, "owner", Vec::new())
|
(None, "owner", Vec::new())
|
||||||
} else if let Some(arn_str) = role_arn {
|
} else if let Some(arn_str) = role_arn {
|
||||||
let Ok(arn) = ARN::parse(arn_str) else { return false };
|
let Ok(arn) = ARN::parse(arn_str) else {
|
||||||
|
tracing::warn!(
|
||||||
|
parent_user = %parent_user,
|
||||||
|
role_arn = %arn_str,
|
||||||
|
"prepare_sts_auth: invalid role ARN in STS claims"
|
||||||
|
);
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
|
};
|
||||||
let p = MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice();
|
let p = MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice();
|
||||||
(None, "role", p)
|
(None, "role", p)
|
||||||
} else {
|
} else {
|
||||||
@@ -758,7 +966,7 @@ impl<T: Store> IamSys<T> {
|
|||||||
None => {
|
None => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
parent_user = %parent_user,
|
parent_user = %parent_user,
|
||||||
"is_allowed_sts: groups fallback failed — parent user not found; policy evaluation will use no groups"
|
"prepare_sts_auth: groups fallback failed, parent user not found"
|
||||||
);
|
);
|
||||||
(None, "parent_user_credentials")
|
(None, "parent_user_credentials")
|
||||||
}
|
}
|
||||||
@@ -768,9 +976,10 @@ impl<T: Store> IamSys<T> {
|
|||||||
(effective_groups, groups_source, p)
|
(effective_groups, groups_source, p)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut combined_policy = Policy::default();
|
||||||
|
|
||||||
if !is_owner && policies.is_empty() {
|
if !is_owner && policies.is_empty() {
|
||||||
// For OIDC/STS users, policies may be specified in JWT claims rather than IAM DB.
|
// For OIDC/STS users, policies may be specified in JWT claims rather than IAM DB.
|
||||||
// Resolve claim-based policy names against built-in default policies.
|
|
||||||
if let Some(claim_policies) = args.claims.get("policy").and_then(|v| v.as_str()) {
|
if let Some(claim_policies) = args.claims.get("policy").and_then(|v| v.as_str()) {
|
||||||
use rustfs_policy::policy::default::DEFAULT_POLICIES;
|
use rustfs_policy::policy::default::DEFAULT_POLICIES;
|
||||||
let mut resolved = Vec::new();
|
let mut resolved = Vec::new();
|
||||||
@@ -786,162 +995,161 @@ impl<T: Store> IamSys<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !resolved.is_empty() {
|
if !resolved.is_empty() {
|
||||||
let combined = Policy::merge_policies(resolved);
|
combined_policy = Policy::merge_policies(resolved);
|
||||||
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy(args);
|
} else if args.deny_only {
|
||||||
if has_session_policy {
|
combined_policy = Policy::default();
|
||||||
return is_allowed_sp && combined.is_allowed(args).await;
|
} else {
|
||||||
}
|
return PreparedIamAuth {
|
||||||
return combined.is_allowed(args).await;
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
} else if args.deny_only {
|
||||||
|
combined_policy = Policy::default();
|
||||||
|
} else {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
} else if !is_owner {
|
||||||
if args.deny_only {
|
let (a, c) = self.store.merge_policies(&policies.join(",")).await;
|
||||||
let combined_policy = Policy::default();
|
if a.is_empty() {
|
||||||
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy(args);
|
if args.deny_only {
|
||||||
if has_session_policy {
|
combined_policy = Policy::default();
|
||||||
return is_allowed_sp && combined_policy.is_allowed(args).await;
|
} else {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return combined_policy.is_allowed(args).await;
|
} else {
|
||||||
|
combined_policy = c;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let combined_policy = {
|
let session_policy = prepare_session_policy(args, false);
|
||||||
if is_owner {
|
|
||||||
Policy::default()
|
|
||||||
} else {
|
|
||||||
let (a, c) = self.store.merge_policies(&policies.join(",")).await;
|
|
||||||
if a.is_empty() {
|
|
||||||
if args.deny_only {
|
|
||||||
Policy::default()
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy(args);
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"is_allowed_sts: action={:?}, has_session_policy={}, is_allowed_sp={}, is_owner={}, parent_user={}, groups_source={}, effective_groups={:?}",
|
"prepare_sts_auth: action={:?}, is_owner={}, parent_user={}, groups_source={}, effective_groups={:?}",
|
||||||
args.action,
|
args.action,
|
||||||
has_session_policy,
|
|
||||||
is_allowed_sp,
|
|
||||||
is_owner,
|
is_owner,
|
||||||
parent_user,
|
parent_user,
|
||||||
groups_source,
|
groups_source,
|
||||||
effective_groups
|
effective_groups
|
||||||
);
|
);
|
||||||
if has_session_policy {
|
PreparedIamAuth {
|
||||||
return is_allowed_sp && (is_owner || combined_policy.is_allowed(args).await);
|
needs_existing_object_tag: policy_needs_existing_object_tag_for_args(&combined_policy, args).await
|
||||||
|
|| prepared_session_policy_needs_existing_object_tag_for_args(&session_policy, args).await,
|
||||||
|
mode: PreparedIamMode::Sts {
|
||||||
|
is_owner,
|
||||||
|
combined_policy,
|
||||||
|
session_policy,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
is_owner || combined_policy.is_allowed(args).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_safe_claim_policy_name(policy: &str) -> bool {
|
async fn prepare_service_account_auth(&self, args: &Args<'_>, parent_user: &str) -> PreparedIamAuth {
|
||||||
!policy.is_empty() && policy.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn is_allowed_service_account(&self, args: &Args<'_>, parent_user: &str) -> bool {
|
|
||||||
let Some(p) = args.claims.get("parent") else {
|
let Some(p) = args.claims.get("parent") else {
|
||||||
return false;
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if p.as_str() != Some(parent_user) {
|
if p.as_str() != Some(parent_user) {
|
||||||
return false;
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user);
|
let is_owner = matches!(get_global_action_cred(), Some(cred) if cred.access_key == parent_user);
|
||||||
|
|
||||||
let role_arn = args.get_role_arn();
|
let role_arn = args.get_role_arn();
|
||||||
|
|
||||||
let svc_policies = {
|
let svc_policies = if is_owner {
|
||||||
if is_owner {
|
Vec::new()
|
||||||
Vec::new()
|
} else if role_arn.is_some() {
|
||||||
} else if role_arn.is_some() {
|
let Ok(arn) = ARN::parse(role_arn.unwrap_or_default()) else {
|
||||||
let Ok(arn) = ARN::parse(role_arn.unwrap_or_default()) else { return false };
|
tracing::warn!(
|
||||||
MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice()
|
parent_user = %parent_user,
|
||||||
} else {
|
role_arn = ?role_arn,
|
||||||
let Ok(p) = self.policy_db_get(parent_user, args.groups).await else { return false };
|
"prepare_service_account_auth: invalid role ARN in service account claims"
|
||||||
p
|
);
|
||||||
}
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice()
|
||||||
|
} else {
|
||||||
|
let Ok(policies) = self.policy_db_get(parent_user, args.groups).await else {
|
||||||
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
policies
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_owner && svc_policies.is_empty() {
|
if !is_owner && svc_policies.is_empty() {
|
||||||
return false;
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let combined_policy = {
|
let combined_policy = if is_owner {
|
||||||
if is_owner {
|
Policy::default()
|
||||||
Policy::default()
|
} else {
|
||||||
} else {
|
let (a, c) = self.store.merge_policies(&svc_policies.join(",")).await;
|
||||||
let (a, c) = self.store.merge_policies(&svc_policies.join(",")).await;
|
if a.is_empty() {
|
||||||
if a.is_empty() {
|
return PreparedIamAuth {
|
||||||
return false;
|
needs_existing_object_tag: false,
|
||||||
}
|
mode: PreparedIamMode::Deny,
|
||||||
c
|
};
|
||||||
}
|
}
|
||||||
|
c
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut parent_args = args.clone();
|
|
||||||
parent_args.account = parent_user;
|
|
||||||
|
|
||||||
let Some(sa) = args.claims.get(&iam_policy_claim_name_sa()) else {
|
let Some(sa) = args.claims.get(&iam_policy_claim_name_sa()) else {
|
||||||
return false;
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(sa_str) = sa.as_str() else {
|
let Some(sa_str) = sa.as_str() else {
|
||||||
return false;
|
return PreparedIamAuth {
|
||||||
|
needs_existing_object_tag: false,
|
||||||
|
mode: PreparedIamMode::Deny,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if sa_str == INHERITED_POLICY_TYPE {
|
let mode = if sa_str == INHERITED_POLICY_TYPE {
|
||||||
return is_owner || combined_policy.is_allowed(&parent_args).await;
|
PreparedServicePolicyMode::Inherited
|
||||||
|
} else {
|
||||||
|
PreparedServicePolicyMode::SessionBound
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_policy = prepare_session_policy(args, true);
|
||||||
|
let needs_existing_object_tag = policy_needs_existing_object_tag_for_args(&combined_policy, args).await
|
||||||
|
|| matches!(mode, PreparedServicePolicyMode::SessionBound)
|
||||||
|
&& prepared_session_policy_needs_existing_object_tag_for_args(&session_policy, args).await;
|
||||||
|
|
||||||
|
PreparedIamAuth {
|
||||||
|
needs_existing_object_tag,
|
||||||
|
mode: PreparedIamMode::ServiceAccount {
|
||||||
|
is_owner,
|
||||||
|
parent_user: parent_user.to_string(),
|
||||||
|
combined_policy,
|
||||||
|
mode,
|
||||||
|
session_policy,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy_for_service_account(args);
|
|
||||||
if has_session_policy {
|
|
||||||
return is_allowed_sp && (is_owner || combined_policy.is_allowed(&parent_args).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
is_owner || combined_policy.is_allowed(&parent_args).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_combined_policy(&self, policies: &[String]) -> Policy {
|
|
||||||
self.store.merge_policies(&policies.join(",")).await.1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_allowed(&self, args: &Args<'_>) -> bool {
|
pub async fn is_allowed(&self, args: &Args<'_>) -> bool {
|
||||||
if args.is_owner {
|
let prepared = self.prepare_auth(args).await;
|
||||||
return true;
|
self.eval_prepared(&prepared, args).await
|
||||||
}
|
|
||||||
|
|
||||||
let opa_enable = Self::get_policy_plugin_client().await;
|
|
||||||
if let Some(opa_enable) = opa_enable {
|
|
||||||
return opa_enable.is_allowed(args).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok((is_temp, parent_user)) = self.is_temp_user(args.account).await else { return false };
|
|
||||||
|
|
||||||
if is_temp {
|
|
||||||
return self.is_allowed_sts(args, &parent_user).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok((is_svc, parent_user)) = self.is_service_account(args.account).await else { return false };
|
|
||||||
|
|
||||||
if is_svc {
|
|
||||||
return self.is_allowed_service_account(args, &parent_user).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(policies) = self.policy_db_get(args.account, args.groups).await else { return false };
|
|
||||||
|
|
||||||
if policies.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.get_combined_policy(&policies).await.is_allowed(args).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the underlying store is ready
|
/// Check if the underlying store is ready
|
||||||
@@ -950,55 +1158,56 @@ impl<T: Store> IamSys<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_allowed_by_session_policy(args: &Args<'_>) -> (bool, bool) {
|
async fn prepared_session_policy_needs_existing_object_tag_for_args(policy: &PreparedSessionPolicy, args: &Args<'_>) -> bool {
|
||||||
let Some(policy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else {
|
match policy {
|
||||||
return (false, false);
|
PreparedSessionPolicy::Policy(p) => policy_needs_existing_object_tag_for_args(p, args).await,
|
||||||
};
|
PreparedSessionPolicy::None | PreparedSessionPolicy::DenyAll => false,
|
||||||
|
|
||||||
let has_session_policy = true;
|
|
||||||
|
|
||||||
let Some(policy_str) = policy.as_str() else {
|
|
||||||
return (has_session_policy, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(sub_policy) = Policy::parse_config(policy_str.as_bytes()) else {
|
|
||||||
return (has_session_policy, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if sub_policy.version.is_empty() {
|
|
||||||
return (has_session_policy, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut session_policy_args = args.clone();
|
|
||||||
session_policy_args.is_owner = false;
|
|
||||||
|
|
||||||
(has_session_policy, pollster::block_on(sub_policy.is_allowed(&session_policy_args)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_allowed_by_session_policy_for_service_account(args: &Args<'_>) -> (bool, bool) {
|
fn prepare_session_policy(args: &Args<'_>, empty_is_none: bool) -> PreparedSessionPolicy {
|
||||||
let Some(policy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else {
|
let Some(policy_str) = extract_session_policy_text(args.claims) else {
|
||||||
return (false, false);
|
return PreparedSessionPolicy::None;
|
||||||
};
|
|
||||||
|
|
||||||
let mut has_session_policy = true;
|
|
||||||
|
|
||||||
let Some(policy_str) = policy.as_str() else {
|
|
||||||
return (has_session_policy, false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(sub_policy) = Policy::parse_config(policy_str.as_bytes()) else {
|
let Ok(sub_policy) = Policy::parse_config(policy_str.as_bytes()) else {
|
||||||
return (has_session_policy, false);
|
return PreparedSessionPolicy::DenyAll;
|
||||||
};
|
};
|
||||||
|
|
||||||
if sub_policy.version.is_empty() && sub_policy.statements.is_empty() && sub_policy.id.is_empty() {
|
if empty_is_none {
|
||||||
has_session_policy = false;
|
if sub_policy.version.is_empty() && sub_policy.statements.is_empty() && sub_policy.id.is_empty() {
|
||||||
return (has_session_policy, false);
|
return PreparedSessionPolicy::None;
|
||||||
|
}
|
||||||
|
return PreparedSessionPolicy::Policy(sub_policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut session_policy_args = args.clone();
|
if sub_policy.version.is_empty() {
|
||||||
session_policy_args.is_owner = false;
|
return PreparedSessionPolicy::DenyAll;
|
||||||
|
}
|
||||||
|
|
||||||
(has_session_policy, pollster::block_on(sub_policy.is_allowed(&session_policy_args)))
|
PreparedSessionPolicy::Policy(sub_policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_session_policy_text(claims: &HashMap<String, Value>) -> Option<String> {
|
||||||
|
if let Some(policy_str) = claims.get(SESSION_POLICY_NAME_EXTRACTED).and_then(|v| v.as_str()) {
|
||||||
|
return Some(policy_str.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let encoded = claims.get(SESSION_POLICY_NAME).and_then(|v| v.as_str())?;
|
||||||
|
let bytes = base64_simd::URL_SAFE_NO_PAD.decode_to_vec(encoded.as_bytes()).ok()?;
|
||||||
|
String::from_utf8(bytes).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn evaluate_prepared_session_policy(policy: &PreparedSessionPolicy, args: &Args<'_>) -> Option<bool> {
|
||||||
|
match policy {
|
||||||
|
PreparedSessionPolicy::None => None,
|
||||||
|
PreparedSessionPolicy::DenyAll => Some(false),
|
||||||
|
PreparedSessionPolicy::Policy(p) => {
|
||||||
|
let mut session_policy_args = args.clone();
|
||||||
|
session_policy_args.is_owner = false;
|
||||||
|
Some(p.is_allowed(&session_policy_args).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -1050,6 +1259,7 @@ mod tests {
|
|||||||
use rustfs_policy::auth::UserIdentity;
|
use rustfs_policy::auth::UserIdentity;
|
||||||
use rustfs_policy::policy::Args;
|
use rustfs_policy::policy::Args;
|
||||||
use rustfs_policy::policy::action::{Action, AdminAction, S3Action};
|
use rustfs_policy::policy::action::{Action, AdminAction, S3Action};
|
||||||
|
use rustfs_policy::policy::policy_uses_existing_object_tag_conditions;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@@ -1301,7 +1511,8 @@ mod tests {
|
|||||||
deny_only: false,
|
deny_only: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let allowed = iam_sys.is_allowed_sts(&args, parent_user).await;
|
let prepared = iam_sys.prepare_sts_auth(&args, parent_user).await;
|
||||||
|
let allowed = iam_sys.eval_prepared(&prepared, &args).await;
|
||||||
assert!(
|
assert!(
|
||||||
allowed,
|
allowed,
|
||||||
"STS temp credentials with no groups in args should still be allowed via parent user's group policy (readwrite)"
|
"STS temp credentials with no groups in args should still be allowed via parent user's group policy (readwrite)"
|
||||||
@@ -1342,7 +1553,8 @@ mod tests {
|
|||||||
deny_only: true,
|
deny_only: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let allowed = iam_sys.is_allowed_sts(&args, parent_user).await;
|
let prepared = iam_sys.prepare_sts_auth(&args, parent_user).await;
|
||||||
|
let allowed = iam_sys.eval_prepared(&prepared, &args).await;
|
||||||
assert!(
|
assert!(
|
||||||
!allowed,
|
!allowed,
|
||||||
"session policy Deny must be evaluated even when IAM policies are empty and deny_only is set"
|
"session policy Deny must be evaluated even when IAM policies are empty and deny_only is set"
|
||||||
@@ -1381,7 +1593,8 @@ mod tests {
|
|||||||
deny_only: true,
|
deny_only: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let allowed = iam_sys.is_allowed_sts(&args, parent_user).await;
|
let prepared = iam_sys.prepare_sts_auth(&args, parent_user).await;
|
||||||
|
let allowed = iam_sys.eval_prepared(&prepared, &args).await;
|
||||||
assert!(
|
assert!(
|
||||||
allowed,
|
allowed,
|
||||||
"deny_only with no matching Deny in session policy should still allow self-service-style checks"
|
"deny_only with no matching Deny in session policy should still allow self-service-style checks"
|
||||||
@@ -1412,4 +1625,252 @@ mod tests {
|
|||||||
"regular user mapped policy must be written to user_policies for bucket user listing"
|
"regular user mapped policy must be written to user_policies for bucket user listing"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_prepare_auth_eval_matches_prepare_sts_auth_for_parent_policy_fallback() {
|
||||||
|
let store = StsTestMockStore { empty_policies: false };
|
||||||
|
let cache_manager = IamCache::new(store).await;
|
||||||
|
let iam_sys = IamSys::new(cache_manager);
|
||||||
|
|
||||||
|
let parent_user = "sts-fallback-test-parent";
|
||||||
|
let claims = HashMap::new();
|
||||||
|
let groups: Option<Vec<String>> = None;
|
||||||
|
let args = Args {
|
||||||
|
account: parent_user,
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::ListBucketAction),
|
||||||
|
bucket: "mybucket",
|
||||||
|
conditions: &HashMap::new(),
|
||||||
|
is_owner: false,
|
||||||
|
object: "",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sts_prepared = iam_sys.prepare_sts_auth(&args, parent_user).await;
|
||||||
|
let sts_eval = iam_sys.eval_prepared(&sts_prepared, &args).await;
|
||||||
|
let prepared = iam_sys.prepare_auth(&args).await;
|
||||||
|
let eval = iam_sys.eval_prepared(&prepared, &args).await;
|
||||||
|
assert_eq!(sts_eval, eval, "prepare_auth must match explicit STS preparation for this identity");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_prepare_auth_detects_existing_object_tag_in_session_policy() {
|
||||||
|
let store = StsTestMockStore { empty_policies: true };
|
||||||
|
let cache_manager = IamCache::new(store).await;
|
||||||
|
let iam_sys = IamSys::new(cache_manager);
|
||||||
|
let sts_access_key = "sts-session-tag-test-user";
|
||||||
|
|
||||||
|
let sts_user = UserIdentity::from(Credentials {
|
||||||
|
access_key: sts_access_key.to_string(),
|
||||||
|
secret_key: "longenoughsecret".to_string(),
|
||||||
|
session_token: "sts-token".to_string(),
|
||||||
|
status: ACCOUNT_ON.to_string(),
|
||||||
|
parent_user: "sts-empty-parent-policy-test".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
Cache::add_or_update(&iam_sys.store.cache.sts_accounts, sts_access_key, &sts_user, OffsetDateTime::now_utc());
|
||||||
|
|
||||||
|
let mut claims = HashMap::new();
|
||||||
|
claims.insert(
|
||||||
|
SESSION_POLICY_NAME_EXTRACTED.to_string(),
|
||||||
|
Value::String(
|
||||||
|
r#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"],"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}}]
|
||||||
|
}"#
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let groups: Option<Vec<String>> = None;
|
||||||
|
let args = Args {
|
||||||
|
account: sts_access_key,
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &HashMap::new(),
|
||||||
|
is_owner: false,
|
||||||
|
object: "obj",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prepared = iam_sys.prepare_auth(&args).await;
|
||||||
|
assert!(
|
||||||
|
prepared.needs_existing_object_tag,
|
||||||
|
"session policy with ExistingObjectTag must request object tag loading"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_policy_uses_existing_object_tag_matches_condition_keys_only() {
|
||||||
|
let with_value_only = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:GetObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:prefix":"ExistingObjectTag/security"}}
|
||||||
|
}]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("policy with value-only ExistingObjectTag text should parse");
|
||||||
|
assert!(
|
||||||
|
!policy_uses_existing_object_tag_conditions(&with_value_only),
|
||||||
|
"ExistingObjectTag text in values should not trigger tag dependency"
|
||||||
|
);
|
||||||
|
|
||||||
|
let with_condition_key = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:GetObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}
|
||||||
|
}]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("policy with ExistingObjectTag condition key should parse");
|
||||||
|
assert!(
|
||||||
|
policy_uses_existing_object_tag_conditions(&with_condition_key),
|
||||||
|
"ExistingObjectTag condition key must trigger tag dependency"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_policy_uses_existing_object_tag_when_only_secondary_action_has_tag_condition() {
|
||||||
|
let split_action_policy = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[
|
||||||
|
{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:DeleteObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:DeleteObjectVersion"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("split-action policy should parse");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
policy_uses_existing_object_tag_conditions(&split_action_policy),
|
||||||
|
"full merged policy must still be detectable as containing ExistingObjectTag keys"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_prepare_auth_detects_existing_object_tag_in_encoded_session_policy() {
|
||||||
|
let store = StsTestMockStore { empty_policies: true };
|
||||||
|
let cache_manager = IamCache::new(store).await;
|
||||||
|
let iam_sys = IamSys::new(cache_manager);
|
||||||
|
let sts_access_key = "sts-session-tag-encoded-test-user";
|
||||||
|
|
||||||
|
let sts_user = UserIdentity::from(Credentials {
|
||||||
|
access_key: sts_access_key.to_string(),
|
||||||
|
secret_key: "longenoughsecret".to_string(),
|
||||||
|
session_token: "sts-token".to_string(),
|
||||||
|
status: ACCOUNT_ON.to_string(),
|
||||||
|
parent_user: "sts-empty-parent-policy-test".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
Cache::add_or_update(&iam_sys.store.cache.sts_accounts, sts_access_key, &sts_user, OffsetDateTime::now_utc());
|
||||||
|
|
||||||
|
let session_policy_json = r#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"],"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}}]
|
||||||
|
}"#;
|
||||||
|
let mut claims = HashMap::new();
|
||||||
|
claims.insert(
|
||||||
|
SESSION_POLICY_NAME.to_string(),
|
||||||
|
Value::String(base64_simd::URL_SAFE_NO_PAD.encode_to_string(session_policy_json.as_bytes())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let groups: Option<Vec<String>> = None;
|
||||||
|
let args = Args {
|
||||||
|
account: sts_access_key,
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &HashMap::new(),
|
||||||
|
is_owner: false,
|
||||||
|
object: "obj",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prepared = iam_sys.prepare_auth(&args).await;
|
||||||
|
assert!(
|
||||||
|
prepared.needs_existing_object_tag,
|
||||||
|
"base64 sessionPolicy with ExistingObjectTag must request object tag loading"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_prepare_auth_service_account_inherited_ignores_session_policy_tag_hint() {
|
||||||
|
let store = StsTestMockStore { empty_policies: false };
|
||||||
|
let cache_manager = IamCache::new(store).await;
|
||||||
|
let iam_sys = IamSys::new(cache_manager);
|
||||||
|
|
||||||
|
let service_account_access_key = "svc-inherited-tag-hint-test-user";
|
||||||
|
let parent_user = "sts-fallback-test-parent";
|
||||||
|
let mut service_account_claims = HashMap::new();
|
||||||
|
service_account_claims.insert(iam_policy_claim_name_sa(), Value::String(INHERITED_POLICY_TYPE.to_string()));
|
||||||
|
let service_identity = UserIdentity::from(Credentials {
|
||||||
|
access_key: service_account_access_key.to_string(),
|
||||||
|
secret_key: "longenoughsecret".to_string(),
|
||||||
|
status: ACCOUNT_ON.to_string(),
|
||||||
|
parent_user: parent_user.to_string(),
|
||||||
|
claims: Some(service_account_claims),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
Cache::add_or_update(
|
||||||
|
&iam_sys.store.cache.users,
|
||||||
|
service_account_access_key,
|
||||||
|
&service_identity,
|
||||||
|
OffsetDateTime::now_utc(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut request_claims = HashMap::new();
|
||||||
|
request_claims.insert("parent".to_string(), Value::String(parent_user.to_string()));
|
||||||
|
request_claims.insert(iam_policy_claim_name_sa(), Value::String(INHERITED_POLICY_TYPE.to_string()));
|
||||||
|
request_claims.insert(
|
||||||
|
SESSION_POLICY_NAME_EXTRACTED.to_string(),
|
||||||
|
Value::String(
|
||||||
|
r#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"],"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}}]
|
||||||
|
}"#
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let groups: Option<Vec<String>> = Some(vec!["testgroup".to_string()]);
|
||||||
|
let args = Args {
|
||||||
|
account: service_account_access_key,
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &HashMap::new(),
|
||||||
|
is_owner: false,
|
||||||
|
object: "obj",
|
||||||
|
claims: &request_claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prepared = iam_sys.prepare_auth(&args).await;
|
||||||
|
assert!(
|
||||||
|
!prepared.needs_existing_object_tag,
|
||||||
|
"inherited service account should not require object tag fetch based on session policy hint"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use super::{Effect, Error as IamError, ID, Statement, action::Action, statement::BPStatement};
|
use super::{
|
||||||
|
Effect, Error as IamError, Functions, ID, Statement, action::Action, statement::BPStatement,
|
||||||
|
statement::variable_resolver_for_policy_args,
|
||||||
|
};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -272,6 +275,77 @@ pub fn iam_policy_claim_name_sa() -> String {
|
|||||||
rustfs_credentials::IAM_POLICY_CLAIM_NAME_SA.to_string()
|
rustfs_credentials::IAM_POLICY_CLAIM_NAME_SA.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_existing_object_tag_condition_key(key: &str) -> bool {
|
||||||
|
matches!(key, "ExistingObjectTag" | "s3:ExistingObjectTag")
|
||||||
|
|| key.starts_with("ExistingObjectTag/")
|
||||||
|
|| key.starts_with("s3:ExistingObjectTag/")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value_uses_existing_object_tag_condition_key(value: &Value) -> bool {
|
||||||
|
match value {
|
||||||
|
Value::Object(obj) => obj
|
||||||
|
.iter()
|
||||||
|
.any(|(key, value)| is_existing_object_tag_condition_key(key) || value_uses_existing_object_tag_condition_key(value)),
|
||||||
|
Value::Array(items) => items.iter().any(value_uses_existing_object_tag_condition_key),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if `conditions` JSON references `s3:ExistingObjectTag` / `ExistingObjectTag/...` keys.
|
||||||
|
pub fn functions_use_existing_object_tag(conditions: &Functions) -> bool {
|
||||||
|
serde_json::to_value(conditions)
|
||||||
|
.map(|v| value_uses_existing_object_tag_condition_key(&v))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn policy_uses_existing_object_tag_conditions(policy: &Policy) -> bool {
|
||||||
|
policy
|
||||||
|
.statements
|
||||||
|
.iter()
|
||||||
|
.any(|statement| functions_use_existing_object_tag(&statement.conditions))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bucket_policy_uses_existing_object_tag_conditions(policy: &BucketPolicy) -> bool {
|
||||||
|
policy
|
||||||
|
.statements
|
||||||
|
.iter()
|
||||||
|
.any(|statement| functions_use_existing_object_tag(&statement.conditions))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when at least one statement that applies to `args` may evaluate ExistingObjectTag conditions.
|
||||||
|
pub async fn policy_needs_existing_object_tag_for_args(policy: &Policy, args: &Args<'_>) -> bool {
|
||||||
|
if !policy_uses_existing_object_tag_conditions(policy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let resolver = variable_resolver_for_policy_args(args);
|
||||||
|
for statement in &policy.statements {
|
||||||
|
if !functions_use_existing_object_tag(&statement.conditions) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if statement.request_reaches_condition_eval(args, &resolver).await {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when at least one bucket-policy statement that applies to `args` may evaluate ExistingObjectTag conditions.
|
||||||
|
pub async fn bucket_policy_needs_existing_object_tag_for_args(policy: &BucketPolicy, args: &BucketPolicyArgs<'_>) -> bool {
|
||||||
|
if !bucket_policy_uses_existing_object_tag_conditions(policy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for statement in &policy.statements {
|
||||||
|
if !functions_use_existing_object_tag(&statement.conditions) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if statement.request_reaches_condition_eval(args).await {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
pub mod default {
|
pub mod default {
|
||||||
use std::{collections::HashSet, sync::LazyLock};
|
use std::{collections::HashSet, sync::LazyLock};
|
||||||
|
|
||||||
@@ -1231,6 +1305,61 @@ mod test {
|
|||||||
assert_eq!(statement["Principal"]["AWS"], "*");
|
assert_eq!(statement["Principal"]["AWS"], "*");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_existing_object_tag_condition_helpers() {
|
||||||
|
let identity_policy = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:GetObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}
|
||||||
|
}]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("identity policy with ExistingObjectTag key should parse");
|
||||||
|
assert!(
|
||||||
|
policy_uses_existing_object_tag_conditions(&identity_policy),
|
||||||
|
"identity policy ExistingObjectTag key should be detected"
|
||||||
|
);
|
||||||
|
|
||||||
|
let identity_value_only = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:GetObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:prefix":"ExistingObjectTag/security"}}
|
||||||
|
}]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("identity policy with value-only marker should parse");
|
||||||
|
assert!(
|
||||||
|
!policy_uses_existing_object_tag_conditions(&identity_value_only),
|
||||||
|
"value-only marker must not be treated as ExistingObjectTag condition key"
|
||||||
|
);
|
||||||
|
|
||||||
|
let bucket_policy: BucketPolicy = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Principal":"*",
|
||||||
|
"Action":["s3:GetObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}
|
||||||
|
}]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("bucket policy with ExistingObjectTag key should parse");
|
||||||
|
assert!(
|
||||||
|
bucket_policy_uses_existing_object_tag_conditions(&bucket_policy),
|
||||||
|
"bucket policy ExistingObjectTag key should be detected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bucket_policy_serialize_single_action_as_array() {
|
fn test_bucket_policy_serialize_single_action_as_array() {
|
||||||
use crate::policy::action::{Action, ActionSet, S3Action};
|
use crate::policy::action::{Action, ActionSet, S3Action};
|
||||||
@@ -1359,4 +1488,209 @@ mod test {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_policy_needs_existing_object_tag_narrows_by_action() {
|
||||||
|
use crate::policy::Args;
|
||||||
|
use crate::policy::action::{Action, S3Action};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
let split_policy = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[
|
||||||
|
{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:DeleteObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:DeleteObjectVersion"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("split-action policy should parse");
|
||||||
|
|
||||||
|
let groups: Option<Vec<String>> = None;
|
||||||
|
let cond = HashMap::new();
|
||||||
|
let claims = HashMap::new();
|
||||||
|
|
||||||
|
let args_get = Args {
|
||||||
|
account: "user",
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &cond,
|
||||||
|
is_owner: false,
|
||||||
|
object: "k",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
!policy_needs_existing_object_tag_for_args(&split_policy, &args_get).await,
|
||||||
|
"GetObject should not match statements with DeleteObject/DeleteObjectVersion"
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_del = Args {
|
||||||
|
account: "user",
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::DeleteObjectAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &cond,
|
||||||
|
is_owner: false,
|
||||||
|
object: "k",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
!policy_needs_existing_object_tag_for_args(&split_policy, &args_del).await,
|
||||||
|
"DeleteObject matches only the statement without ExistingObjectTag"
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_delv = Args {
|
||||||
|
account: "user",
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::DeleteObjectVersionAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &cond,
|
||||||
|
is_owner: false,
|
||||||
|
object: "k",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
policy_needs_existing_object_tag_for_args(&split_policy, &args_delv).await,
|
||||||
|
"DeleteObjectVersion matches the statement with ExistingObjectTag"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_policy_needs_existing_object_tag_narrows_by_resource() {
|
||||||
|
use crate::policy::Args;
|
||||||
|
use crate::policy::action::{Action, S3Action};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
let policy = Policy::parse_config(
|
||||||
|
br#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[
|
||||||
|
{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Action":["s3:GetObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/private/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("policy should parse");
|
||||||
|
|
||||||
|
let groups: Option<Vec<String>> = None;
|
||||||
|
let cond = HashMap::new();
|
||||||
|
let claims = HashMap::new();
|
||||||
|
|
||||||
|
let args_public = Args {
|
||||||
|
account: "user",
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &cond,
|
||||||
|
is_owner: false,
|
||||||
|
object: "public/a.txt",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
!policy_needs_existing_object_tag_for_args(&policy, &args_public).await,
|
||||||
|
"resource mismatch should skip ExistingObjectTag fetch hint"
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_private = Args {
|
||||||
|
account: "user",
|
||||||
|
groups: &groups,
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
bucket: "bucket",
|
||||||
|
conditions: &cond,
|
||||||
|
is_owner: false,
|
||||||
|
object: "private/a.txt",
|
||||||
|
claims: &claims,
|
||||||
|
deny_only: false,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
policy_needs_existing_object_tag_for_args(&policy, &args_private).await,
|
||||||
|
"resource match should keep ExistingObjectTag fetch hint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bucket_policy_needs_existing_object_tag_narrows_by_principal() {
|
||||||
|
use crate::policy::BucketPolicyArgs;
|
||||||
|
use crate::policy::action::{Action, S3Action};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
let bucket_policy: BucketPolicy = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Statement":[
|
||||||
|
{
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Principal":{"AWS":["alice"]},
|
||||||
|
"Action":["s3:GetObject"],
|
||||||
|
"Resource":["arn:aws:s3:::bucket/private/*"],
|
||||||
|
"Condition":{"StringEquals":{"s3:ExistingObjectTag/security":"public"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("bucket policy should parse");
|
||||||
|
|
||||||
|
let groups: Option<Vec<String>> = None;
|
||||||
|
let cond = HashMap::new();
|
||||||
|
|
||||||
|
let args_bob = BucketPolicyArgs {
|
||||||
|
bucket: "bucket",
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
is_owner: false,
|
||||||
|
account: "bob",
|
||||||
|
groups: &groups,
|
||||||
|
conditions: &cond,
|
||||||
|
object: "private/a.txt",
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
!bucket_policy_needs_existing_object_tag_for_args(&bucket_policy, &args_bob).await,
|
||||||
|
"principal mismatch should skip ExistingObjectTag fetch hint"
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_alice_public = BucketPolicyArgs {
|
||||||
|
bucket: "bucket",
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
is_owner: false,
|
||||||
|
account: "alice",
|
||||||
|
groups: &groups,
|
||||||
|
conditions: &cond,
|
||||||
|
object: "public/a.txt",
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
!bucket_policy_needs_existing_object_tag_for_args(&bucket_policy, &args_alice_public).await,
|
||||||
|
"resource mismatch should skip ExistingObjectTag fetch hint"
|
||||||
|
);
|
||||||
|
|
||||||
|
let args_alice_private = BucketPolicyArgs {
|
||||||
|
bucket: "bucket",
|
||||||
|
action: Action::S3Action(S3Action::GetObjectAction),
|
||||||
|
is_owner: false,
|
||||||
|
account: "alice",
|
||||||
|
groups: &groups,
|
||||||
|
conditions: &cond,
|
||||||
|
object: "private/a.txt",
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
bucket_policy_needs_existing_object_tag_for_args(&bucket_policy, &args_alice_private).await,
|
||||||
|
"principal and resource match should keep ExistingObjectTag fetch hint"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,24 @@ pub struct Statement {
|
|||||||
pub conditions: Functions,
|
pub conditions: Functions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds the same [`VariableResolver`] as [`Statement::is_allowed`].
|
||||||
|
pub(crate) fn variable_resolver_for_policy_args(args: &Args<'_>) -> VariableResolver {
|
||||||
|
let mut context = VariableContext::new();
|
||||||
|
context.claims = Some(args.claims.clone());
|
||||||
|
context.conditions = args.conditions.clone();
|
||||||
|
context.account_id = Some(args.account.to_string());
|
||||||
|
|
||||||
|
let username = if let Some(parent) = args.claims.get("parent").and_then(|v| v.as_str()) {
|
||||||
|
parent.to_string()
|
||||||
|
} else {
|
||||||
|
args.account.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
context.username = Some(username);
|
||||||
|
|
||||||
|
VariableResolver::new(context)
|
||||||
|
}
|
||||||
|
|
||||||
impl Statement {
|
impl Statement {
|
||||||
fn is_kms(&self) -> bool {
|
fn is_kms(&self) -> bool {
|
||||||
for act in self.actions.iter() {
|
for act in self.actions.iter() {
|
||||||
@@ -69,67 +87,62 @@ impl Statement {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_allowed(&self, args: &Args<'_>) -> bool {
|
/// Returns true when this statement would reach `conditions.evaluate_with_resolver` in
|
||||||
let mut context = VariableContext::new();
|
/// [`Statement::is_allowed`] (including the KMS shortcut path). Does not evaluate conditions.
|
||||||
context.claims = Some(args.claims.clone());
|
pub(crate) async fn request_reaches_condition_eval(&self, args: &Args<'_>, resolver: &VariableResolver) -> bool {
|
||||||
context.conditions = args.conditions.clone();
|
if (!self.actions.is_match(&args.action) && !self.actions.is_empty()) || self.not_actions.is_match(&args.action) {
|
||||||
context.account_id = Some(args.account.to_string());
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let username = if let Some(parent) = args.claims.get("parent").and_then(|v| v.as_str()) {
|
let mut resource = String::from(args.bucket);
|
||||||
// For temp credentials or service account credentials, username is parent_user
|
if !args.object.is_empty() {
|
||||||
parent.to_string()
|
if !args.object.starts_with('/') {
|
||||||
} else {
|
|
||||||
// For regular user credentials, username is access_key
|
|
||||||
args.account.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
context.username = Some(username);
|
|
||||||
|
|
||||||
let resolver = VariableResolver::new(context);
|
|
||||||
|
|
||||||
let check = 'c: {
|
|
||||||
if (!self.actions.is_match(&args.action) && !self.actions.is_empty()) || self.not_actions.is_match(&args.action) {
|
|
||||||
break 'c false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut resource = String::from(args.bucket);
|
|
||||||
if !args.object.is_empty() {
|
|
||||||
if !args.object.starts_with('/') {
|
|
||||||
resource.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
resource.push_str(args.object);
|
|
||||||
} else {
|
|
||||||
resource.push('/');
|
resource.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.is_kms() && (resource == "/" || self.resources.is_empty()) {
|
resource.push_str(args.object);
|
||||||
break 'c self.conditions.evaluate_with_resolver(args.conditions, Some(&resolver)).await;
|
} else {
|
||||||
}
|
resource.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
if self.resources.is_empty() && self.not_resources.is_empty() && !self.is_admin() && !self.is_sts() {
|
if self.is_kms() && (resource == "/" || self.resources.is_empty()) {
|
||||||
break 'c false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.resources.is_empty()
|
if self.resources.is_empty() && self.not_resources.is_empty() && !self.is_admin() && !self.is_sts() {
|
||||||
&& !self
|
return false;
|
||||||
.resources
|
}
|
||||||
.is_match_with_resolver(&resource, args.conditions, Some(&resolver))
|
|
||||||
.await
|
|
||||||
&& !self.is_admin()
|
|
||||||
&& !self.is_sts()
|
|
||||||
{
|
|
||||||
break 'c false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.not_resources.is_empty()
|
if !self.resources.is_empty()
|
||||||
&& self
|
&& !self
|
||||||
.not_resources
|
.resources
|
||||||
.is_match_with_resolver(&resource, args.conditions, Some(&resolver))
|
.is_match_with_resolver(&resource, args.conditions, Some(resolver))
|
||||||
.await
|
.await
|
||||||
&& !self.is_admin()
|
&& !self.is_admin()
|
||||||
&& !self.is_sts()
|
&& !self.is_sts()
|
||||||
{
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.not_resources.is_empty()
|
||||||
|
&& self
|
||||||
|
.not_resources
|
||||||
|
.is_match_with_resolver(&resource, args.conditions, Some(resolver))
|
||||||
|
.await
|
||||||
|
&& !self.is_admin()
|
||||||
|
&& !self.is_sts()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_allowed(&self, args: &Args<'_>) -> bool {
|
||||||
|
let resolver = variable_resolver_for_policy_args(args);
|
||||||
|
|
||||||
|
let check = 'c: {
|
||||||
|
if !self.request_reaches_condition_eval(args, &resolver).await {
|
||||||
break 'c false;
|
break 'c false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,32 +220,41 @@ pub struct BPStatement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BPStatement {
|
impl BPStatement {
|
||||||
pub async fn is_allowed(&self, args: &BucketPolicyArgs<'_>) -> bool {
|
/// Returns true when this statement would reach `conditions.evaluate` in [`BPStatement::is_allowed`].
|
||||||
let check = 'c: {
|
pub(crate) async fn request_reaches_condition_eval(&self, args: &BucketPolicyArgs<'_>) -> bool {
|
||||||
if !self.principal.is_match(args.account) {
|
if !self.principal.is_match(args.account) {
|
||||||
break 'c false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!self.actions.is_match(&args.action) && !self.actions.is_empty()) || self.not_actions.is_match(&args.action) {
|
if (!self.actions.is_match(&args.action) && !self.actions.is_empty()) || self.not_actions.is_match(&args.action) {
|
||||||
break 'c false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut resource = String::from(args.bucket);
|
let mut resource = String::from(args.bucket);
|
||||||
if !args.object.is_empty() {
|
if !args.object.is_empty() {
|
||||||
if !args.object.starts_with('/') {
|
if !args.object.starts_with('/') {
|
||||||
resource.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
resource.push_str(args.object);
|
|
||||||
} else {
|
|
||||||
resource.push('/');
|
resource.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.resources.is_empty() && !self.resources.is_match(&resource, args.conditions).await {
|
resource.push_str(args.object);
|
||||||
break 'c false;
|
} else {
|
||||||
}
|
resource.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
if !self.not_resources.is_empty() && self.not_resources.is_match(&resource, args.conditions).await {
|
if !self.resources.is_empty() && !self.resources.is_match(&resource, args.conditions).await {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.not_resources.is_empty() && self.not_resources.is_match(&resource, args.conditions).await {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_allowed(&self, args: &BucketPolicyArgs<'_>) -> bool {
|
||||||
|
let check = 'c: {
|
||||||
|
if !self.request_reaches_condition_eval(args).await {
|
||||||
break 'c false;
|
break 'c false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,6 @@ pub async fn validate_admin_request_with_bucket(
|
|||||||
Err(s3_error!(AccessDenied, "Access Denied"))
|
Err(s3_error!(AccessDenied, "Access Denied"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unified authentication request handler for both UI and CLI
|
|
||||||
///
|
|
||||||
/// This function provides a single entry point for authentication,
|
|
||||||
/// Unified authentication request handler for both UI and CLI
|
/// Unified authentication request handler for both UI and CLI
|
||||||
///
|
///
|
||||||
/// This function provides a single entry point for authentication,
|
/// This function provides a single entry point for authentication,
|
||||||
|
|||||||
+593
-193
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user