diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-07-13 16:21:36 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-13 16:21:36 -0700 |
| commit | 7cb1ca13082568bfdcdab974d8cefddf650443c5 (patch) | |
| tree | 573b5e15d080d37b9312cade4151da9e3fb7ddee /weed/s3api/s3api_object_retention.go | |
| parent | 1549ee2e154ab040e211ac7b3bc361272069abef (diff) | |
| download | seaweedfs-7cb1ca13082568bfdcdab974d8cefddf650443c5.tar.xz seaweedfs-7cb1ca13082568bfdcdab974d8cefddf650443c5.zip | |
Add policy engine (#6970)
Diffstat (limited to 'weed/s3api/s3api_object_retention.go')
| -rw-r--r-- | weed/s3api/s3api_object_retention.go | 84 |
1 files changed, 65 insertions, 19 deletions
diff --git a/weed/s3api/s3api_object_retention.go b/weed/s3api/s3api_object_retention.go index bedf693ef..6747ac84c 100644 --- a/weed/s3api/s3api_object_retention.go +++ b/weed/s3api/s3api_object_retention.go @@ -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 } |
