mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 11:06:17 +00:00
a65856bdf4
* fix: get_condition_values * fix checksum crc32c * fix clippy
289 lines
8.7 KiB
Rust
289 lines
8.7 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 crate::policy::Args as PArgs;
|
|
use rustfs_config::{ENV_PREFIX, opa::*};
|
|
use serde::Deserialize;
|
|
use serde_json::json;
|
|
use std::{collections::HashMap, env, time::Duration};
|
|
use tracing::{error, info};
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct Args {
|
|
pub url: String,
|
|
pub auth_token: String,
|
|
}
|
|
impl Args {
|
|
pub fn enable(&self) -> bool {
|
|
!self.url.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct AuthZPlugin {
|
|
client: reqwest::Client,
|
|
args: Args,
|
|
}
|
|
|
|
fn check() -> Result<(), String> {
|
|
let env_list = env::vars();
|
|
let mut candidate = HashMap::new();
|
|
let prefix = format!("{ENV_PREFIX}{POLICY_PLUGIN_SUB_SYS}").to_uppercase();
|
|
for (key, value) in env_list {
|
|
if key.starts_with(&prefix) {
|
|
candidate.insert(key.to_string(), value);
|
|
}
|
|
}
|
|
|
|
//check required env vars
|
|
if candidate.remove(ENV_POLICY_PLUGIN_OPA_URL).is_none() {
|
|
return Err(format!("Missing required env var: {ENV_POLICY_PLUGIN_OPA_URL}"));
|
|
}
|
|
|
|
// check optional env vars
|
|
candidate.remove(ENV_POLICY_PLUGIN_AUTH_TOKEN);
|
|
if !candidate.is_empty() {
|
|
return Err(format!("Invalid env vars: {candidate:?}"));
|
|
}
|
|
Ok(())
|
|
}
|
|
async fn validate(config: &Args) -> Result<(), String> {
|
|
let client = reqwest::Client::new();
|
|
|
|
match client.post(&config.url).send().await {
|
|
Ok(resp) => {
|
|
match resp.status() {
|
|
reqwest::StatusCode::OK => {
|
|
info!("OPA is ready to accept requests.");
|
|
}
|
|
_ => {
|
|
return Err(format!("OPA returned an error: {}", resp.status()));
|
|
}
|
|
};
|
|
}
|
|
Err(err) => {
|
|
return Err(format!("Error connecting to OPA: {err}"));
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn lookup_config() -> Result<Args, String> {
|
|
let args = Args::default();
|
|
|
|
let get_cfg =
|
|
|cfg: &str| -> Result<String, String> { env::var(cfg).map_err(|e| format!("Error getting env var {cfg}: {e:?}")) };
|
|
|
|
let url = get_cfg(ENV_POLICY_PLUGIN_OPA_URL);
|
|
if url.is_err() {
|
|
info!("OPA is not enabled.");
|
|
return Ok(args);
|
|
}
|
|
check()?;
|
|
let args = Args {
|
|
url: url.ok().unwrap(),
|
|
auth_token: get_cfg(ENV_POLICY_PLUGIN_AUTH_TOKEN).unwrap_or_default(),
|
|
};
|
|
validate(&args).await?;
|
|
Ok(args)
|
|
}
|
|
|
|
impl AuthZPlugin {
|
|
pub fn new(config: Args) -> Self {
|
|
let client = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(5))
|
|
.connect_timeout(Duration::from_secs(1))
|
|
.pool_max_idle_per_host(10)
|
|
.pool_idle_timeout(Some(Duration::from_secs(60)))
|
|
.tcp_keepalive(Some(Duration::from_secs(30)))
|
|
.tcp_nodelay(true)
|
|
.http2_keep_alive_interval(Some(Duration::from_secs(30)))
|
|
.http2_keep_alive_timeout(Duration::from_secs(15))
|
|
.build()
|
|
.unwrap();
|
|
|
|
Self { client, args: config }
|
|
}
|
|
|
|
pub async fn is_allowed(&self, args: &PArgs<'_>) -> bool {
|
|
let payload = self.build_opa_input(args);
|
|
|
|
let mut request = self.client.post(self.args.url.clone()).json(&payload);
|
|
if !self.args.auth_token.is_empty() {
|
|
request = request.header("Authorization", format!("Bearer {}", self.args.auth_token));
|
|
}
|
|
|
|
match request.send().await {
|
|
Ok(resp) => {
|
|
let status = resp.status();
|
|
if !status.is_success() {
|
|
error!("OPA returned non-success status: {}", status);
|
|
return false;
|
|
}
|
|
|
|
match resp.json::<OpaResponseEnum>().await {
|
|
Ok(response_enum) => match response_enum {
|
|
OpaResponseEnum::SimpleResult(result) => result.result,
|
|
OpaResponseEnum::AllowResult(response) => response.result.allow,
|
|
},
|
|
Err(err) => {
|
|
error!("Error parsing OPA response: {:?}", err);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
error!("Error sending request to OPA: {:?}", err);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_opa_input(&self, args: &PArgs<'_>) -> serde_json::Value {
|
|
let groups = match args.groups {
|
|
Some(g) => g.clone(),
|
|
None => vec![],
|
|
};
|
|
let action_str: &str = (&args.action).into();
|
|
json!({
|
|
// Core authorization parameters for OPA policy evaluation
|
|
"input":{
|
|
"identity": {
|
|
"account": args.account,
|
|
"groups": groups,
|
|
"is_owner": args.is_owner,
|
|
"claims": args.claims
|
|
},
|
|
|
|
"resource": {
|
|
"bucket": args.bucket,
|
|
"object": args.object,
|
|
"arn": if args.object.is_empty() {
|
|
format!("arn:aws:s3:::{}", args.bucket)
|
|
} else {
|
|
format!("arn:aws:s3:::{}/{}", args.bucket, args.object)
|
|
}
|
|
},
|
|
|
|
"action": action_str,
|
|
|
|
"context": {
|
|
"conditions": args.conditions,
|
|
"deny_only": args.deny_only,
|
|
"timestamp": chrono::Utc::now().to_rfc3339()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct OpaResultAllow {
|
|
allow: bool,
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct OpaResult {
|
|
result: bool,
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct OpaResponse {
|
|
result: OpaResultAllow,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(untagged)]
|
|
enum OpaResponseEnum {
|
|
SimpleResult(OpaResult),
|
|
AllowResult(OpaResponse),
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use temp_env;
|
|
|
|
#[test]
|
|
fn test_check_valid_config() {
|
|
// Use temp_env to temporarily set environment variables
|
|
temp_env::with_vars(
|
|
[
|
|
("RUSTFS_POLICY_PLUGIN_URL", Some("http://localhost:8181/v1/data/rustfs/authz/allow")),
|
|
("RUSTFS_POLICY_PLUGIN_AUTH_TOKEN", Some("test-token")),
|
|
],
|
|
|| {
|
|
assert!(check().is_ok());
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_missing_required_env() {
|
|
temp_env::with_var_unset("RUSTFS_POLICY_PLUGIN_URL", || {
|
|
temp_env::with_var("RUSTFS_POLICY_PLUGIN_AUTH_TOKEN", Some("test-token"), || {
|
|
let result = check();
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Missing required env var"));
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_invalid_env_vars() {
|
|
temp_env::with_vars(
|
|
[
|
|
("RUSTFS_POLICY_PLUGIN_URL", Some("http://localhost:8181/v1/data/rustfs/authz/allow")),
|
|
("RUSTFS_POLICY_PLUGIN_INVALID", Some("invalid-value")),
|
|
],
|
|
|| {
|
|
let result = check();
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Invalid env vars"));
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_lookup_config_not_enabled() {
|
|
temp_env::with_var_unset("RUSTFS_POLICY_PLUGIN_URL", || {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
let result = rt.block_on(async { lookup_config().await });
|
|
|
|
// Should return the default empty Args
|
|
assert!(result.is_ok());
|
|
let args = result.unwrap();
|
|
assert!(!args.enable());
|
|
assert_eq!(args.url, "");
|
|
assert_eq!(args.auth_token, "");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_args_enable() {
|
|
// Test Args enable method
|
|
let args_enabled = Args {
|
|
url: "http://localhost:8181".to_string(),
|
|
auth_token: "token".to_string(),
|
|
};
|
|
assert!(args_enabled.enable());
|
|
|
|
let args_disabled = Args {
|
|
url: "".to_string(),
|
|
auth_token: "".to_string(),
|
|
};
|
|
assert!(!args_disabled.enable());
|
|
}
|
|
}
|