You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							356 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							356 lines
						
					
					
						
							12 KiB
						
					
					
				| package s3api | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"errors" | |
| 	"fmt" | |
| 	"net/http" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" | |
| 	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" | |
| ) | |
| 
 | |
| // GetObjectAclHandler Get object ACL | |
| // https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html | |
| func (s3a *S3ApiServer) GetObjectAclHandler(w http.ResponseWriter, r *http.Request) { | |
| 	// collect parameters | |
| 	bucket, object := s3_constants.GetBucketAndObject(r) | |
| 	glog.V(3).Infof("GetObjectAclHandler %s %s", bucket, object) | |
| 
 | |
| 	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone { | |
| 		s3err.WriteErrorResponse(w, r, err) | |
| 		return | |
| 	} | |
| 
 | |
| 	// Check for specific version ID in query parameters | |
| 	versionId := r.URL.Query().Get("versionId") | |
| 
 | |
| 	// Check if versioning is configured for the bucket (Enabled or Suspended) | |
| 	versioningConfigured, err := s3a.isVersioningConfigured(bucket) | |
| 	if err != nil { | |
| 		if err == filer_pb.ErrNotFound { | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) | |
| 			return | |
| 		} | |
| 		glog.Errorf("GetObjectAclHandler: Error checking versioning status for bucket %s: %v", bucket, err) | |
| 		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) | |
| 		return | |
| 	} | |
| 
 | |
| 	var entry *filer_pb.Entry | |
| 
 | |
| 	if versioningConfigured { | |
| 		// Handle versioned object ACL retrieval - use same logic as GetObjectHandler | |
| 		if versionId != "" { | |
| 			// Request for specific version | |
| 			glog.V(2).Infof("GetObjectAclHandler: requesting ACL for specific version %s of %s%s", versionId, bucket, object) | |
| 			entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId) | |
| 		} else { | |
| 			// Request for latest version | |
| 			glog.V(2).Infof("GetObjectAclHandler: requesting ACL for latest version of %s%s", bucket, object) | |
| 			entry, err = s3a.getLatestObjectVersion(bucket, object) | |
| 		} | |
| 
 | |
| 		if err != nil { | |
| 			glog.Errorf("GetObjectAclHandler: Failed to get object version %s for %s%s: %v", versionId, bucket, object, err) | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 			return | |
| 		} | |
| 
 | |
| 		// Check if this is a delete marker | |
| 		if entry.Extended != nil { | |
| 			if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" { | |
| 				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 				return | |
| 			} | |
| 		} | |
| 	} else { | |
| 		// Handle regular (non-versioned) object ACL retrieval | |
| 		bucketDir := s3a.option.BucketsPath + "/" + bucket | |
| 		entry, err = s3a.getEntry(bucketDir, object) | |
| 		if err != nil { | |
| 			if errors.Is(err, filer_pb.ErrNotFound) { | |
| 				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 				return | |
| 			} | |
| 			glog.Errorf("GetObjectAclHandler: error checking object %s/%s: %v", bucket, object, err) | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) | |
| 			return | |
| 		} | |
| 	} | |
| 
 | |
| 	if entry == nil { | |
| 		s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 		return | |
| 	} | |
| 
 | |
| 	// Get object owner from metadata, fallback to request account | |
| 	var objectOwner string | |
| 	var objectOwnerDisplayName string | |
| 	amzAccountId := r.Header.Get(s3_constants.AmzAccountId) | |
| 
 | |
