Skip to content

Commit

Permalink
HDDS-9203. Allow generating/revoking S3 secret for other users via RE…
Browse files Browse the repository at this point in the history
…ST (#5233)
  • Loading branch information
ivanzlenko authored Sep 6, 2023
1 parent 7ac0465 commit 4ee6c07
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 81 deletions.
4 changes: 2 additions & 2 deletions hadoop-hdds/docs/content/security/SecuringS3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions hadoop-hdds/docs/content/security/SecuringS3.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 桶时自动进行认证。
Expand Down
12 changes: 11 additions & 1 deletion hadoop-ozone/dist/src/main/smoketest/s3/secretgenerate.robot
Original file line number Diff line number Diff line change
Expand Up @@ -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} <awsAccessKey>.*</awsAccessKey><awsSecret>.*</awsSecret>
ELSE
Should contain ${result} S3 Secret endpoint is disabled.
END

S3 Gateway Generate Secret By Username
Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit test user testuser testuser.keytab
${result} = Execute curl -X PUT --negotiate -u : -v ${ENDPOINT_URL}/secret/testuser2
IF '${SECURITY_ENABLED}' == 'true'
Should contain ${result} HTTP/1.1 200 OK ignore_case=True
Should Match Regexp ${result} <awsAccessKey>.*</awsAccessKey><awsSecret>.*</awsSecret>
Expand Down
11 changes: 10 additions & 1 deletion hadoop-ozone/dist/src/main/smoketest/s3/secretrevoke.robot
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ ${SECURITY_ENABLED} true

S3 Gateway Revoke Secret
Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit HTTP user
${result} = Execute curl -X POST --negotiate -u : -v ${ENDPOINT_URL}/secret/revoke
${result} = Execute curl -X DELETE --negotiate -u : -v ${ENDPOINT_URL}/secret
IF '${SECURITY_ENABLED}' == 'true'
Should contain ${result} HTTP/1.1 200 OK ignore_case=True
ELSE
Should contain ${result} S3 Secret endpoint is disabled.
END

S3 Gateway Revoke Secret By Username
Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit test user testuser testuser.keytab
${result} = Execute curl -X DELETE --negotiate -u : -v ${ENDPOINT_URL}/secret/testuser2
IF '${SECURITY_ENABLED}' == 'true'
Should contain ${result} HTTP/1.1 200 OK ignore_case=True
ELSE
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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))
Expand All @@ -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();
Expand Down

0 comments on commit 4ee6c07

Please sign in to comment.