Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HDDS-9203. Allow generating/revoking S3 secret for other users via REST #5233

Merged
merged 3 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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