| 	if entry.Extended != nil { | |
| 		if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists { | |
| 			objectOwner = string(ownerBytes) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Fallback to current account if no owner stored | |
| 	if objectOwner == "" { | |
| 		objectOwner = amzAccountId | |
| 	} | |
| 
 | |
| 	objectOwnerDisplayName = s3a.iam.GetAccountNameById(objectOwner) | |
| 
 | |
| 	// Build ACL response | |
| 	response := AccessControlPolicy{ | |
| 		Owner: CanonicalUser{ | |
| 			ID:          objectOwner, | |
| 			DisplayName: objectOwnerDisplayName, | |
| 		}, | |
| 	} | |
| 
 | |
| 	// Get grants from stored ACL metadata | |
| 	grants := GetAcpGrants(entry.Extended) | |
| 	if len(grants) > 0 { | |
| 		// Convert AWS SDK grants to local Grant format | |
| 		for _, grant := range grants { | |
| 			localGrant := Grant{ | |
| 				Permission: Permission(*grant.Permission), | |
| 			} | |
| 
 | |
| 			if grant.Grantee != nil { | |
| 				localGrant.Grantee = Grantee{ | |
| 					Type:   *grant.Grantee.Type, | |
| 					XMLXSI: "CanonicalUser", | |
| 					XMLNS:  "http://www.w3.org/2001/XMLSchema-instance", | |
| 				} | |
| 
 | |
| 				if grant.Grantee.ID != nil { | |
| 					localGrant.Grantee.ID = *grant.Grantee.ID | |
| 					localGrant.Grantee.DisplayName = s3a.iam.GetAccountNameById(*grant.Grantee.ID) | |
| 				} | |
| 
 | |
| 				if grant.Grantee.URI != nil { | |
| 					localGrant.Grantee.URI = *grant.Grantee.URI | |
| 				} | |
| 			} | |
| 
 | |
| 			response.AccessControlList.Grant = append(response.AccessControlList.Grant, localGrant) | |
| 		} | |
| 	} else { | |
| 		// Fallback to default full control for object owner | |
| 		response.AccessControlList.Grant = append(response.AccessControlList.Grant, Grant{ | |
| 			Grantee: Grantee{ | |
| 				ID:          objectOwner, | |
| 				DisplayName: objectOwnerDisplayName, | |
| 				Type:        "CanonicalUser", | |
| 				XMLXSI:      "CanonicalUser", | |
| 				XMLNS:       "http://www.w3.org/2001/XMLSchema-instance"}, | |
| 			Permission: Permission(s3_constants.PermissionFullControl), | |
| 		}) | |
| 	} | |
| 
 | |
| 	writeSuccessResponseXML(w, r, response) | |
| } | |
| 
 | |
| // PutObjectAclHandler Put object ACL | |
| // https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html | |
| func (s3a *S3ApiServer) PutObjectAclHandler(w http.ResponseWriter, r *http.Request) { | |
| 	// collect parameters | |
| 	bucket, object := s3_constants.GetBucketAndObject(r) | |
| 	glog.V(3).Infof("PutObjectAclHandler %s %s", bucket, object) | |
| 
 | |
| 	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone { | |
| 		s3err.WriteErrorResponse(w, r, err) | |
| 		return | |
| 	} | |
| 
 | |
| 	// Check for specific version ID in query parameters | |
| 	versionId := r.URL.Query().Get("versionId") | |
| 
 | |
| 	// Check if versioning is configured for the bucket (Enabled or Suspended) | |
| 	versioningConfigured, err := s3a.isVersioningConfigured(bucket) | |
| 	if err != nil { | |
| 		if err == filer_pb.ErrNotFound { | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) | |
| 			return | |
| 		} | |
| 		glog.Errorf("PutObjectAclHandler: Error checking versioning status for bucket %s: %v", bucket, err) | |
| 		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) | |
| 		return | |
| 	} | |
| 
 | |
| 	var entry *filer_pb.Entry | |
| 
 | |
| 	if versioningConfigured { | |
| 		// Handle versioned object ACL modification - use same logic as GetObjectHandler | |
| 		if versionId != "" { | |
| 			// Request for specific version | |
| 			glog.V(2).Infof("PutObjectAclHandler: modifying ACL for specific version %s of %s%s", versionId, bucket, object) | |
| 			entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId) | |
| 		} else { | |
| 			// Request for latest version | |
| 			glog.V(2).Infof("PutObjectAclHandler: modifying ACL for latest version of %s%s", bucket, object) | |
| 			entry, err = s3a.getLatestObjectVersion(bucket, object) | |
| 		} | |
| 
 | |
| 		if err != nil { | |
| 			glog.Errorf("PutObjectAclHandler: Failed to get object version %s for %s%s: %v", versionId, bucket, object, err) | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 			return | |
| 		} | |
| 
 | |
| 		// Check if this is a delete marker | |
| 		if entry.Extended != nil { | |
| 			if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" { | |
| 				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 				return | |
| 			} | |
| 		} | |
| 	} else { | |
| 		// Handle regular (non-versioned) object ACL modification | |
| 		bucketDir := s3a.option.BucketsPath + "/" + bucket | |
| 		entry, err = s3a.getEntry(bucketDir, object) | |
| 		if err != nil { | |
| 			if errors.Is(err, filer_pb.ErrNotFound) { | |
| 				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 				return | |
| 			} | |
| 			glog.Errorf("PutObjectAclHandler: error checking object %s/%s: %v", bucket, object, err) | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) | |
| 			return | |
| 		} | |
| 	} | |
| 
 | |
| 	if entry == nil { | |
| 		s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey) | |
| 		return | |
| 	} | |
| 
 | |
| 	// Get current object owner from metadata | |
| 	var objectOwner string | |
| 	amzAccountId := r.Header.Get(s3_constants.AmzAccountId) | |
| 
 | |
