Skip to content

Commit 75aabae

Browse files
authored
HDDS-14123. Extract BucketAclHandler from BucketEndpoint#Put (#9516)
1 parent 10ea33b commit 75aabae

8 files changed

Lines changed: 623 additions & 187 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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

Comments
 (0)