mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 00:26:53 +00:00
feat: migrate FTP/SFTP to protocols crate and update dependencies (#1580)
Signed-off-by: yxrxy <yxrxytrigger@gmail.com> Signed-off-by: houseme <housemecn@gmail.com> Signed-off-by: heihutu <30542132+heihutu@users.noreply.github.com> Co-authored-by: houseme <housemecn@gmail.com> Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com> Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod s3;
|
||||
|
||||
pub use s3::StorageBackend as S3StorageBackend;
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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 async_trait::async_trait;
|
||||
use s3s::dto::*;
|
||||
|
||||
#[async_trait]
|
||||
pub trait StorageBackend: Send + Sync {
|
||||
/// Error type for this storage backend
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
/// Get object content and metadata
|
||||
async fn get_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
start_pos: Option<u64>,
|
||||
) -> Result<GetObjectOutput, Self::Error>;
|
||||
async fn get_object_range(
|
||||
&self,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
start_pos: u64,
|
||||
length: u64,
|
||||
) -> Result<GetObjectOutput, Self::Error>;
|
||||
/// Put object content with metadata
|
||||
async fn put_object(&self, input: PutObjectInput, access_key: &str, secret_key: &str)
|
||||
-> Result<PutObjectOutput, Self::Error>;
|
||||
/// Delete an object
|
||||
async fn delete_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
) -> Result<DeleteObjectOutput, Self::Error>;
|
||||
/// Get object metadata without content
|
||||
async fn head_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
) -> Result<HeadObjectOutput, Self::Error>;
|
||||
/// Check if bucket exists and get metadata
|
||||
async fn head_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result<HeadBucketOutput, Self::Error>;
|
||||
/// List objects in a bucket with pagination
|
||||
async fn list_objects_v2(
|
||||
&self,
|
||||
input: ListObjectsV2Input,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
) -> Result<ListObjectsV2Output, Self::Error>;
|
||||
/// List all buckets (requires authentication)
|
||||
async fn list_buckets(&self, access_key: &str, secret_key: &str) -> Result<ListBucketsOutput, Self::Error>;
|
||||
/// Create a new bucket
|
||||
async fn create_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result<CreateBucketOutput, Self::Error>;
|
||||
/// Delete a bucket (must be empty)
|
||||
async fn delete_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result<DeleteBucketOutput, Self::Error>;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use rustfs_credentials;
|
||||
use rustfs_policy::policy::action::S3Action as PolicyS3Action;
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
|
||||
use super::session::SessionContext;
|
||||
|
||||
/// Authorization errors
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthorizationError {
|
||||
#[error("Access denied")]
|
||||
AccessDenied,
|
||||
}
|
||||
|
||||
/// S3 actions that can be performed through the gateway
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum S3Action {
|
||||
// Bucket operations
|
||||
CreateBucket,
|
||||
DeleteBucket,
|
||||
ListBucket,
|
||||
ListBuckets,
|
||||
HeadBucket,
|
||||
|
||||
// Object operations
|
||||
GetObject,
|
||||
PutObject,
|
||||
DeleteObject,
|
||||
HeadObject,
|
||||
CopyObject,
|
||||
|
||||
// Multipart operations
|
||||
CreateMultipartUpload,
|
||||
UploadPart,
|
||||
CompleteMultipartUpload,
|
||||
AbortMultipartUpload,
|
||||
ListMultipartUploads,
|
||||
ListParts,
|
||||
|
||||
// ACL operations
|
||||
GetBucketAcl,
|
||||
PutBucketAcl,
|
||||
GetObjectAcl,
|
||||
PutObjectAcl,
|
||||
}
|
||||
|
||||
impl From<S3Action> for PolicyS3Action {
|
||||
fn from(action: S3Action) -> Self {
|
||||
match action {
|
||||
S3Action::CreateBucket => PolicyS3Action::CreateBucketAction,
|
||||
S3Action::DeleteBucket => PolicyS3Action::DeleteBucketAction,
|
||||
S3Action::ListBucket => PolicyS3Action::ListBucketAction,
|
||||
S3Action::ListBuckets => PolicyS3Action::ListAllMyBucketsAction,
|
||||
S3Action::HeadBucket => PolicyS3Action::HeadBucketAction,
|
||||
S3Action::GetObject => PolicyS3Action::GetObjectAction,
|
||||
S3Action::PutObject => PolicyS3Action::PutObjectAction,
|
||||
S3Action::DeleteObject => PolicyS3Action::DeleteObjectAction,
|
||||
S3Action::HeadObject => PolicyS3Action::GetObjectAction,
|
||||
S3Action::CreateMultipartUpload => PolicyS3Action::PutObjectAction,
|
||||
S3Action::UploadPart => PolicyS3Action::PutObjectAction,
|
||||
S3Action::CompleteMultipartUpload => PolicyS3Action::PutObjectAction,
|
||||
S3Action::AbortMultipartUpload => PolicyS3Action::AbortMultipartUploadAction,
|
||||
S3Action::ListMultipartUploads => PolicyS3Action::ListBucketMultipartUploadsAction,
|
||||
S3Action::ListParts => PolicyS3Action::ListMultipartUploadPartsAction,
|
||||
S3Action::GetBucketAcl => PolicyS3Action::GetBucketPolicyAction,
|
||||
S3Action::PutBucketAcl => PolicyS3Action::PutBucketPolicyAction,
|
||||
S3Action::GetObjectAcl => PolicyS3Action::GetObjectAction,
|
||||
S3Action::PutObjectAcl => PolicyS3Action::PutObjectAction,
|
||||
S3Action::CopyObject => PolicyS3Action::PutObjectAction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<S3Action> for rustfs_policy::policy::action::Action {
|
||||
fn from(action: S3Action) -> Self {
|
||||
rustfs_policy::policy::action::Action::S3Action(action.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl S3Action {
|
||||
/// Get the string representation of the action
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
S3Action::CreateBucket => "s3:CreateBucket",
|
||||
S3Action::DeleteBucket => "s3:DeleteBucket",
|
||||
S3Action::ListBucket => "s3:ListBucket",
|
||||
S3Action::ListBuckets => "s3:ListAllMyBuckets",
|
||||
S3Action::HeadBucket => "s3:ListBucket",
|
||||
S3Action::GetObject => "s3:GetObject",
|
||||
S3Action::PutObject => "s3:PutObject",
|
||||
S3Action::DeleteObject => "s3:DeleteObject",
|
||||
S3Action::HeadObject => "s3:GetObject",
|
||||
S3Action::CreateMultipartUpload => "s3:PutObject",
|
||||
S3Action::UploadPart => "s3:PutObject",
|
||||
S3Action::CompleteMultipartUpload => "s3:PutObject",
|
||||
S3Action::AbortMultipartUpload => "s3:AbortMultipartUpload",
|
||||
S3Action::ListMultipartUploads => "s3:ListBucketMultipartUploads",
|
||||
S3Action::ListParts => "s3:ListMultipartUploadParts",
|
||||
S3Action::GetBucketAcl => "s3:GetBucketAcl",
|
||||
S3Action::PutBucketAcl => "s3:PutBucketAcl",
|
||||
S3Action::GetObjectAcl => "s3:GetObjectAcl",
|
||||
S3Action::PutObjectAcl => "s3:PutObjectAcl",
|
||||
S3Action::CopyObject => "s3:PutObject",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if operation is supported for the given protocol
|
||||
pub fn is_operation_supported(protocol: super::session::Protocol, action: &S3Action) -> bool {
|
||||
match protocol {
|
||||
super::session::Protocol::Ftps => match action {
|
||||
// Bucket operations
|
||||
S3Action::CreateBucket => true,
|
||||
S3Action::DeleteBucket => true,
|
||||
|
||||
// Object operations
|
||||
S3Action::GetObject => true, // RETR command
|
||||
S3Action::PutObject => true, // STOR and APPE commands both map to PutObject
|
||||
S3Action::DeleteObject => true, // DELE command
|
||||
S3Action::HeadObject => true, // SIZE command
|
||||
|
||||
// Multipart operations
|
||||
S3Action::CreateMultipartUpload => false,
|
||||
S3Action::UploadPart => false,
|
||||
S3Action::CompleteMultipartUpload => false,
|
||||
S3Action::AbortMultipartUpload => false,
|
||||
S3Action::ListMultipartUploads => false,
|
||||
S3Action::ListParts => false,
|
||||
|
||||
// ACL operations
|
||||
S3Action::GetBucketAcl => false,
|
||||
S3Action::PutBucketAcl => false,
|
||||
S3Action::GetObjectAcl => false,
|
||||
S3Action::PutObjectAcl => false,
|
||||
|
||||
// Other operations
|
||||
S3Action::CopyObject => false, // No native copy support in FTPS
|
||||
S3Action::ListBucket => true, // LIST command
|
||||
S3Action::ListBuckets => true, // LIST at root level
|
||||
S3Action::HeadBucket => true, // Can check if directory exists
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a principal is allowed to perform an S3 action
|
||||
pub async fn is_authorized(session_context: &SessionContext, action: &S3Action, bucket: &str, object: Option<&str>) -> bool {
|
||||
let iam_sys = match rustfs_iam::get() {
|
||||
Ok(sys) => sys,
|
||||
Err(e) => {
|
||||
error!("IAM system unavailable: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Create policy arguments
|
||||
let mut claims = HashMap::new();
|
||||
claims.insert(
|
||||
"principal".to_string(),
|
||||
serde_json::Value::String(session_context.principal.access_key().to_string()),
|
||||
);
|
||||
|
||||
let policy_action: rustfs_policy::policy::action::Action = action.clone().into();
|
||||
|
||||
// Check if user is the owner (admin)
|
||||
let is_owner = if let Some(global_cred) = rustfs_credentials::get_global_action_cred() {
|
||||
session_context.principal.access_key() == global_cred.access_key
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let args = rustfs_policy::policy::Args {
|
||||
account: session_context.principal.access_key(),
|
||||
groups: &session_context.principal.user_identity.credentials.groups,
|
||||
action: policy_action,
|
||||
bucket,
|
||||
conditions: &HashMap::new(),
|
||||
is_owner,
|
||||
object: object.unwrap_or(""),
|
||||
claims: &claims,
|
||||
deny_only: false,
|
||||
};
|
||||
|
||||
iam_sys.is_allowed(&args).await
|
||||
}
|
||||
|
||||
/// Authorize an operation and return an error if not authorized
|
||||
pub async fn authorize_operation(
|
||||
session_context: &SessionContext,
|
||||
action: &S3Action,
|
||||
bucket: &str,
|
||||
object: Option<&str>,
|
||||
) -> Result<(), AuthorizationError> {
|
||||
// check if the operation is supported
|
||||
if !is_operation_supported(session_context.protocol, action) {
|
||||
return Err(AuthorizationError::AccessDenied);
|
||||
}
|
||||
|
||||
// check IAM authorization
|
||||
if is_authorized(session_context, action, bucket, object).await {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AuthorizationError::AccessDenied)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod client;
|
||||
pub mod gateway;
|
||||
pub mod session;
|
||||
|
||||
pub use client::s3::StorageBackend as S3StorageBackend;
|
||||
pub use gateway::{AuthorizationError, S3Action, authorize_operation, is_operation_supported};
|
||||
pub use session::{ProtocolPrincipal, SessionContext};
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use rustfs_policy::auth::UserIdentity;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Protocol types
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Protocol {
|
||||
Ftps,
|
||||
}
|
||||
|
||||
/// Protocol principal representing an authenticated user
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProtocolPrincipal {
|
||||
/// User identity from IAM system
|
||||
pub user_identity: Arc<UserIdentity>,
|
||||
}
|
||||
|
||||
impl ProtocolPrincipal {
|
||||
pub fn new(user_identity: Arc<UserIdentity>) -> Self {
|
||||
Self { user_identity }
|
||||
}
|
||||
pub fn access_key(&self) -> &str {
|
||||
&self.user_identity.credentials.access_key
|
||||
}
|
||||
}
|
||||
|
||||
/// Session context for protocol operations
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionContext {
|
||||
/// The protocol principal (authenticated user)
|
||||
pub principal: ProtocolPrincipal,
|
||||
/// The protocol type
|
||||
pub protocol: Protocol,
|
||||
/// The source IP address
|
||||
pub source_ip: IpAddr,
|
||||
}
|
||||
|
||||
impl SessionContext {
|
||||
/// Create a new session context
|
||||
pub fn new(principal: ProtocolPrincipal, protocol: Protocol, source_ip: IpAddr) -> Self {
|
||||
Self {
|
||||
principal,
|
||||
protocol,
|
||||
source_ip,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the access key for this session
|
||||
pub fn access_key(&self) -> &str {
|
||||
self.principal.access_key()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user