diff --git a/hadoop-hdds/docs/content/security/SecuringS3.md b/hadoop-hdds/docs/content/security/SecuringS3.md
index 21af3734351..e6218b95e91 100644
--- a/hadoop-hdds/docs/content/security/SecuringS3.md
+++ b/hadoop-hdds/docs/content/security/SecuringS3.md
@@ -42,10 +42,10 @@ The user needs to `kinit` first and once they have authenticated via kerberos
ozone s3 getsecret
```
-* Or by sending request to /secret/generate S3 REST endpoint.
+* Or by sending request to /secret S3 REST endpoint.
```bash
-curl -X POST --negotiate -u : https://localhost:9879/secret/generate
+curl -X PUT --negotiate -u : https://localhost:9879/secret
```
This command will talk to ozone, validate the user via Kerberos and generate
diff --git a/hadoop-hdds/docs/content/security/SecuringS3.zh.md b/hadoop-hdds/docs/content/security/SecuringS3.zh.md
index ff27f2de567..218786fd366 100644
--- a/hadoop-hdds/docs/content/security/SecuringS3.zh.md
+++ b/hadoop-hdds/docs/content/security/SecuringS3.zh.md
@@ -36,10 +36,10 @@ icon: cloud
ozone s3 getsecret
```
-* 或者通过向 /secret/generate S3 REST 端点发送请求。
+* 或者通过向 /secret S3 REST 端点发送请求。
```bash
-curl -X POST --negotiate -u : https://localhost:9879/secret/generate
+curl -X PUT --negotiate -u : https://localhost:9879/secret
```
这条命令会与 Ozone 进行通信,对用户进行 Kerberos 认证并生成 AWS 凭据,结果会直接打印在屏幕上,你可以将其配置在 _.aws._ 文件中,这样可以在操作 Ozone S3 桶时自动进行认证。
diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/secretgenerate.robot b/hadoop-ozone/dist/src/main/smoketest/s3/secretgenerate.robot
index 8224d9ac02d..b9f6993f45e 100644
--- a/hadoop-ozone/dist/src/main/smoketest/s3/secretgenerate.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/s3/secretgenerate.robot
@@ -31,7 +31,17 @@ ${ENDPOINT_URL} http://s3g:9878
S3 Gateway Generate Secret
Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit HTTP user
- ${result} = Execute curl -X POST --negotiate -u : -v ${ENDPOINT_URL}/secret/generate
+ ${result} = Execute curl -X PUT --negotiate -u : -v ${ENDPOINT_URL}/secret
+ IF '${SECURITY_ENABLED}' == 'true'
+ Should contain ${result} HTTP/1.1 200 OK ignore_case=True
+ Should Match Regexp ${result}
- * http://www.apache.org/licenses/LICENSE-2.0 - *
- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - * - */ - -package org.apache.hadoop.ozone.s3secret; - -import org.apache.hadoop.ozone.audit.S3GAction; -import org.apache.hadoop.ozone.om.helpers.S3SecretValue; - -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.Response; -import java.io.IOException; - -/** - * Endpoint to generate and return S3 secret. - */ -@Path("/secret/generate") -@S3SecretEnabled -public class S3SecretGenerateEndpoint extends S3SecretEndpointBase { - @POST - public Response generate() throws IOException { - S3SecretResponse s3SecretResponse = new S3SecretResponse(); - S3SecretValue s3SecretValue = generateS3Secret(); - s3SecretResponse.setAwsSecret(s3SecretValue.getAwsSecret()); - s3SecretResponse.setAwsAccessKey(s3SecretValue.getAwsAccessKey()); - AUDIT.logReadSuccess(buildAuditMessageForSuccess( - S3GAction.GENERATE_SECRET, getAuditParameters())); - return Response.ok(s3SecretResponse).build(); - } - - private S3SecretValue generateS3Secret() throws IOException { - return getClient().getObjectStore().getS3Secret(userNameFromRequest()); - } -} diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretRevokeEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretManagementEndpoint.java similarity index 50% rename from hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretRevokeEndpoint.java rename to hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretManagementEndpoint.java index 423790ba92f..3c932da57d7 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretRevokeEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretManagementEndpoint.java @@ -20,31 +20,74 @@ import org.apache.hadoop.ozone.audit.S3GAction; import org.apache.hadoop.ozone.om.exceptions.OMException; +import org.apache.hadoop.ozone.om.helpers.S3SecretValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.POST; +import javax.annotation.Nullable; +import javax.ws.rs.DELETE; +import javax.ws.rs.PUT; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import java.io.IOException; import static javax.ws.rs.core.Response.Status.NOT_FOUND; /** - * Revoke secret endpoint. + * Endpoint to manage S3 secret. */ -@Path("/secret/revoke") +@Path("/secret") @S3SecretEnabled -public class S3SecretRevokeEndpoint extends S3SecretEndpointBase { - +public class S3SecretManagementEndpoint extends S3SecretEndpointBase { private static final Logger LOG = - LoggerFactory.getLogger(S3SecretRevokeEndpoint.class); + LoggerFactory.getLogger(S3SecretManagementEndpoint.class); + + @PUT + public Response generate() throws IOException { + return generateInternal(null); + } + @PUT + @Path("/{username}") + public Response generate(@PathParam("username") String username) + throws IOException { + return generateInternal(username); + } - @POST + private Response generateInternal(@Nullable String username) + throws IOException { + S3SecretResponse s3SecretResponse = new S3SecretResponse(); + S3SecretValue s3SecretValue = generateS3Secret(username); + s3SecretResponse.setAwsSecret(s3SecretValue.getAwsSecret()); + s3SecretResponse.setAwsAccessKey(s3SecretValue.getAwsAccessKey()); + AUDIT.logReadSuccess(buildAuditMessageForSuccess( + S3GAction.GENERATE_SECRET, getAuditParameters())); + return Response.ok(s3SecretResponse).build(); + } + + private S3SecretValue generateS3Secret(@Nullable String username) + throws IOException { + String actualUsername = username == null ? userNameFromRequest() : username; + return getClient().getObjectStore().getS3Secret(actualUsername); + } + + @DELETE public Response revoke() throws IOException { + return revokeInternal(null); + } + + @DELETE + @Path("/{username}") + public Response revoke(@PathParam("username") String username) + throws IOException { + return revokeInternal(username); + } + + private Response revokeInternal(@Nullable String username) + throws IOException { try { - revokeSecret(); + revokeSecret(username); AUDIT.logWriteSuccess(buildAuditMessageForSuccess( S3GAction.REVOKE_SECRET, getAuditParameters())); return Response.ok().build(); @@ -62,8 +105,8 @@ public Response revoke() throws IOException { } } - private void revokeSecret() throws IOException { - getClient().getObjectStore().revokeS3Secret(userNameFromRequest()); + private void revokeSecret(@Nullable String username) throws IOException { + String actualUsername = username == null ? userNameFromRequest() : username; + getClient().getObjectStore().revokeS3Secret(actualUsername); } - } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretGenerate.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretGenerate.java index 6a8b63e83a9..f3c17d5807e 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretGenerate.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretGenerate.java @@ -35,10 +35,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; /** @@ -47,9 +48,10 @@ @ExtendWith(MockitoExtension.class) public class TestSecretGenerate { private static final String USER_NAME = "test"; + private static final String OTHER_USER_NAME = "test2"; private static final String USER_SECRET = "test_secret"; - private S3SecretGenerateEndpoint endpoint; + private S3SecretManagementEndpoint endpoint; @Mock private ClientProtocol proxy; @@ -62,31 +64,43 @@ public class TestSecretGenerate { @Mock private Principal principal; + private static S3SecretValue getS3SecretValue(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + return new S3SecretValue((String) args[0], USER_SECRET); + } + @BeforeEach void setUp() throws IOException { - S3SecretValue value = new S3SecretValue(USER_NAME, USER_SECRET); - when(proxy.getS3Secret(eq(USER_NAME))).thenReturn(value); + when(proxy.getS3Secret(any())).then(TestSecretGenerate::getS3SecretValue); OzoneConfiguration conf = new OzoneConfiguration(); OzoneClient client = new OzoneClientStub(new ObjectStoreStub(conf, proxy)); - when(principal.getName()).thenReturn(USER_NAME); - when(securityContext.getUserPrincipal()).thenReturn(principal); - when(context.getSecurityContext()).thenReturn(securityContext); - when(uriInfo.getPathParameters()).thenReturn(new MultivaluedHashMap<>()); when(uriInfo.getQueryParameters()).thenReturn(new MultivaluedHashMap<>()); when(context.getUriInfo()).thenReturn(uriInfo); - endpoint = new S3SecretGenerateEndpoint(); + endpoint = new S3SecretManagementEndpoint(); endpoint.setClient(client); endpoint.setContext(context); } @Test void testSecretGenerate() throws IOException { + when(principal.getName()).thenReturn(USER_NAME); + when(securityContext.getUserPrincipal()).thenReturn(principal); + when(context.getSecurityContext()).thenReturn(securityContext); + S3SecretResponse response = (S3SecretResponse) endpoint.generate().getEntity(); assertEquals(USER_SECRET, response.getAwsSecret()); assertEquals(USER_NAME, response.getAwsAccessKey()); } + + @Test + void testSecretGenerateWithUsername() throws IOException { + S3SecretResponse response = + (S3SecretResponse) endpoint.generate(OTHER_USER_NAME).getEntity(); + assertEquals(USER_SECRET, response.getAwsSecret()); + assertEquals(OTHER_USER_NAME, response.getAwsAccessKey()); + } } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretRevoke.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretRevoke.java index 8a1f81d1329..a319496419d 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretRevoke.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretRevoke.java @@ -55,8 +55,9 @@ @ExtendWith(MockitoExtension.class) public class TestSecretRevoke { private static final String USER_NAME = "test"; + private static final String OTHER_USER_NAME = "test2"; - private S3SecretRevokeEndpoint endpoint; + private S3SecretManagementEndpoint endpoint; @Mock private ObjectStoreStub objectStore; @@ -73,27 +74,38 @@ public class TestSecretRevoke { void setUp() { OzoneClient client = new OzoneClientStub(objectStore); - when(principal.getName()).thenReturn(USER_NAME); - when(securityContext.getUserPrincipal()).thenReturn(principal); - when(context.getSecurityContext()).thenReturn(securityContext); - when(uriInfo.getPathParameters()).thenReturn(new MultivaluedHashMap<>()); when(uriInfo.getQueryParameters()).thenReturn(new MultivaluedHashMap<>()); when(context.getUriInfo()).thenReturn(uriInfo); - endpoint = new S3SecretRevokeEndpoint(); + endpoint = new S3SecretManagementEndpoint(); endpoint.setClient(client); endpoint.setContext(context); } + private void mockSecurityContext() { + when(principal.getName()).thenReturn(USER_NAME); + when(securityContext.getUserPrincipal()).thenReturn(principal); + when(context.getSecurityContext()).thenReturn(securityContext); + } + @Test void testSecretRevoke() throws IOException { + mockSecurityContext(); endpoint.revoke(); verify(objectStore, times(1)).revokeS3Secret(eq(USER_NAME)); } + @Test + void testSecretRevokeWithUsername() throws IOException { + endpoint.revoke(OTHER_USER_NAME); + verify(objectStore, times(1)) + .revokeS3Secret(eq(OTHER_USER_NAME)); + } + @Test void testSecretSequentialRevokes() throws IOException { + mockSecurityContext(); Response firstResponse = endpoint.revoke(); assertEquals(OK.getStatusCode(), firstResponse.getStatus()); doThrow(new OMException(S3_SECRET_NOT_FOUND)) @@ -104,6 +116,7 @@ void testSecretSequentialRevokes() throws IOException { @Test void testSecretRevokesHandlesException() throws IOException { + mockSecurityContext(); doThrow(new OMException(ACCESS_DENIED)) .when(objectStore).revokeS3Secret(any()); Response response = endpoint.revoke();