| 	if entry.Extended != nil { | |
| 		if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists { | |
| 			objectOwner = string(ownerBytes) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Fallback to current account if no owner stored | |
| 	if objectOwner == "" { | |
| 		objectOwner = amzAccountId | |
| 	} | |
| 
 | |
| 	// **PERMISSION CHECKS** | |
|  | |
| 	// 1. Check if user is admin (admins can modify any ACL) | |
| 	if !s3a.isUserAdmin(r) { | |
| 		// 2. Check object ownership - only object owner can modify ACL (unless admin) | |
| 		if objectOwner != amzAccountId { | |
| 			glog.V(3).Infof("PutObjectAclHandler: Access denied - user %s is not owner of object %s/%s (owner: %s)", | |
| 				amzAccountId, bucket, object, objectOwner) | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) | |
| 			return | |
| 		} | |
| 
 | |
| 		// 3. Check object-level WRITE_ACP permission | |
| 		// Create the specific action for this object | |
| 		writeAcpAction := Action(fmt.Sprintf("WriteAcp:%s/%s", bucket, object)) | |
| 		identity, errCode := s3a.iam.authRequest(r, writeAcpAction) | |
| 		if errCode != s3err.ErrNone { | |
| 			glog.V(3).Infof("PutObjectAclHandler: Auth failed for WriteAcp action on %s/%s: %v", bucket, object, errCode) | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) | |
| 			return | |
| 		} | |
| 
 | |
| 		// 4. Verify the authenticated identity can perform WriteAcp on this specific object | |
| 		if identity == nil || !identity.canDo(writeAcpAction, bucket, object) { | |
| 			glog.V(3).Infof("PutObjectAclHandler: Identity %v cannot perform WriteAcp on %s/%s", identity, bucket, object) | |
| 			s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) | |
| 			return | |
| 		} | |
| 	} else { | |
| 		glog.V(3).Infof("PutObjectAclHandler: Admin user %s granted ACL modification permission for %s/%s", amzAccountId, bucket, object) | |
| 	} | |
| 
 | |
| 	// Get bucket config for ownership settings | |
| 	bucketConfig, errCode := s3a.getBucketConfig(bucket) | |
| 	if errCode != s3err.ErrNone { | |
| 		s3err.WriteErrorResponse(w, r, errCode) | |
| 		return | |
| 	} | |
| 
 | |
| 	bucketOwnership := bucketConfig.Ownership | |
| 	bucketOwnerId := bucketConfig.Owner | |
| 
 | |
| 	// Extract ACL from request (either canned ACL or XML body) | |
| 	// This function also validates that the owner in the request matches the object owner | |
| 	grants, errCode := ExtractAcl(r, s3a.iam, bucketOwnership, bucketOwnerId, objectOwner, amzAccountId) | |
| 	if errCode != s3err.ErrNone { | |
| 		s3err.WriteErrorResponse(w, r, errCode) | |
| 		return | |
| 	} | |
| 
 | |
| 	// Store ACL in object metadata | |
| 	if errCode := AssembleEntryWithAcp(entry, objectOwner, grants); errCode != s3err.ErrNone { | |
| 		glog.Errorf("PutObjectAclHandler: failed to assemble entry with ACP: %v", errCode) | |
| 		s3err.WriteErrorResponse(w, r, errCode) | |
| 		return | |
| 	} | |
| 
 | |
| 	// Calculate the correct directory for ACL update | |
| 	var updateDirectory string | |
| 
 | |
| 	if versioningConfigured { | |
| 		if versionId != "" && versionId != "null" { | |
| 			// Versioned object - update the specific version file in .versions directory | |
| 			updateDirectory = s3a.option.BucketsPath + "/" + bucket + "/" + object + ".versions" | |
| 		} else { | |
| 			// Latest version in versioned bucket - could be null version or versioned object | |
| 			// Extract version ID from the entry to determine where it's stored | |
| 			var actualVersionId string | |
| 			if entry.Extended != nil { | |
| 				if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists { | |
| 					actualVersionId = string(versionIdBytes) | |
| 				} | |
| 			} | |
| 
 | |
| 			if actualVersionId == "null" || actualVersionId == "" { | |
| 				// Null version (pre-versioning object) - stored as regular file | |
| 				updateDirectory = s3a.option.BucketsPath + "/" + bucket | |
| 			} else { | |
| 				// Versioned object - stored in .versions directory | |
| 				updateDirectory = s3a.option.BucketsPath + "/" + bucket + "/" + object + ".versions" | |
| 			} | |
| 		} | |
| 	} else { | |
| 		// Non-versioned object - stored as regular file | |
| 		updateDirectory = s3a.option.BucketsPath + "/" + bucket | |
| 	} | |
| 
 | |
| 	// Update the object with new ACL metadata | |
| 	err = s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { | |
| 		request := &filer_pb.UpdateEntryRequest{ | |
| 			Directory: updateDirectory, | |
| 			Entry:     entry, | |
| 		} | |
| 
 | |
| 		if _, err := client.UpdateEntry(context.Background(), request); err != nil { | |
| 			return err | |
| 		} | |
| 		return nil | |
| 	}) | |
| 
 | |
| 	if err != nil { | |
| 		glog.Errorf("PutObjectAclHandler: failed to update entry: %v", err) | |
| 		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) | |
| 		return | |
| 	} | |
| 
 | |
| 	glog.V(3).Infof("PutObjectAclHandler: Successfully updated ACL for %s/%s by user %s", bucket, object, amzAccountId) | |
| 	writeSuccessResponseEmpty(w, r) | |
| }
 |