Add policy engine (#6970)
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
@@ -26,6 +27,12 @@ var (
|
||||
ErrGovernanceModeActive = errors.New("object is under GOVERNANCE mode retention and cannot be deleted or modified without bypass")
|
||||
)
|
||||
|
||||
// Error definitions for Object Lock
|
||||
var (
|
||||
ErrObjectUnderLegalHold = errors.New("object is under legal hold and cannot be deleted or modified")
|
||||
ErrGovernanceBypassNotPermitted = errors.New("user does not have permission to bypass governance retention")
|
||||
)
|
||||
|
||||
const (
|
||||
// Maximum retention period limits according to AWS S3 specifications
|
||||
MaxRetentionDays = 36500 // Maximum number of days for object retention (100 years)
|
||||
@@ -103,13 +110,13 @@ func (or *ObjectRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
|
||||
// This approach is optimized for small XML payloads typical in S3 API requests
|
||||
// (retention configurations, legal hold settings, etc.) where the overhead of
|
||||
// streaming parsing is acceptable for the memory efficiency benefits.
|
||||
func parseXML[T any](r *http.Request, result *T) error {
|
||||
if r.Body == nil {
|
||||
func parseXML[T any](request *http.Request, result *T) error {
|
||||
if request.Body == nil {
|
||||
return fmt.Errorf("error parsing XML: empty request body")
|
||||
}
|
||||
defer r.Body.Close()
|
||||
defer request.Body.Close()
|
||||
|
||||
decoder := xml.NewDecoder(r.Body)
|
||||
decoder := xml.NewDecoder(request.Body)
|
||||
if err := decoder.Decode(result); err != nil {
|
||||
return fmt.Errorf("error parsing XML: %v", err)
|
||||
}
|
||||
@@ -118,27 +125,27 @@ func parseXML[T any](r *http.Request, result *T) error {
|
||||
}
|
||||
|
||||
// parseObjectRetention parses XML retention configuration from request body
|
||||
func parseObjectRetention(r *http.Request) (*ObjectRetention, error) {
|
||||
func parseObjectRetention(request *http.Request) (*ObjectRetention, error) {
|
||||
var retention ObjectRetention
|
||||
if err := parseXML(r, &retention); err != nil {
|
||||
if err := parseXML(request, &retention); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &retention, nil
|
||||
}
|
||||
|
||||
// parseObjectLegalHold parses XML legal hold configuration from request body
|
||||
func parseObjectLegalHold(r *http.Request) (*ObjectLegalHold, error) {
|
||||
func parseObjectLegalHold(request *http.Request) (*ObjectLegalHold, error) {
|
||||
var legalHold ObjectLegalHold
|
||||
if err := parseXML(r, &legalHold); err != nil {
|
||||
if err := parseXML(request, &legalHold); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &legalHold, nil
|
||||
}
|
||||
|
||||
// parseObjectLockConfiguration parses XML object lock configuration from request body
|
||||
func parseObjectLockConfiguration(r *http.Request) (*ObjectLockConfiguration, error) {
|
||||
func parseObjectLockConfiguration(request *http.Request) (*ObjectLockConfiguration, error) {
|
||||
var config ObjectLockConfiguration
|
||||
if err := parseXML(r, &config); err != nil {
|
||||
if err := parseXML(request, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
@@ -514,8 +521,39 @@ func (s3a *S3ApiServer) isObjectLegalHoldActive(bucket, object, versionId string
|
||||
return legalHold.Status == s3_constants.LegalHoldOn, nil
|
||||
}
|
||||
|
||||
// checkGovernanceBypassPermission checks if the user has permission to bypass governance retention
|
||||
func (s3a *S3ApiServer) checkGovernanceBypassPermission(request *http.Request, bucket, object string) bool {
|
||||
// Use the existing IAM auth system to check the specific permission
|
||||
// Create the governance bypass action with proper bucket/object concatenation
|
||||
// Note: path.Join would drop bucket if object has leading slash, so use explicit formatting
|
||||
resource := fmt.Sprintf("%s/%s", bucket, strings.TrimPrefix(object, "/"))
|
||||
action := Action(fmt.Sprintf("%s:%s", s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION, resource))
|
||||
|
||||
// Use the IAM system to authenticate and authorize this specific action
|
||||
identity, errCode := s3a.iam.authRequest(request, action)
|
||||
if errCode != s3err.ErrNone {
|
||||
glog.V(3).Infof("IAM auth failed for governance bypass: %v", errCode)
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify that the authenticated identity can perform this action
|
||||
if identity != nil && identity.canDo(action, bucket, object) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Additional check: allow users with Admin action to bypass governance retention
|
||||
// Use the proper S3 Admin action constant instead of generic isAdmin() method
|
||||
adminAction := Action(fmt.Sprintf("%s:%s", s3_constants.ACTION_ADMIN, resource))
|
||||
if identity != nil && identity.canDo(adminAction, bucket, object) {
|
||||
glog.V(2).Infof("Admin user %s granted governance bypass permission for %s/%s", identity.Name, bucket, object)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// checkObjectLockPermissions checks if an object can be deleted or modified
|
||||
func (s3a *S3ApiServer) checkObjectLockPermissions(bucket, object, versionId string, bypassGovernance bool) error {
|
||||
func (s3a *S3ApiServer) checkObjectLockPermissions(request *http.Request, bucket, object, versionId string, bypassGovernance bool) error {
|
||||
// Get retention configuration and status in a single call to avoid duplicate fetches
|
||||
retention, retentionActive, err := s3a.getObjectRetentionWithStatus(bucket, object, versionId)
|
||||
if err != nil {
|
||||
@@ -530,7 +568,7 @@ func (s3a *S3ApiServer) checkObjectLockPermissions(bucket, object, versionId str
|
||||
|
||||
// If object is under legal hold, it cannot be deleted or modified
|
||||
if legalHoldActive {
|
||||
return fmt.Errorf("object is under legal hold and cannot be deleted or modified")
|
||||
return ErrObjectUnderLegalHold
|
||||
}
|
||||
|
||||
// If object is under retention, check the mode
|
||||
@@ -539,8 +577,16 @@ func (s3a *S3ApiServer) checkObjectLockPermissions(bucket, object, versionId str
|
||||
return ErrComplianceModeActive
|
||||
}
|
||||
|
||||
if retention.Mode == s3_constants.RetentionModeGovernance && !bypassGovernance {
|
||||
return ErrGovernanceModeActive
|
||||
if retention.Mode == s3_constants.RetentionModeGovernance {
|
||||
if !bypassGovernance {
|
||||
return ErrGovernanceModeActive
|
||||
}
|
||||
|
||||
// If bypass is requested, check if user has permission
|
||||
if !s3a.checkGovernanceBypassPermission(request, bucket, object) {
|
||||
glog.V(2).Infof("User does not have s3:BypassGovernanceRetention permission for %s/%s", bucket, object)
|
||||
return ErrGovernanceBypassNotPermitted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,14 +613,14 @@ func (s3a *S3ApiServer) isObjectLockAvailable(bucket string) error {
|
||||
|
||||
// checkObjectLockPermissionsForPut checks object lock permissions for PUT operations
|
||||
// This is a shared helper to avoid code duplication in PUT handlers
|
||||
func (s3a *S3ApiServer) checkObjectLockPermissionsForPut(bucket, object string, bypassGovernance bool, versioningEnabled bool) error {
|
||||
func (s3a *S3ApiServer) checkObjectLockPermissionsForPut(request *http.Request, bucket, object string, bypassGovernance bool, versioningEnabled bool) error {
|
||||
// Object Lock only applies to versioned buckets (AWS S3 requirement)
|
||||
if !versioningEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For PUT operations, we check permissions on the current object (empty versionId)
|
||||
if err := s3a.checkObjectLockPermissions(bucket, object, "", bypassGovernance); err != nil {
|
||||
if err := s3a.checkObjectLockPermissions(request, bucket, object, "", bypassGovernance); err != nil {
|
||||
glog.V(2).Infof("checkObjectLockPermissionsForPut: object lock check failed for %s/%s: %v", bucket, object, err)
|
||||
return err
|
||||
}
|
||||
@@ -584,13 +630,13 @@ func (s3a *S3ApiServer) checkObjectLockPermissionsForPut(bucket, object string,
|
||||
// handleObjectLockAvailabilityCheck is a helper function to check object lock availability
|
||||
// and write the appropriate error response if not available. This reduces code duplication
|
||||
// across all retention handlers.
|
||||
func (s3a *S3ApiServer) handleObjectLockAvailabilityCheck(w http.ResponseWriter, r *http.Request, bucket, handlerName string) bool {
|
||||
func (s3a *S3ApiServer) handleObjectLockAvailabilityCheck(w http.ResponseWriter, request *http.Request, bucket, handlerName string) bool {
|
||||
if err := s3a.isObjectLockAvailable(bucket); err != nil {
|
||||
glog.Errorf("%s: object lock not available for bucket %s: %v", handlerName, bucket, err)
|
||||
if errors.Is(err, ErrBucketNotFound) {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
||||
s3err.WriteErrorResponse(w, request, s3err.ErrNoSuchBucket)
|
||||
} else {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||
s3err.WriteErrorResponse(w, request, s3err.ErrInvalidRequest)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user