|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | + * contributor license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright ownership. |
| 5 | + * The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | + * (the "License"); you may not use this file except in compliance with |
| 7 | + * the License. You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | + |
| 18 | +package org.apache.hadoop.ozone.s3.endpoint; |
| 19 | + |
| 20 | +import static org.apache.hadoop.ozone.OzoneAcl.AclScope.ACCESS; |
| 21 | +import static org.apache.hadoop.ozone.OzoneAcl.AclScope.DEFAULT; |
| 22 | +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NOT_IMPLEMENTED; |
| 23 | +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.newError; |
| 24 | +import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType.USER; |
| 25 | + |
| 26 | +import java.io.IOException; |
| 27 | +import java.io.InputStream; |
| 28 | +import java.util.ArrayList; |
| 29 | +import java.util.EnumSet; |
| 30 | +import java.util.List; |
| 31 | +import javax.annotation.PostConstruct; |
| 32 | +import javax.ws.rs.core.Response; |
| 33 | +import org.apache.commons.lang3.StringUtils; |
| 34 | +import org.apache.hadoop.ozone.OzoneAcl; |
| 35 | +import org.apache.hadoop.ozone.audit.S3GAction; |
| 36 | +import org.apache.hadoop.ozone.client.OzoneBucket; |
| 37 | +import org.apache.hadoop.ozone.client.OzoneVolume; |
| 38 | +import org.apache.hadoop.ozone.om.exceptions.OMException; |
| 39 | +import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes; |
| 40 | +import org.apache.hadoop.ozone.om.helpers.OzoneAclUtil; |
| 41 | +import org.apache.hadoop.ozone.s3.exception.OS3Exception; |
| 42 | +import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; |
| 43 | +import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams; |
| 44 | +import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer; |
| 45 | +import org.apache.hadoop.util.Time; |
| 46 | +import org.apache.http.HttpStatus; |
| 47 | +import org.slf4j.Logger; |
| 48 | +import org.slf4j.LoggerFactory; |
| 49 | + |
| 50 | +/** |
| 51 | + * Handler for bucket ACL operations (?acl query parameter). |
| 52 | + * Implements PUT operations for bucket Access Control Lists. |
| 53 | + * |
| 54 | + * This handler extends EndpointBase to inherit all required functionality |
| 55 | + * (configuration, headers, request context, audit logging, metrics, etc.). |
| 56 | + */ |
| 57 | +public class BucketAclHandler extends EndpointBase implements BucketOperationHandler { |
| 58 | + |
| 59 | + private static final Logger LOG = LoggerFactory.getLogger(BucketAclHandler.class); |
| 60 | + |
| 61 | + /** |
| 62 | + * Determine if this handler should handle the current request. |
| 63 | + * @return true if the request has the "acl" query parameter |
| 64 | + */ |
| 65 | + private boolean shouldHandle() { |
| 66 | + return queryParams().get(QueryParams.ACL) != null; |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Implement acl put. |
| 71 | + * <p> |
| 72 | + * see: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketAcl.html |
| 73 | + */ |
| 74 | + @Override |
| 75 | + public Response handlePutRequest(String bucketName, InputStream body) |
| 76 | + throws IOException, OS3Exception { |
| 77 | + |
| 78 | + if (!shouldHandle()) { |
| 79 | + return null; // Not responsible for this request |
| 80 | + } |
| 81 | + |
| 82 | + long startNanos = Time.monotonicNowNanos(); |
| 83 | + S3GAction s3GAction = S3GAction.PUT_ACL; |
| 84 | + |
| 85 | + String grantReads = getHeaders().getHeaderString(S3Acl.GRANT_READ); |
| 86 | + String grantWrites = getHeaders().getHeaderString(S3Acl.GRANT_WRITE); |
| 87 | + String grantReadACP = getHeaders().getHeaderString(S3Acl.GRANT_READ_ACP); |
| 88 | + String grantWriteACP = getHeaders().getHeaderString(S3Acl.GRANT_WRITE_ACP); |
| 89 | + String grantFull = getHeaders().getHeaderString(S3Acl.GRANT_FULL_CONTROL); |
| 90 | + |
| 91 | + try { |
| 92 | + OzoneBucket bucket = getBucket(bucketName); |
| 93 | + S3Owner.verifyBucketOwnerCondition(getHeaders(), bucketName, bucket.getOwner()); |
| 94 | + OzoneVolume volume = getVolume(); |
| 95 | + |
| 96 | + List<OzoneAcl> ozoneAclListOnBucket = new ArrayList<>(); |
| 97 | + List<OzoneAcl> ozoneAclListOnVolume = new ArrayList<>(); |
| 98 | + |
| 99 | + if (grantReads == null && grantWrites == null && grantReadACP == null |
| 100 | + && grantWriteACP == null && grantFull == null) { |
| 101 | + // Handle grants in body |
| 102 | + S3BucketAcl putBucketAclRequest = |
| 103 | + new PutBucketAclRequestUnmarshaller().readFrom(body); |
| 104 | + ozoneAclListOnBucket.addAll( |
| 105 | + S3Acl.s3AclToOzoneNativeAclOnBucket(putBucketAclRequest)); |
| 106 | + ozoneAclListOnVolume.addAll( |
| 107 | + S3Acl.s3AclToOzoneNativeAclOnVolume(putBucketAclRequest)); |
| 108 | + } else { |
| 109 | + // Handle grants in headers |
| 110 | + if (grantReads != null) { |
| 111 | + ozoneAclListOnBucket.addAll(getAndConvertAclOnBucket(grantReads, |
| 112 | + S3Acl.ACLType.READ.getValue())); |
| 113 | + ozoneAclListOnVolume.addAll(getAndConvertAclOnVolume(grantReads, |
| 114 | + S3Acl.ACLType.READ.getValue())); |
| 115 | + } |
| 116 | + if (grantWrites != null) { |
| 117 | + ozoneAclListOnBucket.addAll(getAndConvertAclOnBucket(grantWrites, |
| 118 | + S3Acl.ACLType.WRITE.getValue())); |
| 119 | + ozoneAclListOnVolume.addAll(getAndConvertAclOnVolume(grantWrites, |
| 120 | + S3Acl.ACLType.WRITE.getValue())); |
| 121 | + } |
| 122 | + if (grantReadACP != null) { |
| 123 | + ozoneAclListOnBucket.addAll(getAndConvertAclOnBucket(grantReadACP, |
| 124 | + S3Acl.ACLType.READ_ACP.getValue())); |
| 125 | + ozoneAclListOnVolume.addAll(getAndConvertAclOnVolume(grantReadACP, |
| 126 | + S3Acl.ACLType.READ_ACP.getValue())); |
| 127 | + } |
| 128 | + if (grantWriteACP != null) { |
| 129 | + ozoneAclListOnBucket.addAll(getAndConvertAclOnBucket(grantWriteACP, |
| 130 | + S3Acl.ACLType.WRITE_ACP.getValue())); |
| 131 | + ozoneAclListOnVolume.addAll(getAndConvertAclOnVolume(grantWriteACP, |
| 132 | + S3Acl.ACLType.WRITE_ACP.getValue())); |
| 133 | + } |
| 134 | + if (grantFull != null) { |
| 135 | + ozoneAclListOnBucket.addAll(getAndConvertAclOnBucket(grantFull, |
| 136 | + S3Acl.ACLType.FULL_CONTROL.getValue())); |
| 137 | + ozoneAclListOnVolume.addAll(getAndConvertAclOnVolume(grantFull, |
| 138 | + S3Acl.ACLType.FULL_CONTROL.getValue())); |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + // A put request will reset all previous ACLs on bucket |
| 143 | + bucket.setAcl(ozoneAclListOnBucket); |
| 144 | + |
| 145 | + // A put request will reset input user/group's permission on volume |
| 146 | + List<OzoneAcl> acls = bucket.getAcls(); |
| 147 | + List<OzoneAcl> aclsToRemoveOnVolume = new ArrayList<>(); |
| 148 | + List<OzoneAcl> currentAclsOnVolume = volume.getAcls(); |
| 149 | + |
| 150 | + // Remove input user/group's permission from Volume first |
| 151 | + if (!currentAclsOnVolume.isEmpty()) { |
| 152 | + for (OzoneAcl acl : acls) { |
| 153 | + if (acl.getAclScope() == ACCESS) { |
| 154 | + aclsToRemoveOnVolume.addAll(OzoneAclUtil.filterAclList( |
| 155 | + acl.getName(), acl.getType(), currentAclsOnVolume)); |
| 156 | + } |
| 157 | + } |
| 158 | + for (OzoneAcl acl : aclsToRemoveOnVolume) { |
| 159 | + volume.removeAcl(acl); |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + // Add new permission on Volume |
| 164 | + for (OzoneAcl acl : ozoneAclListOnVolume) { |
| 165 | + volume.addAcl(acl); |
| 166 | + } |
| 167 | + |
| 168 | + getMetrics().updatePutAclSuccessStats(startNanos); |
| 169 | + auditWriteSuccess(s3GAction); |
| 170 | + return Response.status(HttpStatus.SC_OK).build(); |
| 171 | + |
| 172 | + } catch (OMException exception) { |
| 173 | + getMetrics().updatePutAclFailureStats(startNanos); |
| 174 | + auditWriteFailure(s3GAction, exception); |
| 175 | + if (exception.getResult() == ResultCodes.BUCKET_NOT_FOUND) { |
| 176 | + throw newError(S3ErrorTable.NO_SUCH_BUCKET, bucketName, exception); |
| 177 | + } else if (isAccessDenied(exception)) { |
| 178 | + throw newError(S3ErrorTable.ACCESS_DENIED, bucketName, exception); |
| 179 | + } |
| 180 | + throw exception; |
| 181 | + } catch (OS3Exception ex) { |
| 182 | + getMetrics().updatePutAclFailureStats(startNanos); |
| 183 | + auditWriteFailure(s3GAction, ex); |
| 184 | + throw ex; |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + /** |
| 189 | + * Convert ACL string to Ozone ACL on bucket. |
| 190 | + * |
| 191 | + * Example: x-amz-grant-write: id="111122223333", id="555566667777" |
| 192 | + */ |
| 193 | + private List<OzoneAcl> getAndConvertAclOnBucket( |
| 194 | + String value, String permission) throws OS3Exception { |
| 195 | + return parseAndConvertAcl(value, permission, true); |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * Convert ACL string to Ozone ACL on volume. |
| 200 | + */ |
| 201 | + private List<OzoneAcl> getAndConvertAclOnVolume( |
| 202 | + String value, String permission) throws OS3Exception { |
| 203 | + return parseAndConvertAcl(value, permission, false); |
| 204 | + } |
| 205 | + |
| 206 | + /** |
| 207 | + * Parse ACL string and convert to Ozone ACLs. |
| 208 | + * |
| 209 | + * This is a common method extracted from getAndConvertAclOnBucket and |
| 210 | + * getAndConvertAclOnVolume to reduce code duplication. |
| 211 | + * |
| 212 | + * @param value the ACL header value (e.g., "id=\"user1\",id=\"user2\"") |
| 213 | + * @param permission the S3 permission type (READ, WRITE, etc.) |
| 214 | + * @param isBucket true for bucket ACL, false for volume ACL |
| 215 | + * @return list of OzoneAcl objects |
| 216 | + * @throws OS3Exception if parsing fails or grantee type is not supported |
| 217 | + */ |
| 218 | + private List<OzoneAcl> parseAndConvertAcl( |
| 219 | + String value, String permission, boolean isBucket) throws OS3Exception { |
| 220 | + List<OzoneAcl> ozoneAclList = new ArrayList<>(); |
| 221 | + if (StringUtils.isEmpty(value)) { |
| 222 | + return ozoneAclList; |
| 223 | + } |
| 224 | + |
| 225 | + String[] subValues = value.split(","); |
| 226 | + for (String acl : subValues) { |
| 227 | + String[] part = acl.split("="); |
| 228 | + if (part.length != 2) { |
| 229 | + throw newError(S3ErrorTable.INVALID_ARGUMENT, acl); |
| 230 | + } |
| 231 | + |
| 232 | + S3Acl.ACLIdentityType type = |
| 233 | + S3Acl.ACLIdentityType.getTypeFromHeaderType(part[0]); |
| 234 | + if (type == null || !type.isSupported()) { |
| 235 | + LOG.warn("S3 grantee {} is null or not supported", part[0]); |
| 236 | + throw newError(NOT_IMPLEMENTED, part[0]); |
| 237 | + } |
| 238 | + |
| 239 | + String userId = part[1]; |
| 240 | + |
| 241 | + if (isBucket) { |
| 242 | + // Build ACL on Bucket |
| 243 | + EnumSet<IAccessAuthorizer.ACLType> aclsOnBucket = |
| 244 | + S3Acl.getOzoneAclOnBucketFromS3Permission(permission); |
| 245 | + ozoneAclList.add(OzoneAcl.of(USER, userId, DEFAULT, aclsOnBucket)); |
| 246 | + ozoneAclList.add(OzoneAcl.of(USER, userId, ACCESS, aclsOnBucket)); |
| 247 | + } else { |
| 248 | + // Build ACL on Volume |
| 249 | + EnumSet<IAccessAuthorizer.ACLType> aclsOnVolume = |
| 250 | + S3Acl.getOzoneAclOnVolumeFromS3Permission(permission); |
| 251 | + ozoneAclList.add(OzoneAcl.of(USER, userId, ACCESS, aclsOnVolume)); |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + return ozoneAclList; |
| 256 | + } |
| 257 | + |
| 258 | + @Override |
| 259 | + @PostConstruct |
| 260 | + public void init() { |
| 261 | + // No initialization needed for BucketAclHandler |
| 262 | + } |
| 263 | +} |
0 commit comments