From 9e26b15dd8d00150bed000afa9a2519e155236d2 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Fri, 27 Oct 2023 12:08:24 -0400 Subject: [PATCH 01/23] OnBehalfOf tokens odds and ends (#3593) I was doing some inspection around the OBO feature and noticed some items to clean up. Signed-off-by: Peter Nied --- .../security/DefaultConfigurationTests.java | 2 +- .../security/SecurityConfigurationTests.java | 12 ++-- .../http/CertificateAuthenticationTest.java | 4 +- .../http/OnBehalfOfJwtAuthenticationTest.java | 50 +++++++------ .../framework/cluster/TestRestClient.java | 24 +------ .../onbehalf/CreateOnBehalfOfTokenAction.java | 70 +++++++++---------- .../security/authtoken/jwt/JwtVendor.java | 4 +- .../security/authtoken/jwt/JwtVendorTest.java | 68 +++++++++--------- 8 files changed, 105 insertions(+), 129 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java index a9f6cf9b1e..043d3908e9 100644 --- a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java @@ -68,7 +68,7 @@ public void shouldLoadDefaultConfiguration() { Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); } try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.assertCorrectCredentials(ADMIN_USER_NAME); + client.confirmCorrectCredentials(ADMIN_USER_NAME); HttpResponse response = client.get("/_plugins/_security/api/internalusers"); response.assertStatusCode(200); Map users = response.getBodyAs(Map.class); diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java index b35495e23e..cc95f191f7 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java @@ -97,10 +97,10 @@ public void shouldCreateUserViaRestApi_success() { assertThat(httpResponse.getStatusCode(), equalTo(201)); } try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - client.assertCorrectCredentials(USER_ADMIN.getName()); + client.confirmCorrectCredentials(USER_ADMIN.getName()); } try (TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_1, ADDITIONAL_PASSWORD_1)) { - client.assertCorrectCredentials(ADDITIONAL_USER_1); + client.confirmCorrectCredentials(ADDITIONAL_USER_1); } } @@ -160,10 +160,10 @@ public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_pos httpResponse.assertStatusCode(201); } try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - client.assertCorrectCredentials(USER_ADMIN.getName()); + client.confirmCorrectCredentials(USER_ADMIN.getName()); } try (TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_2, ADDITIONAL_PASSWORD_2)) { - client.assertCorrectCredentials(ADDITIONAL_USER_2); + client.confirmCorrectCredentials(ADDITIONAL_USER_2); } } @@ -189,10 +189,10 @@ public void shouldStillWorkAfterUpdateOfSecurityConfig() { cluster.updateUserConfiguration(users); try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - client.assertCorrectCredentials(USER_ADMIN.getName()); + client.confirmCorrectCredentials(USER_ADMIN.getName()); } try (TestRestClient client = cluster.getRestClient(newUser)) { - client.assertCorrectCredentials(newUser.getName()); + client.confirmCorrectCredentials(newUser.getName()); } } diff --git a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java index 7c4d05b714..975ce25efb 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java @@ -89,7 +89,7 @@ public void shouldAuthenticateUserWithCertificate_positiveUserSpoke() { CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_SPOCK); try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { - client.assertCorrectCredentials(USER_SPOCK); + client.confirmCorrectCredentials(USER_SPOCK); } } @@ -98,7 +98,7 @@ public void shouldAuthenticateUserWithCertificate_positiveUserKirk() { CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_KIRK); try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { - client.assertCorrectCredentials(USER_KIRK); + client.confirmCorrectCredentials(USER_KIRK); } } diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 123fb5c770..45fb39d362 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -16,6 +16,7 @@ import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -24,7 +25,6 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; -import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,11 +38,10 @@ import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.aMapWithSize; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasKey; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.contains; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; @@ -56,6 +55,7 @@ public class OnBehalfOfJwtAuthenticationTest { static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + private static final String CREATE_OBO_TOKEN_PATH = "_plugins/_security/api/generateonbehalfoftoken"; private static Boolean oboEnabled = true; private static final String signingKey = Base64.getEncoder() .encodeToString( @@ -139,7 +139,7 @@ public void shouldNotAuthenticateForUsingOBOTokenToAccessOBOEndpoint() { Header adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + oboToken); try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { - TestRestClient.HttpResponse response = client.getOnBehalfOfToken(OBO_DESCRIPTION, adminOboAuthHeader); + TestRestClient.HttpResponse response = client.postJson(CREATE_OBO_TOKEN_PATH, OBO_DESCRIPTION); response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); } } @@ -150,7 +150,7 @@ public void shouldNotAuthenticateForUsingOBOTokenToAccessAccountEndpoint() { Header adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + oboToken); try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { - TestRestClient.HttpResponse response = client.changeInternalUserPassword(CURRENT_AND_NEW_PASSWORDS, adminOboAuthHeader); + TestRestClient.HttpResponse response = client.putJson("_plugins/_security/api/account", CURRENT_AND_NEW_PASSWORDS); response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); } } @@ -173,52 +173,50 @@ public void shouldNotAuthenticateForNonAdminUserWithoutOBOPermission() { public void shouldNotIncludeRolesFromHostMappingInOBOToken() { String oboToken = generateOboToken(OBO_USER_NAME_WITH_HOST_MAPPING, DEFAULT_PASSWORD); - Claims claims = Jwts.parserBuilder().setSigningKey(signingKey).build().parseClaimsJws(oboToken).getBody(); + Claims claims = Jwts.parserBuilder() + .setSigningKey(Base64.getDecoder().decode(signingKey)) + .build() + .parseClaimsJws(oboToken) + .getBody(); Object er = claims.get("er"); EncryptionDecryptionUtil encryptionDecryptionUtil = new EncryptionDecryptionUtil(encryptionKey); String rolesClaim = encryptionDecryptionUtil.decrypt(er.toString()); - List roles = Arrays.stream(rolesClaim.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toUnmodifiableList()); + Set roles = Arrays.stream(rolesClaim.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toSet()); - Assert.assertFalse(roles.contains("host_mapping_role")); + assertThat(roles, equalTo(HOST_MAPPING_OBO_USER.getRoleNames())); + assertThat(roles, not(contains("host_mapping_role"))); } @Test public void shouldNotAuthenticateWithInvalidDurationSeconds() { try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.assertCorrectCredentials(ADMIN_USER_NAME); + client.confirmCorrectCredentials(ADMIN_USER_NAME); TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_DESCRIPTION_WITH_INVALID_DURATIONSECONDS); response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); - Map oboEndPointResponse = (Map) response.getBodyAs(Map.class); - assertTrue(oboEndPointResponse.containsValue("durationSeconds must be an integer.")); + assertThat(response.getTextFromJsonBody("/error"), equalTo("durationSeconds must be an integer.")); } } @Test public void shouldNotAuthenticateWithInvalidAPIParameter() { try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.assertCorrectCredentials(ADMIN_USER_NAME); + client.confirmCorrectCredentials(ADMIN_USER_NAME); TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_DESCRIPTION_WITH_INVALID_PARAMETERS); response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); - Map oboEndPointResponse = (Map) response.getBodyAs(Map.class); - assertTrue(oboEndPointResponse.containsValue("Unrecognized parameter: invalidParameter")); + assertThat(response.getTextFromJsonBody("/error"), equalTo("Unrecognized parameter: invalidParameter")); } } private String generateOboToken(String username, String password) { try (TestRestClient client = cluster.getRestClient(username, password)) { - client.assertCorrectCredentials(username); + client.confirmCorrectCredentials(username); TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); response.assertStatusCode(HttpStatus.SC_OK); - Map oboEndPointResponse = (Map) response.getBodyAs(Map.class); - assertThat( - oboEndPointResponse, - allOf(aMapWithSize(3), hasKey("user"), hasKey("authenticationToken"), hasKey("durationSeconds")) - ); - return oboEndPointResponse.get("authenticationToken").toString(); + assertThat(response.getTextFromJsonBody("/user"), notNullValue()); + assertThat(response.getTextFromJsonBody("/authenticationToken"), notNullValue()); + assertThat(response.getTextFromJsonBody("/durationSeconds"), notNullValue()); + return response.getTextFromJsonBody("/authenticationToken").toString(); } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index afdce66679..55919d814c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -125,29 +125,7 @@ public HttpResponse getAuthInfo(Header... headers) { return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers); } - public HttpResponse getOnBehalfOfToken(String jsonData, Header... headers) { - try { - HttpPost httpPost = new HttpPost( - new URIBuilder(getHttpServerUri() + "/_plugins/_security/api/generateonbehalfoftoken?pretty").build() - ); - httpPost.setEntity(new StringEntity(jsonData)); - return executeRequest(httpPost, mergeHeaders(CONTENT_TYPE_JSON, headers)); - } catch (URISyntaxException ex) { - throw new RuntimeException("Incorrect URI syntax", ex); - } - } - - public HttpResponse changeInternalUserPassword(String jsonData, Header... headers) { - try { - HttpPut httpPut = new HttpPut(new URIBuilder(getHttpServerUri() + "/_plugins/_security/api/account?pretty").build()); - httpPut.setEntity(new StringEntity(jsonData)); - return executeRequest(httpPut, mergeHeaders(CONTENT_TYPE_JSON, headers)); - } catch (URISyntaxException ex) { - throw new RuntimeException("Incorrect URI syntax", ex); - } - } - - public void assertCorrectCredentials(String expectedUserName) { + public void confirmCorrectCredentials(String expectedUserName) { HttpResponse response = getAuthInfo(); assertThat(response, notNullValue()); response.assertStatusCode(200); diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java index fe58e2adb1..a885a42ab2 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -13,7 +13,6 @@ import java.io.IOException; import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -58,8 +57,6 @@ public class CreateOnBehalfOfTokenAction extends BaseRestHandler { private ConfigModel configModel; - private DynamicConfigModel dcm; - public static final Integer OBO_DEFAULT_EXPIRY_SECONDS = 5 * 60; public static final Integer OBO_MAX_EXPIRY_SECONDS = 10 * 60; @@ -67,24 +64,18 @@ public class CreateOnBehalfOfTokenAction extends BaseRestHandler { protected final Logger log = LogManager.getLogger(this.getClass()); - private static final Set RECOGNIZED_PARAMS = new HashSet<>( - Arrays.asList("durationSeconds", "description", "roleSecurityMode", "service") - ); - @Subscribe - public void onConfigModelChanged(ConfigModel configModel) { + public void onConfigModelChanged(final ConfigModel configModel) { this.configModel = configModel; } @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - this.dcm = dcm; + public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { + final Settings settings = dcm.getDynamicOnBehalfOfSettings(); - Settings settings = dcm.getDynamicOnBehalfOfSettings(); - - Boolean enabled = Boolean.parseBoolean(settings.get("enabled")); - String signingKey = settings.get("signing_key"); - String encryptionKey = settings.get("encryption_key"); + final Boolean enabled = Boolean.parseBoolean(settings.get("enabled")); + final String signingKey = settings.get("signing_key"); + final String encryptionKey = settings.get("encryption_key"); if (!Boolean.FALSE.equals(enabled) && signingKey != null && encryptionKey != null) { this.vendor = new JwtVendor(settings, Optional.empty()); @@ -109,7 +100,7 @@ public List routes() { } @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { switch (request.method()) { case POST: return handlePost(request, client); @@ -118,10 +109,10 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } } - private RestChannelConsumer handlePost(RestRequest request, NodeClient client) throws IOException { + private RestChannelConsumer handlePost(final RestRequest request, final NodeClient client) throws IOException { return new RestChannelConsumer() { @Override - public void accept(RestChannel channel) throws Exception { + public void accept(final RestChannel channel) throws Exception { final XContentBuilder builder = channel.newBuilder(); BytesRestResponse response; try { @@ -141,18 +132,14 @@ public void accept(RestChannel channel) throws Exception { validateRequestParameters(requestBody); - Integer tokenDuration = parseAndValidateDurationSeconds(requestBody.get("durationSeconds")); + Integer tokenDuration = parseAndValidateDurationSeconds(requestBody.get(InputParameters.DURATION.paramName)); tokenDuration = Math.min(tokenDuration, OBO_MAX_EXPIRY_SECONDS); - final String description = (String) requestBody.getOrDefault("description", null); - - final Boolean roleSecurityMode = Optional.ofNullable(requestBody.get("roleSecurityMode")) - .map(value -> (Boolean) value) - .orElse(true); // Default to false if null + final String description = (String) requestBody.getOrDefault(InputParameters.DESCRIPTION.paramName, null); - final String service = (String) requestBody.getOrDefault("service", DEFAULT_SERVICE); + final String service = (String) requestBody.getOrDefault(InputParameters.SERVICE.paramName, DEFAULT_SERVICE); final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - Set mappedRoles = mapRoles(user); + final Set mappedRoles = mapRoles(user); builder.startObject(); builder.field("user", user.getName()); @@ -164,14 +151,14 @@ public void accept(RestChannel channel) throws Exception { tokenDuration, mappedRoles.stream().collect(Collectors.toList()), user.getRoles().stream().collect(Collectors.toList()), - roleSecurityMode + false ); builder.field("authenticationToken", token); builder.field("durationSeconds", tokenDuration); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); - } catch (IllegalArgumentException iae) { + } catch (final IllegalArgumentException iae) { builder.startObject().field("error", iae.getMessage()).endObject(); response = new BytesRestResponse(RestStatus.BAD_REQUEST, builder); } catch (final Exception exception) { @@ -187,19 +174,32 @@ public void accept(RestChannel channel) throws Exception { }; } + private enum InputParameters { + DURATION("durationSeconds"), + DESCRIPTION("description"), + SERVICE("service"); + + final String paramName; + + private InputParameters(final String paramName) { + this.paramName = paramName; + } + } + private Set mapRoles(final User user) { return this.configModel.mapSecurityRoles(user, null); } - private void validateRequestParameters(Map requestBody) throws IllegalArgumentException { - for (String key : requestBody.keySet()) { - if (!RECOGNIZED_PARAMS.contains(key)) { - throw new IllegalArgumentException("Unrecognized parameter: " + key); - } + private void validateRequestParameters(final Map requestBody) throws IllegalArgumentException { + for (final String key : requestBody.keySet()) { + Arrays.stream(InputParameters.values()) + .filter(param -> param.paramName.equalsIgnoreCase(key)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Unrecognized parameter: " + key)); } } - private Integer parseAndValidateDurationSeconds(Object durationObj) throws IllegalArgumentException { + private Integer parseAndValidateDurationSeconds(final Object durationObj) throws IllegalArgumentException { if (durationObj == null) { return OBO_DEFAULT_EXPIRY_SECONDS; } @@ -209,7 +209,7 @@ private Integer parseAndValidateDurationSeconds(Object durationObj) throws Illeg } else if (durationObj instanceof String) { try { return Integer.parseInt((String) durationObj); - } catch (NumberFormatException ignored) {} + } catch (final NumberFormatException ignored) {} } throw new IllegalArgumentException("durationSeconds must be an integer."); } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 5500eb5588..754d961883 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -108,7 +108,7 @@ public String createJwt( Integer expirySeconds, List roles, List backendRoles, - boolean roleSecurityMode + boolean includeBackendRoles ) throws JOSEException, ParseException { final Date now = new Date(timeProvider.getAsLong()); @@ -139,7 +139,7 @@ public String createJwt( throw new IllegalArgumentException("Roles cannot be null"); } - if (!roleSecurityMode && backendRoles != null) { + if (includeBackendRoles && backendRoles != null) { String listOfBackendRoles = String.join(",", backendRoles); claimsBuilder.claim("br", listOfBackendRoles); } diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index e271c7b838..dd4dd19aa2 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -102,7 +102,7 @@ public void testCreateJwtWithRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + final String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); SignedJWT signedJWT = SignedJWT.parse(encodedJwt); @@ -119,14 +119,14 @@ public void testCreateJwtWithRoles() throws Exception { } @Test - public void testCreateJwtWithRoleSecurityMode() throws Exception { - String issuer = "cluster_0"; - String subject = "admin"; - String audience = "audience_0"; - List roles = List.of("IT", "HR"); - List backendRoles = List.of("Sales", "Support"); - String expectedRoles = "IT,HR"; - String expectedBackendRoles = "Sales,Support"; + public void testCreateJwtWithBackendRolesIncluded() throws Exception { + final String issuer = "cluster_0"; + final String subject = "admin"; + final String audience = "audience_0"; + final List roles = List.of("IT", "HR"); + final List backendRoles = List.of("Sales", "Support"); + final String expectedRoles = "IT,HR"; + final String expectedBackendRoles = "Sales,Support"; int expirySeconds = 300; LongSupplier currentTime = () -> (long) 100; @@ -139,7 +139,7 @@ public void testCreateJwtWithRoleSecurityMode() throws Exception { // CS-ENFORCE-SINGLE .build(); final JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + final String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); SignedJWT signedJWT = SignedJWT.parse(encodedJwt); @@ -166,10 +166,10 @@ public void testCreateJwtWithNegativeExpiry() { Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); - Throwable exception = assertThrows(RuntimeException.class, () -> { + final Throwable exception = assertThrows(RuntimeException.class, () -> { try { jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); - } catch (Exception e) { + } catch (final Exception e) { throw new RuntimeException(e); } }); @@ -189,10 +189,10 @@ public void testCreateJwtWithExceededExpiry() throws Exception { Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - Throwable exception = assertThrows(RuntimeException.class, () -> { + final Throwable exception = assertThrows(RuntimeException.class, () -> { try { jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); - } catch (Exception e) { + } catch (final Exception e) { throw new RuntimeException(e); } }); @@ -204,18 +204,18 @@ public void testCreateJwtWithExceededExpiry() throws Exception { @Test public void testCreateJwtWithBadEncryptionKey() { - String issuer = "cluster_0"; - String subject = "admin"; - String audience = "audience_0"; - List roles = List.of("admin"); - Integer expirySeconds = 300; + final String issuer = "cluster_0"; + final String subject = "admin"; + final String audience = "audience_0"; + final List roles = List.of("admin"); + final Integer expirySeconds = 300; Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); - Throwable exception = assertThrows(RuntimeException.class, () -> { + final Throwable exception = assertThrows(RuntimeException.class, () -> { try { new JwtVendor(settings, Optional.empty()).createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); - } catch (Exception e) { + } catch (final Exception e) { throw new RuntimeException(e); } }); @@ -233,10 +233,10 @@ public void testCreateJwtWithBadRoles() { Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); - Throwable exception = assertThrows(RuntimeException.class, () -> { + final Throwable exception = assertThrows(RuntimeException.class, () -> { try { jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of(), true); - } catch (Exception e) { + } catch (final Exception e) { throw new RuntimeException(e); } }); @@ -249,7 +249,7 @@ public void testCreateJwtLogsCorrectly() throws Exception { logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); when(mockAppender.getName()).thenReturn("MockAppender"); when(mockAppender.isStarted()).thenReturn(true); - Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); + final Logger logger = (Logger) LogManager.getLogger(JwtVendor.class); logger.addAppender(mockAppender); logger.setLevel(Level.DEBUG); @@ -258,24 +258,24 @@ public void testCreateJwtLogsCorrectly() throws Exception { String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); - String issuer = "cluster_0"; - String subject = "admin"; - String audience = "audience_0"; - List roles = List.of("IT", "HR"); - List backendRoles = List.of("Sales", "Support"); - int expirySeconds = 300; + final String issuer = "cluster_0"; + final String subject = "admin"; + final String audience = "audience_0"; + final List roles = List.of("IT", "HR"); + final List backendRoles = List.of("Sales", "Support"); + final int expirySeconds = 300; - JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + final JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); verify(mockAppender, times(1)).append(logEventCaptor.capture()); - LogEvent logEvent = logEventCaptor.getValue(); - String logMessage = logEvent.getMessage().getFormattedMessage(); + final LogEvent logEvent = logEventCaptor.getValue(); + final String logMessage = logEvent.getMessage().getFormattedMessage(); assertTrue(logMessage.startsWith("Created JWT:")); - String[] parts = logMessage.split("\\."); + final String[] parts = logMessage.split("\\."); assertTrue(parts.length >= 3); } } From 035c4cac1986f65046920d190fe3b99a3642b8c4 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Date: Fri, 27 Oct 2023 13:10:06 -0400 Subject: [PATCH 02/23] Remove unneeded decoding from AuthTokenProcessorHandler (#3605) Remove unneeded decoding from AuthTokenProcessorHandler --------- Signed-off-by: Darshit Chanpura --- .../http/saml/AuthTokenProcessorHandler.java | 22 +++------ .../http/saml/HTTPSamlAuthenticatorTest.java | 49 ++++++++++--------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java index 9f9e654b69..393cedc3b5 100644 --- a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java +++ b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java @@ -99,21 +99,17 @@ class AuthTokenProcessorHandler { this.samlRolesSeparatorPattern = Pattern.compile(samlRolesSeparator); } - if (samlRolesKey == null || samlRolesKey.length() == 0) { + if (samlRolesKey == null || samlRolesKey.isEmpty()) { log.warn("roles_key is not configured, will only extract subject from SAML"); samlRolesKey = null; } - if (samlSubjectKey == null || samlSubjectKey.length() == 0) { + if (samlSubjectKey == null || samlSubjectKey.isEmpty()) { // If subjectKey == null, get subject from the NameID element. // Thus, this is a valid configuration. samlSubjectKey = null; } - if (samlRolesSeparator == null || samlRolesSeparator.length() == 0) { - samlRolesSeparator = null; - } - this.initJwtExpirySettings(settings); this.signingKey = this.createJwkFromSettings(settings, jwtSettings); this.jwsHeader = this.createJwsHeaderFromSettings(); @@ -128,12 +124,7 @@ Optional handle(RestRequest restRequest) throws Exception { sm.checkPermission(new SpecialPermission()); } - return AccessController.doPrivileged(new PrivilegedExceptionAction>() { - @Override - public Optional run() throws SamlConfigException, IOException { - return handleLowLevel(restRequest); - } - }); + return AccessController.doPrivileged((PrivilegedExceptionAction>) () -> handleLowLevel(restRequest)); } catch (PrivilegedActionException e) { if (e.getCause() instanceof Exception) { throw (Exception) e.getCause(); @@ -252,7 +243,7 @@ JWK createJwkFromSettings(Settings settings, Settings jwtSettings) throws Except String exchangeKey = settings.get("exchange_key"); if (!Strings.isNullOrEmpty(exchangeKey)) { - exchangeKey = padSecret(new String(Base64.getDecoder().decode(exchangeKey), StandardCharsets.UTF_8), JWSAlgorithm.HS512); + exchangeKey = padSecret(new String(Base64.getUrlDecoder().decode(exchangeKey), StandardCharsets.UTF_8), JWSAlgorithm.HS512); return new OctetSequenceKey.Builder(exchangeKey.getBytes(StandardCharsets.UTF_8)).algorithm(JWSAlgorithm.HS512) .keyUse(KeyUse.SIGNATURE) @@ -266,7 +257,10 @@ JWK createJwkFromSettings(Settings settings, Settings jwtSettings) throws Except ); } - String k = padSecret(new String(Base64.getDecoder().decode(jwkSettings.get("k")), StandardCharsets.UTF_8), JWSAlgorithm.HS512); + String k = padSecret( + new String(Base64.getUrlDecoder().decode(jwkSettings.get("k")), StandardCharsets.UTF_8), + JWSAlgorithm.HS512 + ); return new OctetSequenceKey.Builder(k.getBytes(StandardCharsets.UTF_8)).algorithm(JWSAlgorithm.HS512) .keyUse(KeyUse.SIGNATURE) diff --git a/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java index b475a42e2c..bbb0850392 100644 --- a/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/saml/HTTPSamlAuthenticatorTest.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -167,36 +168,40 @@ public void basicTest() throws Exception { mockSamlIdpServer.setAuthenticateUser("horst"); mockSamlIdpServer.setEndpointQueryString(null); - Settings settings = Settings.builder() - .put(IDP_METADATA_URL, mockSamlIdpServer.getMetadataUri()) - .put("kibana_url", "http://wherever") - .put("idp.entity_id", mockSamlIdpServer.getIdpEntityId()) - .put("exchange_key", "abc") - .put("roles_key", "roles") - .put("path.home", ".") - .build(); + Set exchangeKeys = Set.of("abc", "6aff3042-1327-4f3d-82f0-40a157ac4464"); + // should work with both keys + for (String exchKey : exchangeKeys) { + Settings settings = Settings.builder() + .put(IDP_METADATA_URL, mockSamlIdpServer.getMetadataUri()) + .put("kibana_url", "http://wherever") + .put("idp.entity_id", mockSamlIdpServer.getIdpEntityId()) + .put("exchange_key", exchKey) + .put("roles_key", "roles") + .put("path.home", ".") + .build(); - HTTPSamlAuthenticator samlAuthenticator = new HTTPSamlAuthenticator(settings, null); + HTTPSamlAuthenticator samlAuthenticator = new HTTPSamlAuthenticator(settings, null); - AuthenticateHeaders authenticateHeaders = getAutenticateHeaders(samlAuthenticator); + AuthenticateHeaders authenticateHeaders = getAutenticateHeaders(samlAuthenticator); - String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); + String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location); - RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); + RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders); - String responseJson = getResponse(samlAuthenticator, tokenRestRequest); - HashMap response = DefaultObjectMapper.objectMapper.readValue( - responseJson, - new TypeReference>() { - } - ); - String authorization = (String) response.get("authorization"); + String responseJson = getResponse(samlAuthenticator, tokenRestRequest); + HashMap response = DefaultObjectMapper.objectMapper.readValue( + responseJson, + new TypeReference>() { + } + ); + String authorization = (String) response.get("authorization"); - Assert.assertNotNull("Expected authorization attribute in JSON: " + responseJson, authorization); + Assert.assertNotNull("Expected authorization attribute in JSON: " + responseJson, authorization); - SignedJWT jwt = SignedJWT.parse(authorization.replaceAll("\\s*bearer\\s*", "")); + SignedJWT jwt = SignedJWT.parse(authorization.replaceAll("\\s*bearer\\s*", "")); - Assert.assertEquals("horst", jwt.getJWTClaimsSet().getClaim("sub")); + Assert.assertEquals("horst", jwt.getJWTClaimsSet().getClaim("sub")); + } } private Optional sendToAuthenticator(HTTPSamlAuthenticator samlAuthenticator, RestRequest request) { From 7a44f204ddc07915d436c4de0d211842899a2621 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:13:20 -0400 Subject: [PATCH 03/23] Bump org.apache.httpcomponents:httpcore from 4.4.13 to 4.4.16 (#3612) Bumps org.apache.httpcomponents:httpcore from 4.4.13 to 4.4.16. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.httpcomponents:httpcore&package-manager=gradle&previous-version=4.4.13&new-version=4.4.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9855a3de86..0a1a97e7ef 100644 --- a/build.gradle +++ b/build.gradle @@ -729,7 +729,7 @@ dependencies { integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14" integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14" integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.13" - integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.13" + integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16" integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" //spotless From 84b1d4bc3a7aba6ba87fa3e558c4169a39ef1158 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:13:51 -0400 Subject: [PATCH 04/23] Bump commons-cli:commons-cli from 1.5.0 to 1.6.0 (#3614) Bumps commons-cli:commons-cli from 1.5.0 to 1.6.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=commons-cli:commons-cli&package-manager=gradle&previous-version=1.5.0&new-version=1.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0a1a97e7ef..995c3785ef 100644 --- a/build.gradle +++ b/build.gradle @@ -570,7 +570,7 @@ dependencies { implementation "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}" implementation "com.google.guava:guava:${guava_version}" implementation 'org.greenrobot:eventbus-java:3.3.1' - implementation 'commons-cli:commons-cli:1.5.0' + implementation 'commons-cli:commons-cli:1.6.0' implementation "org.bouncycastle:bcprov-jdk15to18:${versions.bouncycastle}" implementation 'org.ldaptive:ldaptive:1.2.3' implementation 'com.nimbusds:nimbus-jose-jwt:9.31' From d34f23739c0e317e50102a8ba96c38de643c663a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:14:18 -0400 Subject: [PATCH 05/23] Bump org.apache.santuario:xmlsec from 2.3.3 to 2.3.4 (#3615) Bumps org.apache.santuario:xmlsec from 2.3.3 to 2.3.4. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.santuario:xmlsec&package-manager=gradle&previous-version=2.3.3&new-version=2.3.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 995c3785ef..3180afc44e 100644 --- a/build.gradle +++ b/build.gradle @@ -647,7 +647,7 @@ dependencies { runtimeOnly "org.glassfish.jaxb:txw2:${jaxb_version}" runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.5.1' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.3.1' - runtimeOnly 'org.apache.santuario:xmlsec:2.3.3' + runtimeOnly 'org.apache.santuario:xmlsec:2.3.4' runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" runtimeOnly 'org.checkerframework:checker-qual:3.39.0' runtimeOnly "org.bouncycastle:bcpkix-jdk15to18:${versions.bouncycastle}" From a888aab384ecdef0b75abc57efb563c638ef048c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:14:42 -0400 Subject: [PATCH 06/23] Bump org.apache.commons:commons-text from 1.10.0 to 1.11.0 (#3616) Bumps org.apache.commons:commons-text from 1.10.0 to 1.11.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.commons:commons-text&package-manager=gradle&previous-version=1.10.0&new-version=1.11.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3180afc44e..60216e60ee 100644 --- a/build.gradle +++ b/build.gradle @@ -634,7 +634,7 @@ dependencies { implementation "com.nulab-inc:zxcvbn:1.8.2" runtimeOnly 'com.google.guava:failureaccess:1.0.2' - runtimeOnly 'org.apache.commons:commons-text:1.10.0' + runtimeOnly 'org.apache.commons:commons-text:1.11.0' runtimeOnly "org.glassfish.jaxb:jaxb-runtime:${jaxb_version}" runtimeOnly 'com.google.j2objc:j2objc-annotations:2.8' compileOnly 'com.google.code.findbugs:jsr305:3.0.2' From a3499994ebd18acd90c100eb5a3884fa7a9c0e24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:09:55 -0400 Subject: [PATCH 07/23] Bump com.nimbusds:nimbus-jose-jwt from 9.31 to 9.37 (#3613) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 60216e60ee..fae53d849d 100644 --- a/build.gradle +++ b/build.gradle @@ -573,7 +573,7 @@ dependencies { implementation 'commons-cli:commons-cli:1.6.0' implementation "org.bouncycastle:bcprov-jdk15to18:${versions.bouncycastle}" implementation 'org.ldaptive:ldaptive:1.2.3' - implementation 'com.nimbusds:nimbus-jose-jwt:9.31' + implementation 'com.nimbusds:nimbus-jose-jwt:9.37' //JWT implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}" From 0a1c9ee8e7e2b1240d034a533ae234f5fcf842e1 Mon Sep 17 00:00:00 2001 From: Eduardo Corazon <79670342+EduardoCorazon@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:11:53 -0500 Subject: [PATCH 08/23] Re-enables CrossClusterSearchTests (#3554) Signed-off-by: Eduardo Corazon --- .../java/org/opensearch/security/CrossClusterSearchTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java b/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java index d9e4d2b5f3..410ad1c670 100644 --- a/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java +++ b/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java @@ -18,7 +18,6 @@ import org.apache.commons.lang3.tuple.Pair; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -65,7 +64,7 @@ * This is a parameterized test so that one test class is used to test security plugin behaviour when ccsMinimizeRoundtrips * option is enabled or disabled. Method {@link #parameters()} is a source of parameters values. */ -@Ignore("Setting up two clusters at once seems to be prone to issues where they have port mismatches") + @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class CrossClusterSearchTests { From 1ad8cf480cd31d2fd10fdb16e974816cb64fe0a8 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Mon, 30 Oct 2023 15:03:49 -0400 Subject: [PATCH 09/23] [Refactor] Improve asynchronous test cases format (#3601) Signed-off-by: Peter Nied --- .../security/ResourceFocusedTests.java | 126 ++++-------------- .../security/rest/CompressionTests.java | 107 ++++++--------- .../test/framework/AsyncActions.java | 99 ++++++++++++++ .../resources/log4j2-test.properties | 3 + 4 files changed, 170 insertions(+), 165 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/test/framework/AsyncActions.java diff --git a/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java b/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java index a25423471f..5d441d0063 100644 --- a/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java +++ b/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java @@ -16,17 +16,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.lang.management.GarbageCollectorMXBean; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryPoolMXBean; -import java.lang.management.MemoryUsage; import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.zip.GZIPOutputStream; @@ -35,12 +27,16 @@ import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.opensearch.action.index.IndexRequest; import org.opensearch.client.Client; +import org.opensearch.test.framework.AsyncActions; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.User; import org.opensearch.test.framework.cluster.ClusterManager; @@ -52,6 +48,7 @@ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class ResourceFocusedTests { + private final static Logger LOG = LogManager.getLogger(AsyncActions.class); private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); private static final User LIMITED_USER = new User("limited_user").roles( new TestSecurityConfig.Role("limited-role").clusterPermissions( @@ -93,9 +90,8 @@ public void testUnauthenticatedFewBig() { final String requestPath = "/*/_search"; final int parrallelism = 5; final int totalNumberOfRequests = 100; - final boolean statsPrinter = false; - runResourceTest(size, requestPath, parrallelism, totalNumberOfRequests, statsPrinter); + runResourceTest(size, requestPath, parrallelism, totalNumberOfRequests); } @Test @@ -105,9 +101,8 @@ public void testUnauthenticatedManyMedium() { final String requestPath = "/*/_search"; final int parrallelism = 20; final int totalNumberOfRequests = 10_000; - final boolean statsPrinter = false; - runResourceTest(size, requestPath, parrallelism, totalNumberOfRequests, statsPrinter); + runResourceTest(size, requestPath, parrallelism, totalNumberOfRequests); } @Test @@ -116,62 +111,27 @@ public void testUnauthenticatedTonsSmall() { final RequestBodySize size = RequestBodySize.Small; final String requestPath = "/*/_search"; final int parrallelism = 100; - final int totalNumberOfRequests = 1_000_000; - final boolean statsPrinter = false; + final int totalNumberOfRequests = 15_000; - runResourceTest(size, requestPath, parrallelism, totalNumberOfRequests, statsPrinter); + runResourceTest(size, requestPath, parrallelism, totalNumberOfRequests); } - private Long runResourceTest( + private void runResourceTest( final RequestBodySize size, final String requestPath, final int parrallelism, - final int totalNumberOfRequests, - final boolean statsPrinter + final int totalNumberOfRequests ) { final byte[] compressedRequestBody = createCompressedRequestBody(size); try (final TestRestClient client = cluster.getRestClient(new BasicHeader("Content-Encoding", "gzip"))) { - - if (statsPrinter) { - printStats(); - } - final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); - post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); - - final ForkJoinPool forkJoinPool = new ForkJoinPool(parrallelism); - - final List> waitingOn = IntStream.rangeClosed(1, totalNumberOfRequests) - .boxed() - .map(i -> CompletableFuture.runAsync(() -> client.executeRequest(post), forkJoinPool)) - .collect(Collectors.toList()); - Supplier getCount = () -> waitingOn.stream().filter(cf -> cf.isDone() && !cf.isCompletedExceptionally()).count(); - - CompletableFuture statPrinter = statsPrinter ? CompletableFuture.runAsync(() -> { - while (true) { - printStats(); - System.err.println(" & Succesful completions: " + getCount.get()); - try { - Thread.sleep(500); - } catch (Exception e) { - break; - } - } - }, forkJoinPool) : CompletableFuture.completedFuture(null); - - final CompletableFuture allOfThem = CompletableFuture.allOf(waitingOn.toArray(new CompletableFuture[0])); - - try { - allOfThem.get(30, TimeUnit.SECONDS); - statPrinter.cancel(true); - } catch (final Exception e) { - // Ignored - } - - if (statsPrinter) { - printStats(); - System.err.println(" & Succesful completions: " + getCount.get()); - } - return getCount.get(); + final var requests = AsyncActions.generate(() -> { + final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); + post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); + return client.executeRequest(post); + }, parrallelism, totalNumberOfRequests); + + AsyncActions.getAll(requests, 2, TimeUnit.MINUTES) + .forEach((response) -> { response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); }); } } @@ -217,51 +177,17 @@ private byte[] createCompressedRequestBody(final RequestBodySize size) { gzipOutputStream.finish(); final byte[] compressedRequestBody = byteArrayOutputStream.toByteArray(); - System.err.println( - "^^^" - + String.format( - "Original size was %,d bytes, compressed to %,d bytes, ratio %,.2f", - uncompressedBytesSize, - compressedRequestBody.length, - ((double) uncompressedBytesSize / compressedRequestBody.length) - ) + LOG.info( + String.format( + "Original size was %,d bytes, compressed to %,d bytes, ratio %,.2f", + uncompressedBytesSize, + compressedRequestBody.length, + ((double) uncompressedBytesSize / compressedRequestBody.length) + ) ); return compressedRequestBody; } catch (final IOException ioe) { throw new RuntimeException(ioe); } } - - private void printStats() { - System.err.println("** Stats "); - printMemory(); - printMemoryPools(); - printGCPools(); - } - - private void printMemory() { - final Runtime runtime = Runtime.getRuntime(); - - final long totalMemory = runtime.totalMemory(); // Total allocated memory - final long freeMemory = runtime.freeMemory(); // Amount of free memory - final long usedMemory = totalMemory - freeMemory; // Amount of used memory - - System.err.println(" Memory Total: " + totalMemory + " Free:" + freeMemory + " Used:" + usedMemory); - } - - private void printMemoryPools() { - List memoryPools = ManagementFactory.getMemoryPoolMXBeans(); - for (MemoryPoolMXBean memoryPool : memoryPools) { - MemoryUsage usage = memoryPool.getUsage(); - System.err.println(" " + memoryPool.getName() + " USED: " + usage.getUsed() + " MAX: " + usage.getMax()); - } - } - - private void printGCPools() { - List garbageCollectors = ManagementFactory.getGarbageCollectorMXBeans(); - for (GarbageCollectorMXBean garbageCollector : garbageCollectors) { - System.err.println(" " + garbageCollector.getName() + " COLLECTION TIME: " + garbageCollector.getCollectionTime()); - } - } - } diff --git a/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java b/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java index cf07f93ad8..aa747e2586 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java @@ -11,9 +11,9 @@ package org.opensearch.security.rest; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; + import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.message.BasicHeader; @@ -21,11 +21,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.anyOf; -import static org.hamcrest.MatcherAssert.assertThat; +import org.opensearch.test.framework.AsyncActions; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -34,15 +30,14 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.zip.GZIPOutputStream; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.getBasicAuthHeader; @@ -60,7 +55,7 @@ public class CompressionTests { .build(); @Test - public void testAuthenticatedGzippedRequests() throws Exception { + public void testAuthenticatedGzippedRequests() { final String requestPath = "/*/_search"; final int parallelism = 10; final int totalNumberOfRequests = 100; @@ -69,31 +64,13 @@ public void testAuthenticatedGzippedRequests() throws Exception { final byte[] compressedRequestBody = createCompressedRequestBody(rawBody); try (final TestRestClient client = cluster.getRestClient(ADMIN_USER, new BasicHeader("Content-Encoding", "gzip"))) { + final var requests = AsyncActions.generate(() -> { + final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); + post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); + return client.executeRequest(post); + }, parallelism, totalNumberOfRequests); - final ForkJoinPool forkJoinPool = new ForkJoinPool(parallelism); - - final List> waitingOn = IntStream.rangeClosed(1, totalNumberOfRequests) - .boxed() - .map(i -> CompletableFuture.supplyAsync(() -> { - final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); - post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); - return client.executeRequest(post); - }, forkJoinPool)) - .collect(Collectors.toList()); - - final CompletableFuture allOfThem = CompletableFuture.allOf(waitingOn.toArray(new CompletableFuture[0])); - - allOfThem.get(30, TimeUnit.SECONDS); - - waitingOn.stream().forEach(future -> { - try { - final HttpResponse response = future.get(); - response.assertStatusCode(HttpStatus.SC_OK); - } catch (final Exception ex) { - throw new RuntimeException(ex); - } - }); - ; + AsyncActions.getAll(requests, 30, TimeUnit.SECONDS).forEach((response) -> { response.assertStatusCode(HttpStatus.SC_OK); }); } } @@ -101,40 +78,40 @@ public void testAuthenticatedGzippedRequests() throws Exception { public void testMixOfAuthenticatedAndUnauthenticatedGzippedRequests() throws Exception { final String requestPath = "/*/_search"; final int parallelism = 10; - final int totalNumberOfRequests = 100; + final int totalNumberOfRequests = 50; final String rawBody = "{ \"query\": { \"match\": { \"foo\": \"bar\" }}}"; final byte[] compressedRequestBody = createCompressedRequestBody(rawBody); try (final TestRestClient client = cluster.getRestClient(new BasicHeader("Content-Encoding", "gzip"))) { - - final ForkJoinPool forkJoinPool = new ForkJoinPool(parallelism); - - final Header basicAuthHeader = getBasicAuthHeader(ADMIN_USER.getName(), ADMIN_USER.getPassword()); - - final List> waitingOn = IntStream.rangeClosed(1, totalNumberOfRequests) - .boxed() - .map(i -> CompletableFuture.supplyAsync(() -> { - final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); - post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); - return i % 2 == 0 ? client.executeRequest(post) : client.executeRequest(post, basicAuthHeader); - }, forkJoinPool)) - .collect(Collectors.toList()); - - final CompletableFuture allOfThem = CompletableFuture.allOf(waitingOn.toArray(new CompletableFuture[0])); - - allOfThem.get(30, TimeUnit.SECONDS); - - waitingOn.stream().forEach(future -> { - try { - final HttpResponse response = future.get(); - assertThat(response.getBody(), not(containsString("json_parse_exception"))); - assertThat(response.getStatusCode(), anyOf(equalTo(HttpStatus.SC_UNAUTHORIZED), equalTo(HttpStatus.SC_OK))); - } catch (final Exception ex) { - throw new RuntimeException(ex); - } + final CountDownLatch countDownLatch = new CountDownLatch(1); + + final var authorizedRequests = AsyncActions.generate(() -> { + countDownLatch.await(); + System.err.println("Generation triggered authorizedRequests"); + final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); + post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); + return client.executeRequest(post, getBasicAuthHeader(ADMIN_USER.getName(), ADMIN_USER.getPassword())); + }, parallelism, totalNumberOfRequests); + + final var unauthorizedRequests = AsyncActions.generate(() -> { + countDownLatch.await(); + System.err.println("Generation triggered unauthorizedRequests"); + final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); + post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); + return client.executeRequest(post); + }, parallelism, totalNumberOfRequests); + + // Make sure all requests start at the same time + countDownLatch.countDown(); + + AsyncActions.getAll(authorizedRequests, 30, TimeUnit.SECONDS).forEach((response) -> { + assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + }); + AsyncActions.getAll(unauthorizedRequests, 30, TimeUnit.SECONDS).forEach((response) -> { + assertThat(response.getBody(), not(containsString("json_parse_exception"))); + assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED)); }); - ; } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/AsyncActions.java b/src/integrationTest/java/org/opensearch/test/framework/AsyncActions.java new file mode 100644 index 0000000000..409aa5a416 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/AsyncActions.java @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ + +package org.opensearch.test.framework; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class AsyncActions { + private final static Logger LOG = LogManager.getLogger(AsyncActions.class); + + /** + * Using the provided generator create a list of completable futures. + * @param parrallelism How many calls to the generator should be done at the same time. + * @param generationCount The total number of calls to the generator to conduct. + * @return The list of completable futures running on the fork join thread pool. + */ + public static List> generate(final Callable generator, final int parrallelism, final int generationCount) { + final ForkJoinPool forkJoinPool = new ForkJoinPool(parrallelism); + return IntStream.rangeClosed(1, generationCount).boxed().map(i -> CompletableFuture.supplyAsync(() -> { + try { + return generator.call(); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + }, forkJoinPool)).collect(Collectors.toList()); + } + + /** + * Waits for futures for a time period and then returns them a list + * @param futures Futures to wait for completion with a result + * @param n Amount of time to wait + * @param unit Time associated with those units + * @return Completed results from the futures + */ + public static List getAll(final List> futures, final int n, final TimeUnit unit) { + LOG.info("Starting to wait for " + futures.size() + " futures to complete in " + unit.toSeconds(n) + " seconds."); + final long startTimeMs = System.currentTimeMillis(); + final CompletableFuture futuresCompleted = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + try { + futuresCompleted.get(n, unit); + } catch (final Exception ex) { + final long completedFuturesCount = futures.stream().filter(CompletableFuture::isDone).count(); + final String perfReport = calculatePerfReport(startTimeMs, completedFuturesCount); + throw new RuntimeException( + "Unable to wait for all futures to complete, of " + + futures.size() + + " futures " + + completedFuturesCount + + " have finished." + + perfReport + ); + } + final long completedFuturesCount = futures.stream().filter(CompletableFuture::isDone).count(); + final String perfReport = calculatePerfReport(startTimeMs, completedFuturesCount); + LOG.info(perfReport); + + final long elapsedTimeMs = System.currentTimeMillis() - startTimeMs; + final long expectedMs = unit.toMillis(n); + if (elapsedTimeMs > .75 * expectedMs) { + LOG.warn("Completion time was within 25% of the expected time, more than this threshold is recommended."); + } + + return futures.stream().map(future -> { + try { + return future.get(); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + }).collect(Collectors.toList()); + } + + private static String calculatePerfReport(final long startTimeMs, final long completedFuturesCount) { + final long elapsedTimeMs = System.currentTimeMillis() - startTimeMs; + final double avgTimePerFutureMs = (double) elapsedTimeMs / completedFuturesCount; + final double futuresPerSecond = 1000 / avgTimePerFutureMs; + return String.format( + "Waited for %d seconds, completion speed was on average %.2fms per future %.2fx per second.", + TimeUnit.MILLISECONDS.toSeconds(elapsedTimeMs), + avgTimePerFutureMs, + futuresPerSecond + ); + } +} diff --git a/src/integrationTest/resources/log4j2-test.properties b/src/integrationTest/resources/log4j2-test.properties index 8d9cf87666..0b865b46b3 100644 --- a/src/integrationTest/resources/log4j2-test.properties +++ b/src/integrationTest/resources/log4j2-test.properties @@ -28,6 +28,7 @@ logger.auditlogs.level = info # Logger required by test org.opensearch.security.http.JwtAuthenticationTests logger.httpjwtauthenticator.name = com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator logger.httpjwtauthenticator.level = debug +logger.backendreg.additivity = false logger.httpjwtauthenticator.appenderRef.capturing.ref = logCapturingAppender #Required by tests: @@ -35,10 +36,12 @@ logger.httpjwtauthenticator.appenderRef.capturing.ref = logCapturingAppender # org.opensearch.security.UserBruteForceAttacksPreventionTests logger.backendreg.name = org.opensearch.security.auth.BackendRegistry logger.backendreg.level = debug +logger.backendreg.additivity = false logger.backendreg.appenderRef.capturing.ref = logCapturingAppender #com.amazon.dlic.auth.ldap #logger.ldap.name=com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend logger.ldap.name=com.amazon.dlic.auth.ldap.backend logger.ldap.level=TRACE +logger.backendreg.additivity = false logger.ldap.appenderRef.capturing.ref = logCapturingAppender From 22a427692d23924b2261cc54eab53b21cb989b76 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 30 Oct 2023 15:57:29 -0400 Subject: [PATCH 10/23] Bump jjwt_version from 0.11.5 to 0.12.3 (#3536) Bump `jjwt_version` from 0.11.5 to 0.12.2. Signed-off-by: Craig Perkins Signed-off-by: Darshit Chanpura Co-authored-by: Darshit Chanpura --- build.gradle | 2 +- .../http/OnBehalfOfJwtAuthenticationTest.java | 12 +++-- .../http/OnBehalfOfAuthenticator.java | 5 +- .../opensearch/security/util/KeyUtils.java | 9 ++-- .../http/jwt/HTTPJwtAuthenticatorTest.java | 22 +++++---- .../http/OnBehalfOfAuthenticatorTest.java | 48 ++++--------------- 6 files changed, 39 insertions(+), 59 deletions(-) diff --git a/build.gradle b/build.gradle index fae53d849d..ad7c27980c 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { apache_cxf_version = '4.0.3' open_saml_version = '4.3.0' one_login_java_saml = '2.9.0' - jjwt_version = '0.11.5' + jjwt_version = '0.12.3' guava_version = '32.1.3-jre' jaxb_version = '2.3.9' diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 45fb39d362..2f8437af69 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -19,9 +19,13 @@ import java.util.Set; import java.util.stream.Collectors; +import javax.crypto.SecretKey; + import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; @@ -173,11 +177,9 @@ public void shouldNotAuthenticateForNonAdminUserWithoutOBOPermission() { public void shouldNotIncludeRolesFromHostMappingInOBOToken() { String oboToken = generateOboToken(OBO_USER_NAME_WITH_HOST_MAPPING, DEFAULT_PASSWORD); - Claims claims = Jwts.parserBuilder() - .setSigningKey(Base64.getDecoder().decode(signingKey)) - .build() - .parseClaimsJws(oboToken) - .getBody(); + SecretKey key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKey)); + + Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(oboToken).getPayload(); Object er = claims.get("er"); EncryptionDecryptionUtil encryptionDecryptionUtil = new EncryptionDecryptionUtil(encryptionKey); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index a3d3dec710..a3e0e1ba4e 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Optional; import java.util.Map.Entry; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -173,8 +174,8 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { return null; } - final String audience = claims.getAudience(); - if (audience == null) { + final Set audience = claims.getAudience(); + if (audience == null || audience.isEmpty()) { log.error("Valid jwt on behalf of token with no audience"); return null; } diff --git a/src/main/java/org/opensearch/security/util/KeyUtils.java b/src/main/java/org/opensearch/security/util/KeyUtils.java index 4b3aafbbc6..c232dda3a2 100644 --- a/src/main/java/org/opensearch/security/util/KeyUtils.java +++ b/src/main/java/org/opensearch/security/util/KeyUtils.java @@ -13,13 +13,13 @@ import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; import org.opensearch.core.common.Strings; import java.security.AccessController; -import java.security.Key; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivilegedAction; @@ -48,7 +48,7 @@ public JwtParserBuilder run() { return null; } else { try { - Key key = null; + PublicKey key = null; final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") .replace("-----END PUBLIC KEY-----", ""); @@ -57,6 +57,7 @@ public JwtParserBuilder run() { try { key = getPublicKey(decoded, "RSA"); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { log.debug("No public RSA key, try other algos ({})", e.toString()); } @@ -68,10 +69,10 @@ public JwtParserBuilder run() { } if (Objects.nonNull(key)) { - return Jwts.parserBuilder().setSigningKey(key); + return Jwts.parser().verifyWith(key); } - return Jwts.parserBuilder().setSigningKey(decoded); + return Jwts.parser().verifyWith(Keys.hmacShaKeyFor(decoded)); } catch (Throwable e) { log.error("Error while creating JWT authenticator", e); throw new OpenSearchSecurityException(e.toString(), e); diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java index 225b93dbbb..4f141994e3 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java @@ -33,10 +33,14 @@ import org.junit.Assert; import org.junit.Test; +import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + public class HTTPJwtAuthenticatorTest { final static byte[] secretKeyBytes = new byte[1024]; @@ -67,14 +71,16 @@ public void testEmptyKey() throws Exception { } @Test - public void testBadKey() { - - final AuthCredentials credentials = extractCredentialsFromJwtHeader( - Settings.builder().put("signing_key", BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 })), - Jwts.builder().setSubject("Leonard McCoy") - ); - - Assert.assertNull(credentials); + public void testBadKey() throws Exception { + try { + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + Settings.builder().put("signing_key", BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 })), + Jwts.builder().setSubject("Leonard McCoy") + ); + fail("Expected WeakKeyException"); + } catch (OpenSearchSecurityException e) { + assertTrue("Expected error message to contain WeakKeyException", e.getMessage().contains("WeakKeyException")); + } } @Test diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 478e59ac13..4215aac0ad 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -11,7 +11,6 @@ package org.opensearch.security.http; -import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; @@ -28,22 +27,20 @@ import com.google.common.io.BaseEncoding; import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.WeakKeyException; import org.apache.commons.lang3.RandomStringUtils; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; -import org.apache.logging.log4j.core.ErrorHandler; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.Logger; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; import org.opensearch.common.settings.Settings; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; @@ -60,8 +57,8 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -143,40 +140,13 @@ public void testBadKey() { @Test public void testWeakKeyExceptionHandling() throws Exception { - Appender mockAppender = mock(Appender.class); - ErrorHandler mockErrorHandler = mock(ErrorHandler.class); - when(mockAppender.getHandler()).thenReturn(mockErrorHandler); - when(mockAppender.isStarted()).thenReturn(true); - - ArgumentCaptor logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); - when(mockAppender.getName()).thenReturn("MockAppender"); - doNothing().when(mockAppender).append(logEventCaptor.capture()); - - Logger logger = (Logger) LogManager.getLogger(OnBehalfOfAuthenticator.class); - logger.addAppender(mockAppender); - - JwtParser mockJwtParser = mock(JwtParser.class); - when(mockJwtParser.parseClaimsJws(anyString())).thenThrow(new WeakKeyException("Test Exception")); - Settings settings = Settings.builder().put("signing_key", "testKey").put("encryption_key", claimsEncryptionKey).build(); - OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, "testCluster"); - - Field jwtParserField = OnBehalfOfAuthenticator.class.getDeclaredField("jwtParser"); - jwtParserField.setAccessible(true); - jwtParserField.set(auth, mockJwtParser); - - SecurityRequest mockedRequest = mock(SecurityRequest.class); - when(mockedRequest.header(anyString())).thenReturn("Bearer testToken"); - when(mockedRequest.path()).thenReturn("/some/sample/path"); - - auth.extractCredentials(mockedRequest, null); - - boolean foundLog = logEventCaptor.getAllValues() - .stream() - .anyMatch(event -> event.getMessage().getFormattedMessage().contains("Cannot authenticate user with JWT because of ")); - assertTrue(foundLog); - - logger.removeAppender(mockAppender); + try { + OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, "testCluster"); + fail("Expected WeakKeyException"); + } catch (OpenSearchSecurityException e) { + assertTrue("Expected error message to contain WeakKeyException", e.getMessage().contains("WeakKeyException")); + } } @Test @@ -287,7 +257,7 @@ public void testBearer() throws Exception { Map expectedAttributes = new HashMap<>(); expectedAttributes.put("attr.jwt.iss", "cluster_0"); expectedAttributes.put("attr.jwt.sub", "Leonard McCoy"); - expectedAttributes.put("attr.jwt.aud", "ext_0"); + expectedAttributes.put("attr.jwt.aud", "[ext_0]"); String jwsToken = Jwts.builder() .setIssuer(clusterName) From bd43eefbb9c9785120af89ca87a8f137a0721332 Mon Sep 17 00:00:00 2001 From: Peter Kiib Egede <100785536+pegtrifork@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:55:34 +0100 Subject: [PATCH 11/23] Change log message with jwtString from INFO to TRACE (#3621) Signed-off-by: pegtrifork --- .../dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index 4271e68f1c..da19a808a3 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -123,7 +123,9 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) throw log.info(e.toString()); throw new OpenSearchSecurityException(e.getMessage(), RestStatus.SERVICE_UNAVAILABLE); } catch (BadCredentialsException | ParseException e) { - log.info("Extracting JWT token from {} failed", jwtString, e); + if (log.isTraceEnabled()) { + log.trace("Extracting JWT token from {} failed", jwtString, e); + } return null; } From d10475cbf0cc8ccdf959964c5d30777ee72e7bcc Mon Sep 17 00:00:00 2001 From: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Tue, 31 Oct 2023 08:37:39 -0700 Subject: [PATCH 12/23] Limit service account permissions to system index only(#3607) Signed-off-by: Ryan Liang --- .../ServiceAccountAuthenticationTest.java | 142 ++++++++++++++++++ .../privileges/PrivilegesEvaluator.java | 8 + .../SecurityIndexAccessEvaluator.java | 36 +++++ .../org/opensearch/security/user/User.java | 10 ++ .../opensearch/security/IntegrationTests.java | 2 +- .../security/UserServiceUnitTests.java | 2 +- src/test/resources/internal_users.yml | 7 + 7 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java diff --git a/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java new file mode 100644 index 0000000000..04f943edcf --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.opensearch.test.framework.TestIndex; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_KEY; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class ServiceAccountAuthenticationTest { + + public static final String SERVICE_ATTRIBUTE = "service"; + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + public static final String SERVICE_ACCOUNT_USER_NAME = "test-service-account"; + + static final TestSecurityConfig.User SERVICE_ACCOUNT_ADMIN_USER = new TestSecurityConfig.User(SERVICE_ACCOUNT_USER_NAME).attr( + SERVICE_ATTRIBUTE, + "true" + ) + .roles( + new TestSecurityConfig.Role("test-service-account-role").clusterPermissions("*") + .indexPermissions("*", "system:admin/system_index") + .on("*") + ); + + private static final TestIndex TEST_NON_SYS_INDEX = TestIndex.name("test-non-sys-index") + .setting("index.number_of_shards", 1) + .setting("index.number_of_replicas", 0) + .build(); + + private static final TestIndex TEST_SYS_INDEX = TestIndex.name("test-sys-index") + .setting("index.number_of_shards", 1) + .setting("index.number_of_replicas", 0) + .build(); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .users(ADMIN_USER, SERVICE_ACCOUNT_ADMIN_USER) + .nodeSettings( + Map.of( + SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, + true, + SECURITY_SYSTEM_INDICES_ENABLED_KEY, + true, + SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_admin__all_access"), + SECURITY_SYSTEM_INDICES_KEY, + List.of(TEST_SYS_INDEX.getName()) + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .indices(TEST_NON_SYS_INDEX, TEST_SYS_INDEX) + .build(); + + @Test + public void testClusterHealthWithServiceAccountCred() { + try (TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_ADMIN_USER)) { + client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME); + TestRestClient.HttpResponse response = client.get("_cluster/health"); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + + String responseBody = response.getBody(); + + assertNotNull("Response body should not be null", responseBody); + assertTrue(responseBody.contains("\"type\":\"security_exception\"")); + } + } + + @Test + public void testReadSysIndexWithServiceAccountCred() { + try (TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_ADMIN_USER)) { + client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME); + TestRestClient.HttpResponse response = client.get(TEST_SYS_INDEX.getName()); + response.assertStatusCode(HttpStatus.SC_OK); + + String responseBody = response.getBody(); + + assertNotNull("Response body should not be null", responseBody); + assertTrue(responseBody.contains(TEST_SYS_INDEX.getName())); + } + } + + @Test + public void testReadNonSysIndexWithServiceAccountCred() { + try (TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_ADMIN_USER)) { + client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME); + TestRestClient.HttpResponse response = client.get(TEST_NON_SYS_INDEX.getName()); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + + String responseBody = response.getBody(); + + assertNotNull("Response body should not be null", responseBody); + assertTrue(responseBody.contains("\"type\":\"security_exception\"")); + } + } + + @Test + public void testReadBothWithServiceAccountCred() { + TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_ADMIN_USER); + client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME); + TestRestClient.HttpResponse response = client.get((TEST_SYS_INDEX.getName() + "," + TEST_NON_SYS_INDEX.getName())); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + + String responseBody = response.getBody(); + + assertNotNull("Response body should not be null", responseBody); + assertTrue(responseBody.contains("\"type\":\"security_exception\"")); + + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 48f4db38e9..538b541754 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -353,7 +353,15 @@ public PrivilegesEvaluatorResponse evaluate( namedXContentRegistry ); + final boolean serviceAccountUser = user.isServiceAccount(); if (isClusterPerm(action0)) { + if (serviceAccountUser) { + presponse.missingPrivileges.add(action0); + presponse.allowed = false; + log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); + return presponse; + } + if (!securityRoles.impliesClusterPermissionPermission(action0)) { presponse.missingPrivileges.add(action0); presponse.allowed = false; diff --git a/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java index 3743b27383..4d5fb26050 100644 --- a/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java @@ -199,6 +199,22 @@ private List getAllProtectedSystemIndices(final Resolved requestedResolv return new ArrayList<>(superAdminAccessOnlyIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); } + /** + * Checks if the request contains any regular (non-system and non-protected) indices. + * Regular indices are those that are not categorized as system indices or protected system indices. + * This method helps in identifying requests that might be accessing regular indices alongside system indices. + * @param requestedResolved The resolved object of the request, which contains the list of indices from the original request. + * @return true if the request contains any regular indices, false otherwise. + */ + private boolean requestContainsAnyRegularIndices(final Resolved requestedResolved) { + Set allIndices = requestedResolved.getAllIndices(); + + List allSystemIndices = getAllSystemIndices(requestedResolved); + List allProtectedSystemIndices = getAllProtectedSystemIndices(requestedResolved); + + return allIndices.stream().anyMatch(index -> !allSystemIndices.contains(index) && !allProtectedSystemIndices.contains(index)); + } + /** * Is the current action allowed to be performed on security index * @param action request action on security index @@ -233,8 +249,28 @@ private void evaluateSystemIndicesAccess( ) { // Perform access check is system index permissions are enabled boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved); + boolean containsRegularIndex = requestContainsAnyRegularIndices(requestedResolved); + boolean serviceAccountUser = user.isServiceAccount(); if (isSystemIndexPermissionEnabled) { + if (serviceAccountUser && containsRegularIndex) { + auditLog.logSecurityIndexAttempt(request, action, task); + if (!containsSystemIndex && log.isInfoEnabled()) { + log.info("{} not permitted for a service account {} on non-system indices.", action, securityRoles); + } else if (containsSystemIndex && log.isDebugEnabled()) { + List regularIndices = requestedResolved.getAllIndices() + .stream() + .filter( + index -> !getAllSystemIndices(requestedResolved).contains(index) + && !getAllProtectedSystemIndices(requestedResolved).contains(index) + ) + .collect(Collectors.toList()); + log.debug("Service account cannot access regular indices: {}", regularIndices); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved); if (containsProtectedIndex) { auditLog.logSecurityIndexAttempt(request, action, task); diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index 394b251271..aa9c09a469 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -286,4 +286,14 @@ public final Set getSecurityRoles() { ? Collections.synchronizedSet(Collections.emptySet()) : Collections.unmodifiableSet(this.securityRoles); } + + /** + * Check the custom attributes associated with this user + * + * @return true if it has a service account attributes. otherwise false + */ + public boolean isServiceAccount() { + Map userAttributesMap = this.getCustomAttributesMap(); + return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service")); + } } diff --git a/src/test/java/org/opensearch/security/IntegrationTests.java b/src/test/java/org/opensearch/security/IntegrationTests.java index 63ae25316b..9a4bf7bba8 100644 --- a/src/test/java/org/opensearch/security/IntegrationTests.java +++ b/src/test/java/org/opensearch/security/IntegrationTests.java @@ -331,7 +331,7 @@ public void testSpecialUsernames() throws Exception { setup(); RestHelper rh = nonSslRestHelper(); - Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("bug.99", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("bug.88", "nagilum")).getStatusCode()); Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("a", "b")).getStatusCode()); Assert.assertEquals( HttpStatus.SC_OK, diff --git a/src/test/java/org/opensearch/security/UserServiceUnitTests.java b/src/test/java/org/opensearch/security/UserServiceUnitTests.java index 533a4c7366..6bdef8d167 100644 --- a/src/test/java/org/opensearch/security/UserServiceUnitTests.java +++ b/src/test/java/org/opensearch/security/UserServiceUnitTests.java @@ -43,7 +43,7 @@ public class UserServiceUnitTests { UserService userService; final int SERVICE_ACCOUNTS_IN_SETTINGS = 1; - final int INTERNAL_ACCOUNTS_IN_SETTINGS = 66; + final int INTERNAL_ACCOUNTS_IN_SETTINGS = 67; String serviceAccountUsername = "bug.99"; String internalAccountUsername = "sarek"; diff --git a/src/test/resources/internal_users.yml b/src/test/resources/internal_users.yml index b078d6920f..a5eeb6fddb 100644 --- a/src/test/resources/internal_users.yml +++ b/src/test/resources/internal_users.yml @@ -2,6 +2,13 @@ _meta: type: "internalusers" config_version: 2 +bug.88: + hash: "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m" + reserved: false + hidden: false + backend_roles: [] + attributes: {} + description: "Migrated from v6" bug.99: hash: "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m" reserved: false From 467645241d42c8ae02166c4d41d3e7aa5edafdc7 Mon Sep 17 00:00:00 2001 From: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:04:40 -0400 Subject: [PATCH 13/23] Implement IdentityPlugin in Security plugin (#3538) Signed-off-by: Stephen Crawford Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Signed-off-by: Peter Nied Co-authored-by: Peter Nied --- .../http/OnBehalfOfJwtAuthenticationTest.java | 89 ++++++- .../security/OpenSearchSecurityPlugin.java | 26 +- .../onbehalf/CreateOnBehalfOfTokenAction.java | 92 ++----- .../jwt/ExpiringBearerAuthToken.java | 40 +++ .../security/authtoken/jwt/JwtVendor.java | 52 ++-- .../ConfigurationRepository.java | 2 +- .../dlic/rest/api/InternalUsersApiAction.java | 6 +- .../http/OnBehalfOfAuthenticator.java | 36 ++- .../identity/SecurityTokenManager.java | 141 ++++++++++ .../opensearch/security/user/UserService.java | 6 +- .../security/authtoken/jwt/JwtVendorTest.java | 26 +- .../http/OnBehalfOfAuthenticatorTest.java | 19 +- .../identity/SecurityTokenManagerTest.java | 247 ++++++++++++++++++ 13 files changed, 627 insertions(+), 155 deletions(-) create mode 100644 src/main/java/org/opensearch/security/authtoken/jwt/ExpiringBearerAuthToken.java create mode 100644 src/main/java/org/opensearch/security/identity/SecurityTokenManager.java create mode 100644 src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 2f8437af69..1233e23341 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -11,10 +11,10 @@ package org.opensearch.security.http; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -29,10 +29,12 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; - +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.test.framework.OnBehalfOfConfig; import org.opensearch.test.framework.RolesMapping; @@ -47,6 +49,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.contains; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; @@ -67,6 +70,11 @@ public class OnBehalfOfJwtAuthenticationTest { StandardCharsets.UTF_8 ) ); + private static final String alternativeSigningKey = Base64.getEncoder() + .encodeToString( + "alternativeSigningKeyalternativeSigningKeyalternativeSigningKeyalternativeSigningKey".getBytes(StandardCharsets.UTF_8) + ); + private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); public static final String ADMIN_USER_NAME = "admin"; public static final String OBO_USER_NAME_WITH_PERM = "obo_user"; @@ -110,18 +118,36 @@ public class OnBehalfOfJwtAuthenticationTest { protected final static TestSecurityConfig.User HOST_MAPPING_OBO_USER = new TestSecurityConfig.User(OBO_USER_NAME_WITH_HOST_MAPPING) .roles(HOST_MAPPING_ROLE, ROLE_WITH_OBO_PERM); + private static OnBehalfOfConfig defaultOnBehalfOfConfig() { + return new OnBehalfOfConfig().oboEnabled(oboEnabled).signingKey(signingKey).encryptionKey(encryptionKey); + } + @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .anonymousAuth(false) .users(ADMIN_USER, OBO_USER, OBO_USER_NO_PERM, HOST_MAPPING_OBO_USER) .nodeSettings( - Map.of(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true, SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_admin__all_access")) + Map.of( + SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, + true, + SECURITY_RESTAPI_ROLES_ENABLED, + ADMIN_USER.getRoleNames(), + SECURITY_RESTAPI_ADMIN_ENABLED, + true, + "plugins.security.unsupported.restapi.allow_securityconfig_modification", + true + ) ) .authc(AUTHC_HTTPBASIC_INTERNAL) .rolesMapping(new RolesMapping(HOST_MAPPING_ROLE).hostIPs(HOST_MAPPING_IP)) - .onBehalfOf(new OnBehalfOfConfig().oboEnabled(oboEnabled).signingKey(signingKey).encryptionKey(encryptionKey)) + .onBehalfOf(defaultOnBehalfOfConfig()) .build(); + @Before + public void before() { + patchOnBehalfOfConfig(defaultOnBehalfOfConfig()); + } + @Test public void shouldAuthenticateWithOBOTokenEndPoint() { String oboToken = generateOboToken(ADMIN_USER_NAME, DEFAULT_PASSWORD); @@ -196,7 +222,7 @@ public void shouldNotAuthenticateWithInvalidDurationSeconds() { client.confirmCorrectCredentials(ADMIN_USER_NAME); TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_DESCRIPTION_WITH_INVALID_DURATIONSECONDS); response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); - assertThat(response.getTextFromJsonBody("/error"), equalTo("durationSeconds must be an integer.")); + assertThat(response.getTextFromJsonBody("/error"), equalTo("durationSeconds must be a number.")); } } @@ -210,6 +236,44 @@ public void shouldNotAuthenticateWithInvalidAPIParameter() { } } + @Test + public void shouldNotAllowTokenWhenOboIsDisabled() { + final String oboToken = generateOboToken(OBO_USER_NAME_WITH_PERM, DEFAULT_PASSWORD); + final Header oboHeader = new BasicHeader("Authorization", "Bearer " + oboToken); + authenticateWithOboToken(oboHeader, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_OK); + + // Disable OBO via config and see that the authenticator doesn't authorize + patchOnBehalfOfConfig(defaultOnBehalfOfConfig().oboEnabled(false)); + authenticateWithOboToken(oboHeader, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_UNAUTHORIZED); + + // Reenable OBO via config and see that the authenticator is working again + patchOnBehalfOfConfig(defaultOnBehalfOfConfig().oboEnabled(true)); + authenticateWithOboToken(oboHeader, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_OK); + } + + @Test + public void oboSigningCheckChangeIsDetected() { + final String oboTokenOrignalKey = generateOboToken(OBO_USER_NAME_WITH_PERM, DEFAULT_PASSWORD); + final Header oboHeaderOriginalKey = new BasicHeader("Authorization", "Bearer " + oboTokenOrignalKey); + authenticateWithOboToken(oboHeaderOriginalKey, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_OK); + + // Change the signing key + patchOnBehalfOfConfig(defaultOnBehalfOfConfig().signingKey(alternativeSigningKey)); + + // Original key should no longer work + authenticateWithOboToken(oboHeaderOriginalKey, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_UNAUTHORIZED); + + // Generate new key, check that it is valid + final String oboTokenOtherKey = generateOboToken(OBO_USER_NAME_WITH_PERM, DEFAULT_PASSWORD); + final Header oboHeaderOtherKey = new BasicHeader("Authorization", "Bearer " + oboTokenOtherKey); + authenticateWithOboToken(oboHeaderOtherKey, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_OK); + + // Change back to the original signing key and the original key still works, and the new key doesn't + patchOnBehalfOfConfig(defaultOnBehalfOfConfig()); + authenticateWithOboToken(oboHeaderOriginalKey, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_OK); + authenticateWithOboToken(oboHeaderOtherKey, OBO_USER_NAME_WITH_PERM, HttpStatus.SC_UNAUTHORIZED); + } + private String generateOboToken(String username, String password) { try (TestRestClient client = cluster.getRestClient(username, password)) { client.confirmCorrectCredentials(username); @@ -233,4 +297,19 @@ private void authenticateWithOboToken(Header authHeader, String expectedUsername } } } + + private void patchOnBehalfOfConfig(final OnBehalfOfConfig oboConfig) { + try (final TestRestClient adminClient = cluster.getRestClient(cluster.getAdminCertificate())) { + final XContentBuilder configBuilder = XContentFactory.jsonBuilder(); + configBuilder.value(oboConfig); + + final String patchBody = "[{ \"op\": \"replace\", \"path\": \"/config/dynamic/on_behalf_of\", \"value\":" + + configBuilder.toString() + + "}]"; + final var response = adminClient.patch("_plugins/_security/api/securityconfig", patchBody); + response.assertStatusCode(HttpStatus.SC_OK); + } catch (final IOException ex) { + throw new RuntimeException(ex); + } + } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index b61c9db74f..de7693e393 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -29,6 +29,7 @@ // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions import com.google.common.collect.Lists; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.search.QueryCachingPolicy; @@ -75,12 +76,15 @@ import org.opensearch.extensions.ExtensionsManager; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; +import org.opensearch.identity.Subject; +import org.opensearch.identity.noop.NoopSubject; import org.opensearch.index.IndexModule; import org.opensearch.index.cache.query.QueryCache; import org.opensearch.indices.IndicesService; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.ClusterPlugin; import org.opensearch.plugins.ExtensionAwarePlugin; +import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; @@ -121,6 +125,7 @@ import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.identity.SecurityTokenManager; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; @@ -207,7 +212,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin ClusterPlugin, MapperPlugin, // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings - ExtensionAwarePlugin + ExtensionAwarePlugin, + IdentityPlugin // CS-ENFORCE-SINGLE { @@ -234,6 +240,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SslExceptionHandler sslExceptionHandler; private volatile Client localClient; private final boolean disabled; + private volatile SecurityTokenManager tokenManager; private volatile DynamicConfigFactory dcf; private final List demoCertHashes = new ArrayList(3); private volatile SecurityFilter sf; @@ -561,9 +568,7 @@ public List getRestHandlers( principalExtractor ) ); - CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool, Objects.requireNonNull(cs)); - dcf.registerDCFListener(cobot); - handlers.add(cobot); + handlers.add(new CreateOnBehalfOfTokenAction(tokenManager)); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -1035,6 +1040,7 @@ public Collection createComponents( final XFFResolver xffResolver = new XFFResolver(threadPool); backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool); + tokenManager = new SecurityTokenManager(cs, threadPool, userService); final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); @@ -1084,6 +1090,7 @@ public Collection createComponents( dcf.registerDCFListener(evaluator); dcf.registerDCFListener(restLayerEvaluator); dcf.registerDCFListener(securityRestHandler); + dcf.registerDCFListener(tokenManager); if (!(auditLog instanceof NullAuditLog)) { // Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog dcf.registerDCFListener(auditLog); @@ -1935,6 +1942,17 @@ private static String handleKeyword(final String field) { return field; } + @Override + public Subject getSubject() { + // Not supported + return new NoopSubject(); + } + + @Override + public SecurityTokenManager getTokenManager() { + return tokenManager; + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java index a885a42ab2..0863fee552 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -15,31 +15,21 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.greenrobot.eventbus.Subscribe; import org.opensearch.client.node.NodeClient; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.identity.tokens.OnBehalfOfClaims; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.NamedRoute; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.core.rest.RestStatus; -import org.opensearch.security.authtoken.jwt.JwtVendor; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigModel; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; +import org.opensearch.security.identity.SecurityTokenManager; import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -51,42 +41,16 @@ public class CreateOnBehalfOfTokenAction extends BaseRestHandler { "/_plugins/_security/api" ); - private JwtVendor vendor; - private final ThreadPool threadPool; - private final ClusterService clusterService; - - private ConfigModel configModel; - - public static final Integer OBO_DEFAULT_EXPIRY_SECONDS = 5 * 60; - public static final Integer OBO_MAX_EXPIRY_SECONDS = 10 * 60; - + public static final long OBO_DEFAULT_EXPIRY_SECONDS = 5 * 60; + public static final long OBO_MAX_EXPIRY_SECONDS = 10 * 60; public static final String DEFAULT_SERVICE = "self-issued"; - protected final Logger log = LogManager.getLogger(this.getClass()); + private static final Logger LOG = LogManager.getLogger(CreateOnBehalfOfTokenAction.class); - @Subscribe - public void onConfigModelChanged(final ConfigModel configModel) { - this.configModel = configModel; - } + private final SecurityTokenManager securityTokenManager; - @Subscribe - public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { - final Settings settings = dcm.getDynamicOnBehalfOfSettings(); - - final Boolean enabled = Boolean.parseBoolean(settings.get("enabled")); - final String signingKey = settings.get("signing_key"); - final String encryptionKey = settings.get("encryption_key"); - - if (!Boolean.FALSE.equals(enabled) && signingKey != null && encryptionKey != null) { - this.vendor = new JwtVendor(settings, Optional.empty()); - } else { - this.vendor = null; - } - } - - public CreateOnBehalfOfTokenAction(final Settings settings, final ThreadPool threadPool, final ClusterService clusterService) { - this.threadPool = threadPool; - this.clusterService = clusterService; + public CreateOnBehalfOfTokenAction(final SecurityTokenManager securityTokenManager) { + this.securityTokenManager = securityTokenManager; } @Override @@ -116,45 +80,31 @@ public void accept(final RestChannel channel) throws Exception { final XContentBuilder builder = channel.newBuilder(); BytesRestResponse response; try { - if (vendor == null) { + if (!securityTokenManager.issueOnBehalfOfTokenAllowed()) { channel.sendResponse( new BytesRestResponse( - RestStatus.SERVICE_UNAVAILABLE, + RestStatus.BAD_REQUEST, "The OnBehalfOf token generating API has been disabled, see {link to doc} for more information on this feature." /* TODO: Update the link to the documentation website */ ) ); return; } - final String clusterIdentifier = clusterService.getClusterName().value(); - final Map requestBody = request.contentOrSourceParamParser().map(); validateRequestParameters(requestBody); - Integer tokenDuration = parseAndValidateDurationSeconds(requestBody.get(InputParameters.DURATION.paramName)); + long tokenDuration = parseAndValidateDurationSeconds(requestBody.get(InputParameters.DURATION.paramName)); tokenDuration = Math.min(tokenDuration, OBO_MAX_EXPIRY_SECONDS); final String description = (String) requestBody.getOrDefault(InputParameters.DESCRIPTION.paramName, null); - final String service = (String) requestBody.getOrDefault(InputParameters.SERVICE.paramName, DEFAULT_SERVICE); - final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final Set mappedRoles = mapRoles(user); + final var token = securityTokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims(service, tokenDuration)); builder.startObject(); - builder.field("user", user.getName()); - - final String token = vendor.createJwt( - clusterIdentifier, - user.getName(), - service, - tokenDuration, - mappedRoles.stream().collect(Collectors.toList()), - user.getRoles().stream().collect(Collectors.toList()), - false - ); - builder.field("authenticationToken", token); - builder.field("durationSeconds", tokenDuration); + builder.field("user", token.getSubject()); + builder.field("authenticationToken", token.getCompleteToken()); + builder.field("durationSeconds", token.getExpiresInSeconds()); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); @@ -162,7 +112,7 @@ public void accept(final RestChannel channel) throws Exception { builder.startObject().field("error", iae.getMessage()).endObject(); response = new BytesRestResponse(RestStatus.BAD_REQUEST, builder); } catch (final Exception exception) { - log.error("Unexpected error occurred: ", exception); + LOG.error("Unexpected error occurred: ", exception); builder.startObject().field("error", "An unexpected error occurred. Please check the input and try again.").endObject(); @@ -186,10 +136,6 @@ private InputParameters(final String paramName) { } } - private Set mapRoles(final User user) { - return this.configModel.mapSecurityRoles(user, null); - } - private void validateRequestParameters(final Map requestBody) throws IllegalArgumentException { for (final String key : requestBody.keySet()) { Arrays.stream(InputParameters.values()) @@ -199,7 +145,7 @@ private void validateRequestParameters(final Map requestBody) th } } - private Integer parseAndValidateDurationSeconds(final Object durationObj) throws IllegalArgumentException { + private long parseAndValidateDurationSeconds(final Object durationObj) throws IllegalArgumentException { if (durationObj == null) { return OBO_DEFAULT_EXPIRY_SECONDS; } @@ -208,9 +154,9 @@ private Integer parseAndValidateDurationSeconds(final Object durationObj) throws return (Integer) durationObj; } else if (durationObj instanceof String) { try { - return Integer.parseInt((String) durationObj); + return Long.parseLong((String) durationObj); } catch (final NumberFormatException ignored) {} } - throw new IllegalArgumentException("durationSeconds must be an integer."); + throw new IllegalArgumentException("durationSeconds must be a number."); } } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/ExpiringBearerAuthToken.java b/src/main/java/org/opensearch/security/authtoken/jwt/ExpiringBearerAuthToken.java new file mode 100644 index 0000000000..a0879cd4da --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/ExpiringBearerAuthToken.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.authtoken.jwt; + +import java.util.Date; + +import org.opensearch.identity.tokens.BearerAuthToken; + +public class ExpiringBearerAuthToken extends BearerAuthToken { + private final String subject; + private final Date expiry; + private final long expiresInSeconds; + + public ExpiringBearerAuthToken(final String serializedToken, final String subject, final Date expiry, final long expiresInSeconds) { + super(serializedToken); + this.subject = subject; + this.expiry = expiry; + this.expiresInSeconds = expiresInSeconds; + } + + public String getSubject() { + return subject; + } + + public Date getExpiry() { + return expiry; + } + + public long getExpiresInSeconds() { + return expiresInSeconds; + } +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 754d961883..6340688607 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -46,7 +46,6 @@ public class JwtVendor { private final JWSSigner signer; private final LongSupplier timeProvider; private final EncryptionDecryptionUtil encryptionDecryptionUtil; - private static final Integer DEFAULT_EXPIRY_SECONDS = 300; private static final Integer MAX_EXPIRY_SECONDS = 600; public JwtVendor(final Settings settings, final Optional timeProvider) { @@ -59,11 +58,7 @@ public JwtVendor(final Settings settings, final Optional timeProvi } else { this.encryptionDecryptionUtil = new EncryptionDecryptionUtil(settings.get("encryption_key")); } - if (timeProvider.isPresent()) { - this.timeProvider = timeProvider.get(); - } else { - this.timeProvider = () -> System.currentTimeMillis(); - } + this.timeProvider = timeProvider.orElse(System::currentTimeMillis); } /* @@ -72,15 +67,15 @@ public JwtVendor(final Settings settings, final Optional timeProvi * PublicKeyUse: SIGN * Encryption Algorithm: HS512 * */ - static Tuple createJwkFromSettings(Settings settings) { + static Tuple createJwkFromSettings(final Settings settings) { final OctetSequenceKey key; if (!isKeyNull(settings, "signing_key")) { - String signingKey = settings.get("signing_key"); + final String signingKey = settings.get("signing_key"); key = new OctetSequenceKey.Builder(Base64.getDecoder().decode(signingKey)).algorithm(JWSAlgorithm.HS512) .keyUse(KeyUse.SIGNATURE) .build(); } else { - Settings jwkSettings = settings.getAsSettings("jwt").getAsSettings("key"); + final Settings jwkSettings = settings.getAsSettings("jwt").getAsSettings("key"); if (jwkSettings.isEmpty()) { throw new OpenSearchException( @@ -88,7 +83,7 @@ static Tuple createJwkFromSettings(Settings settings) { ); } - String signingKey = jwkSettings.get("k"); + final String signingKey = jwkSettings.get("k"); key = new OctetSequenceKey.Builder(Base64.getDecoder().decode(signingKey)).algorithm(JWSAlgorithm.HS512) .keyUse(KeyUse.SIGNATURE) .build(); @@ -96,21 +91,22 @@ static Tuple createJwkFromSettings(Settings settings) { try { return new Tuple<>(key, new MACSigner(key)); - } catch (KeyLengthException kle) { + } catch (final KeyLengthException kle) { throw new OpenSearchException(kle); } } - public String createJwt( - String issuer, - String subject, - String audience, - Integer expirySeconds, - List roles, - List backendRoles, - boolean includeBackendRoles + public ExpiringBearerAuthToken createJwt( + final String issuer, + final String subject, + final String audience, + final long requestedExpirySeconds, + final List roles, + final List backendRoles, + final boolean includeBackendRoles ) throws JOSEException, ParseException { - final Date now = new Date(timeProvider.getAsLong()); + final long currentTimeMs = timeProvider.getAsLong(); + final Date now = new Date(currentTimeMs); final JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); claimsBuilder.issuer(issuer); @@ -119,28 +115,22 @@ public String createJwt( claimsBuilder.audience(audience); claimsBuilder.notBeforeTime(now); - if (expirySeconds > MAX_EXPIRY_SECONDS) { - throw new IllegalArgumentException( - "The provided expiration time exceeds the maximum allowed duration of " + MAX_EXPIRY_SECONDS + " seconds" - ); - } - - expirySeconds = (expirySeconds == null) ? DEFAULT_EXPIRY_SECONDS : Math.min(expirySeconds, MAX_EXPIRY_SECONDS); + final long expirySeconds = Math.min(requestedExpirySeconds, MAX_EXPIRY_SECONDS); if (expirySeconds <= 0) { throw new IllegalArgumentException("The expiration time should be a positive integer"); } - final Date expiryTime = new Date(timeProvider.getAsLong() + expirySeconds * 1000); + final Date expiryTime = new Date(currentTimeMs + expirySeconds * 1000); claimsBuilder.expirationTime(expiryTime); if (roles != null) { - String listOfRoles = String.join(",", roles); + final String listOfRoles = String.join(",", roles); claimsBuilder.claim("er", encryptionDecryptionUtil.encrypt(listOfRoles)); } else { throw new IllegalArgumentException("Roles cannot be null"); } if (includeBackendRoles && backendRoles != null) { - String listOfBackendRoles = String.join(",", backendRoles); + final String listOfBackendRoles = String.join(",", backendRoles); claimsBuilder.claim("br", listOfBackendRoles); } @@ -156,6 +146,6 @@ public String createJwt( ); } - return signedJwt.serialize(); + return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime, expirySeconds); } } diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index 17ea48f46c..04ad8f7420 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -461,7 +461,7 @@ public Map> getConfigurationsFromIndex( throw new OpenSearchException(e); } - if (logComplianceEvent && auditLog.getComplianceConfig().isEnabled()) { + if (logComplianceEvent && auditLog.getComplianceConfig() != null && auditLog.getComplianceConfig().isEnabled()) { CType configurationType = configTypes.iterator().next(); Map fields = new HashMap(); fields.put(configurationType.toLCString(), Strings.toString(MediaTypeRegistry.JSON, retVal.get(configurationType))); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 5253504bb8..449762c8ff 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -166,11 +166,7 @@ void generateAuthToken(final RestChannel channel, final SecurityConfiguration se try { final var username = securityConfiguration.entityName(); final var authToken = userService.generateAuthToken(username); - if (!Strings.isNullOrEmpty(authToken)) { - ok(channel, "'" + username + "' authtoken generated " + authToken); - } else { - badRequest(channel, "'" + username + "' authtoken failed to be created."); - } + ok(channel, "'" + username + "' authtoken generated " + authToken); } catch (final UserServiceException e) { badRequest(channel, e.getMessage()); } diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index a3e0e1ba4e..f493b7c919 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -11,25 +11,24 @@ package org.opensearch.security.http; +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; +import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints; + import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.JwtParserBuilder; -import io.jsonwebtoken.security.WeakKeyException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.OpenSearchException; import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; @@ -43,12 +42,14 @@ import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.KeyUtils; -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.security.WeakKeyException; public class OnBehalfOfAuthenticator implements HTTPAuthenticator { + private static final int MINIMUM_SIGNING_KEY_BIT_LENGTH = 512; private static final String REGEX_PATH_PREFIX = "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" + "(.*)"; private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX); @@ -80,17 +81,26 @@ public JwtParser run() { return builder.build(); } }); - this.clusterName = clusterName; this.encryptionUtil = new EncryptionDecryptionUtil(encryptionKey); } private JwtParserBuilder initParserBuilder(final String signingKey) { - JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); + if (signingKey == null) { + throw new OpenSearchSecurityException("Unable to find on behalf of authenticator signing_key"); + } - if (jwtParserBuilder == null) { - throw new OpenSearchSecurityException("Unable to find on behalf of authenticator signing key"); + final int signingKeyLengthBits = signingKey.length() * 8; + if (signingKeyLengthBits < MINIMUM_SIGNING_KEY_BIT_LENGTH) { + throw new OpenSearchSecurityException( + "Signing key size was " + + signingKeyLengthBits + + " bits, which is not secure enough. Please use a signing_key with a size >= " + + MINIMUM_SIGNING_KEY_BIT_LENGTH + + " bits." + ); } + JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); return jwtParserBuilder; } diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java new file mode 100644 index 0000000000..9f4ffecf57 --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -0,0 +1,141 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.identity; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import joptsimple.internal.Strings; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.greenrobot.eventbus.Subscribe; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.identity.Subject; +import org.opensearch.identity.noop.NoopSubject; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.OnBehalfOfClaims; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.security.user.UserService; +import org.opensearch.threadpool.ThreadPool; + +/** + * This class is the Security Plugin's implementation of the TokenManager used by all Identity Plugins. + * It handles the issuance of both Service Account Tokens and On Behalf Of tokens. + */ +public class SecurityTokenManager implements TokenManager { + private static final Logger logger = LogManager.getLogger(SecurityTokenManager.class); + + private final ClusterService cs; + private final ThreadPool threadPool; + private final UserService userService; + + private JwtVendor jwtVendor = null; + private ConfigModel configModel = null; + + public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) { + this.cs = cs; + this.threadPool = threadPool; + this.userService = userService; + } + + @Subscribe + public void onConfigModelChanged(final ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { + final Settings oboSettings = dcm.getDynamicOnBehalfOfSettings(); + final Boolean enabled = oboSettings.getAsBoolean("enabled", false); + if (enabled) { + jwtVendor = createJwtVendor(oboSettings); + } else { + jwtVendor = null; + } + } + + /** For testing */ + JwtVendor createJwtVendor(final Settings settings) { + try { + return new JwtVendor(settings, Optional.empty()); + } catch (final Exception ex) { + logger.error("Unable to create the JwtVendor instance", ex); + return null; + } + } + + public boolean issueOnBehalfOfTokenAllowed() { + return jwtVendor != null && configModel != null; + } + + @Override + public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final OnBehalfOfClaims claims) { + if (!issueOnBehalfOfTokenAllowed()) { + // TODO: link that doc! + throw new OpenSearchSecurityException( + "The OnBehalfOf token generation is not enabled, see {link to doc} for more information on this feature." + ); + } + + if (subject != null && !(subject instanceof NoopSubject)) { + logger.warn("Unsupported subject for OnBehalfOfToken token generation, {}", subject); + throw new IllegalArgumentException("Unsupported subject to generate OnBehalfOfToken"); + } + + if (Strings.isNullOrEmpty(claims.getAudience())) { + throw new IllegalArgumentException("Claims must be supplied with an audience value"); + } + + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + if (user == null) { + throw new OpenSearchSecurityException("Unsupported user to generate OnBehalfOfToken"); + } + + final TransportAddress callerAddress = null; /* OBO tokens must not roles based on location from network address */ + final Set mappedRoles = configModel.mapSecurityRoles(user, callerAddress); + + try { + return jwtVendor.createJwt( + cs.getClusterName().value(), + user.getName(), + claims.getAudience(), + claims.getExpiration(), + mappedRoles.stream().collect(Collectors.toList()), + user.getRoles().stream().collect(Collectors.toList()), + false + ); + } catch (final Exception ex) { + logger.error("Error creating OnBehalfOfToken for " + user.getName(), ex); + throw new OpenSearchSecurityException("Unable to generate OnBehalfOfToken"); + } + } + + @Override + public AuthToken issueServiceAccountToken(final String serviceId) { + try { + return userService.generateAuthToken(serviceId); + } catch (final Exception e) { + logger.error("Error creating sevice final account auth token, service " + serviceId, e); + throw new OpenSearchSecurityException("Unable to issue service account token"); + } + } +} diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index 2d87bd8248..e7c30b97b0 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -41,6 +41,8 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.securityconf.DynamicConfigFactory; @@ -245,7 +247,7 @@ public static String generatePassword() { * @param accountName A string representing the name of the account * @return A string auth token */ - public String generateAuthToken(String accountName) throws IOException { + public AuthToken generateAuthToken(String accountName) throws IOException { final SecurityDynamicConfiguration internalUsersConfiguration = load(getUserConfigName(), false); @@ -286,7 +288,7 @@ public String generateAuthToken(String accountName) throws IOException { saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); authToken = Base64.getUrlEncoder().encodeToString((accountName + ":" + plainTextPassword).getBytes(StandardCharsets.UTF_8)); - return authToken; + return new BasicAuthToken(authToken); } catch (JsonProcessingException ex) { throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index dd4dd19aa2..9c51dd714b 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -40,6 +40,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; @@ -102,9 +103,9 @@ public void testCreateJwtWithRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); + final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, false); - SignedJWT signedJWT = SignedJWT.parse(encodedJwt); + SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); assertThat(signedJWT.getJWTClaimsSet().getClaims().get("iss"), equalTo("cluster_0")); assertThat(signedJWT.getJWTClaimsSet().getClaims().get("sub"), equalTo("admin")); @@ -139,9 +140,9 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { // CS-ENFORCE-SINGLE .build(); final JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); - SignedJWT signedJWT = SignedJWT.parse(encodedJwt); + SignedJWT signedJWT = SignedJWT.parse(authToken.getCompleteToken()); assertThat(signedJWT.getJWTClaimsSet().getClaims().get("iss"), equalTo("cluster_0")); assertThat(signedJWT.getJWTClaimsSet().getClaims().get("sub"), equalTo("admin")); @@ -183,23 +184,16 @@ public void testCreateJwtWithExceededExpiry() throws Exception { String audience = "audience_0"; List roles = List.of("IT", "HR"); List backendRoles = List.of("Sales", "Support"); - int expirySeconds = 900; + int expirySeconds = 900_000; LongSupplier currentTime = () -> (long) 100; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); - final Throwable exception = assertThrows(RuntimeException.class, () -> { - try { - jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); - } catch (final Exception e) { - throw new RuntimeException(e); - } - }); - assertEquals( - "java.lang.IllegalArgumentException: The provided expiration time exceeds the maximum allowed duration of 600 seconds", - exception.getMessage() - ); + final ExpiringBearerAuthToken authToken = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles, true); + // Expiry is a hint, the max value is controlled by the JwtVendor and reduced as is seen fit. + assertThat(authToken.getExpiresInSeconds(), not(equalTo(expirySeconds))); + assertThat(authToken.getExpiresInSeconds(), equalTo(600L)); } @Test diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 4215aac0ad..9f2c5ad48a 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -107,7 +107,7 @@ public void testNoKey() { false ) ); - assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); + assertThat(exception.getMessage(), equalTo("Unable to find on behalf of authenticator signing_key")); } @Test @@ -115,13 +115,16 @@ public void testEmptyKey() { Exception exception = assertThrows( RuntimeException.class, () -> extractCredentialsFromJwtHeader( - null, + "", claimsEncryptionKey, Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy"), false ) ); - assertTrue(exception.getMessage().contains("Unable to find on behalf of authenticator signing key")); + assertThat( + exception.getMessage(), + equalTo("Signing key size was 0 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") + ); } @Test @@ -135,7 +138,10 @@ public void testBadKey() { false ) ); - assertTrue(exception.getMessage().contains("The specified key byte array is 80 bits")); + assertThat( + exception.getMessage(), + equalTo("Signing key size was 128 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") + ); } @Test @@ -145,7 +151,10 @@ public void testWeakKeyExceptionHandling() throws Exception { OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, "testCluster"); fail("Expected WeakKeyException"); } catch (OpenSearchSecurityException e) { - assertTrue("Expected error message to contain WeakKeyException", e.getMessage().contains("WeakKeyException")); + assertThat( + e.getMessage(), + equalTo("Signing key size was 56 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") + ); } } diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java new file mode 100644 index 0000000000..bc3f3f9732 --- /dev/null +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -0,0 +1,247 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.identity; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.ClusterName; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.identity.Subject; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.OnBehalfOfClaims; +import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.security.user.UserService; +import org.opensearch.threadpool.ThreadPool; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SecurityTokenManagerTest { + + private SecurityTokenManager tokenManager; + + @Mock + private JwtVendor jwtVendor; + @Mock + private ClusterService cs; + @Mock + private ThreadPool threadPool; + @Mock + private UserService userService; + + @Before + public void setup() { + tokenManager = spy(new SecurityTokenManager(cs, threadPool, userService)); + } + + @After + public void after() { + verifyNoMoreInteractions(cs); + verifyNoMoreInteractions(threadPool); + verifyNoMoreInteractions(userService); + } + + public void onConfigModelChanged_oboNotSupported() { + final ConfigModel configModel = mock(ConfigModel.class); + + tokenManager.onConfigModelChanged(configModel); + + assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(false)); + verifyNoMoreInteractions(configModel); + } + + @Test + public void onDynamicConfigModelChanged_JwtVendorEnabled() { + final ConfigModel configModel = mock(ConfigModel.class); + final DynamicConfigModel mockConfigModel = createMockJwtVendorInTokenManager(); + + tokenManager.onConfigModelChanged(configModel); + + assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(true)); + verify(mockConfigModel).getDynamicOnBehalfOfSettings(); + verifyNoMoreInteractions(configModel); + } + + @Test + public void onDynamicConfigModelChanged_JwtVendorDisabled() { + final Settings settings = Settings.builder().put("enabled", false).build(); + final DynamicConfigModel dcm = mock(DynamicConfigModel.class); + when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); + tokenManager.onDynamicConfigModelChanged(dcm); + + assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(false)); + verify(dcm).getDynamicOnBehalfOfSettings(); + verify(tokenManager, never()).createJwtVendor(any()); + } + + /** Creates the jwt vendor and returns a mock for validation if needed */ + private DynamicConfigModel createMockJwtVendorInTokenManager() { + final Settings settings = Settings.builder().put("enabled", true).build(); + final DynamicConfigModel dcm = mock(DynamicConfigModel.class); + when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); + doAnswer((invocation) -> jwtVendor).when(tokenManager).createJwtVendor(settings); + tokenManager.onDynamicConfigModelChanged(dcm); + return dcm; + } + + @Test + public void issueServiceAccountToken_error() throws Exception { + final String expectedAccountName = "abc-123"; + when(userService.generateAuthToken(expectedAccountName)).thenThrow(new IOException("foobar")); + + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> tokenManager.issueServiceAccountToken(expectedAccountName) + ); + assertThat(exception.getMessage(), equalTo("Unable to issue service account token")); + + verify(userService).generateAuthToken(expectedAccountName); + } + + @Test + public void issueServiceAccountToken_success() throws Exception { + final String expectedAccountName = "abc-123"; + final AuthToken authToken = mock(AuthToken.class); + when(userService.generateAuthToken(expectedAccountName)).thenReturn(authToken); + + final AuthToken token = tokenManager.issueServiceAccountToken(expectedAccountName); + + assertThat(token, equalTo(authToken)); + + verify(userService).generateAuthToken(expectedAccountName); + } + + @Test + public void issueOnBehalfOfToken_notEnabledOnCluster() { + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> tokenManager.issueOnBehalfOfToken(null, null) + ); + assertThat( + exception.getMessage(), + equalTo("The OnBehalfOf token generation is not enabled, see {link to doc} for more information on this feature.") + ); + } + + @Test + public void issueOnBehalfOfToken_unsupportedSubjectType() { + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> tokenManager.issueOnBehalfOfToken(mock(Subject.class), null) + ); + assertThat(exception.getMessage(), equalTo("Unsupported subject to generate OnBehalfOfToken")); + } + + @Test + public void issueOnBehalfOfToken_missingAudience() { + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims(null, 450L)) + ); + assertThat(exception.getMessage(), equalTo("Claims must be supplied with an audience value")); + } + + @Test + public void issueOnBehalfOfToken_cannotFindUserInThreadContext() { + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + when(threadPool.getThreadContext()).thenReturn(threadContext); + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 450L)) + ); + assertThat(exception.getMessage(), equalTo("Unsupported user to generate OnBehalfOfToken")); + + verify(threadPool).getThreadContext(); + } + + @Test + public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { + doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName(); + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null)); + when(threadPool.getThreadContext()).thenReturn(threadContext); + final ConfigModel configModel = mock(ConfigModel.class); + tokenManager.onConfigModelChanged(configModel); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); + + createMockJwtVendorInTokenManager(); + + when(jwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenThrow( + new RuntimeException("foobar") + ); + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 450L)) + ); + assertThat(exception.getMessage(), equalTo("Unable to generate OnBehalfOfToken")); + + verify(cs).getClusterName(); + verify(threadPool).getThreadContext(); + } + + @Test + public void issueOnBehalfOfToken_success() throws Exception { + doAnswer(invockation -> new ClusterName("cluster17")).when(cs).getClusterName(); + doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon", List.of(), null)); + when(threadPool.getThreadContext()).thenReturn(threadContext); + final ConfigModel configModel = mock(ConfigModel.class); + tokenManager.onConfigModelChanged(configModel); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); + + createMockJwtVendorInTokenManager(); + + final ExpiringBearerAuthToken authToken = mock(ExpiringBearerAuthToken.class); + when(jwtVendor.createJwt(any(), anyString(), anyString(), anyLong(), any(), any(), anyBoolean())).thenReturn(authToken); + final AuthToken returnedToken = tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 450L)); + + assertThat(returnedToken, equalTo(authToken)); + + verify(cs).getClusterName(); + verify(threadPool).getThreadContext(); + } +} From 5de968656d409c8bb0da23ad06f8308b0c9b1443 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Thu, 2 Nov 2023 15:38:12 -0400 Subject: [PATCH 14/23] Update to Gradle 8.4 (#3638) Signed-off-by: Andriy Redko --- bwc-test/gradle/wrapper/gradle-wrapper.properties | 4 ++-- bwc-test/gradlew | 14 +++++++------- gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 14 +++++++------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bwc-test/gradle/wrapper/gradle-wrapper.properties b/bwc-test/gradle/wrapper/gradle-wrapper.properties index 596898c165..3999f7f3f6 100644 --- a/bwc-test/gradle/wrapper/gradle-wrapper.properties +++ b/bwc-test/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225 +distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae diff --git a/bwc-test/gradlew b/bwc-test/gradlew index 0adc8e1a53..1aa94a4269 100755 --- a/bwc-test/gradlew +++ b/bwc-test/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 596898c165..3999f7f3f6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225 +distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae diff --git a/gradlew b/gradlew index 0adc8e1a53..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ From 5f1058ad0418d8a8711d94fa7fdc467788e2de6f Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Fri, 3 Nov 2023 10:56:42 -0400 Subject: [PATCH 15/23] Add Java 11/17/21 matrix for plugin install, test and integration test checks (#3641) Signed-off-by: Andriy Redko --- .github/workflows/ci.yml | 4 ++-- .github/workflows/integration-tests.yml | 2 +- .github/workflows/plugin_install.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f08695275..009cfc8fe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: matrix: gradle_task: ${{ fromJson(needs.generate-test-list.outputs.separateTestsNames) }} platform: [windows-latest, ubuntu-latest] - jdk: [11, 17] + jdk: [11, 17, 21] runs-on: ${{ matrix.platform }} steps: @@ -110,7 +110,7 @@ jobs: strategy: fail-fast: false matrix: - jdk: [11, 17] + jdk: [11, 17, 21] platform: [ubuntu-latest] # Removed windows https://github.com/opensearch-project/security/issues/3423 runs-on: ${{ matrix.platform }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d514cef017..4c2eddcfbc 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - jdk: [11, 17] + jdk: [11, 17, 21] test-run: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] steps: diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml index 39901689be..ae570a9df8 100644 --- a/.github/workflows/plugin_install.yml +++ b/.github/workflows/plugin_install.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - jdk: [11, 17] + jdk: [11, 17, 21] runs-on: ${{ matrix.os }} steps: From 823a31192bf166490bda92c54069a8987761ee97 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 3 Nov 2023 12:09:16 -0400 Subject: [PATCH 16/23] OnBehalfOf tokens feature is disabled by default (#3643) Signed-off-by: Craig Perkins Signed-off-by: Craig Perkins --- .../security/securityconf/impl/v6/ConfigV6.java | 2 +- .../security/securityconf/impl/v7/ConfigV7.java | 4 +++- .../security/securityconf/impl/v6/ConfigV6Test.java | 10 ++++++++++ .../security/securityconf/impl/v7/ConfigV7Test.java | 10 ++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java index 79a745c54e..0c95e56bd1 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java @@ -359,7 +359,7 @@ public String toString() { public static class OnBehalfOfSettings { @JsonProperty("enabled") - private Boolean oboEnabled = Boolean.TRUE; + private Boolean oboEnabled = Boolean.FALSE; @JsonProperty("signing_key") private String signingKey; @JsonProperty("encryption_key") diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index 8fb2199ddf..faeb5d2432 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -148,6 +148,8 @@ public String toString() { + authc + ", authz=" + authz + + ", on_behalf_of=" + + on_behalf_of + "]"; } } @@ -482,7 +484,7 @@ public String toString() { public static class OnBehalfOfSettings { @JsonProperty("enabled") - private Boolean oboEnabled = Boolean.TRUE; + private Boolean oboEnabled = Boolean.FALSE; @JsonProperty("signing_key") private String signingKey; @JsonProperty("encryption_key") diff --git a/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java b/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java index 245127995e..f9febb3bda 100644 --- a/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java +++ b/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java @@ -106,4 +106,14 @@ public void testDashboards() throws Exception { assertEquals(kibana, DefaultObjectMapper.readTree(json)); assertEquals(kibana, DefaultObjectMapper.readValue(json, ConfigV6.Kibana.class)); } + + @Test + public void testOnBehalfOfSettings() { + ConfigV6.OnBehalfOfSettings oboSettings; + + oboSettings = new ConfigV6.OnBehalfOfSettings(); + Assert.assertEquals(oboSettings.getOboEnabled(), Boolean.FALSE); + Assert.assertNull(oboSettings.getSigningKey()); + Assert.assertNull(oboSettings.getEncryptionKey()); + } } diff --git a/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java b/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java index 92af5aeebd..07d446074c 100644 --- a/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java +++ b/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java @@ -97,4 +97,14 @@ public void testDashboards() throws Exception { assertEquals(kibana, DefaultObjectMapper.readTree(json)); assertEquals(kibana, DefaultObjectMapper.readValue(json, ConfigV7.Kibana.class)); } + + @Test + public void testOnBehalfOfSettings() { + ConfigV7.OnBehalfOfSettings oboSettings; + + oboSettings = new ConfigV7.OnBehalfOfSettings(); + Assert.assertEquals(oboSettings.getOboEnabled(), Boolean.FALSE); + Assert.assertNull(oboSettings.getSigningKey()); + Assert.assertNull(oboSettings.getEncryptionKey()); + } } From 850b7dc959024f3868d4e874d10eb1ebbd605ee6 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 3 Nov 2023 12:15:10 -0400 Subject: [PATCH 17/23] Validate requests are not impacted during security cache invalidation (#3637) Signed-off-by: Craig Perkins Signed-off-by: Craig Perkins --- .../opensearch/security/http/AsyncTests.java | 90 +++++++++++++++++++ .../security/http/BasicAuthTests.java | 2 +- .../test/framework/TestSecurityConfig.java | 15 +++- 3 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/AsyncTests.java diff --git a/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java b/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java new file mode 100644 index 0000000000..ee46fb3905 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ + +package org.opensearch.security.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; + +import org.apache.hc.core5.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.opensearch.security.IndexOperationsHelper; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.AsyncActions; +import org.opensearch.test.framework.RolesMapping; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; + +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class AsyncTests { + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").backendRoles("admin"); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().singleNode() + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .rolesMapping(new RolesMapping(ALL_ACCESS).backendRoles("admin")) + .anonymousAuth(false) + .nodeSettings(Map.of(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED, List.of(ALL_ACCESS.getName()))) + .build(); + + @Test + public void testBulkAndCacheInvalidationMixed() throws Exception { + String indexName = "test-index"; + final String invalidateCachePath = "_plugins/_security/api/cache"; + final String nodesPath = "_nodes"; + final String bulkPath = "_bulk"; + final String document = ("{ \"index\": { \"_index\": \"" + indexName + "\" }}\n{ \"foo\": \"bar\" }\n").repeat(5); + final int parallelism = 5; + final int totalNumberOfRequests = 30; + + try (final TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + IndexOperationsHelper.createIndex(cluster, indexName); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + + List> allRequests = new ArrayList>(); + + allRequests.addAll(AsyncActions.generate(() -> { + countDownLatch.await(); + return client.delete(invalidateCachePath); + }, parallelism, totalNumberOfRequests)); + + allRequests.addAll(AsyncActions.generate(() -> { + countDownLatch.await(); + return client.postJson(bulkPath, document); + }, parallelism, totalNumberOfRequests)); + + allRequests.addAll(AsyncActions.generate(() -> { + countDownLatch.await(); + return client.get(nodesPath); + }, parallelism, totalNumberOfRequests)); + + // Make sure all requests start at the same time + countDownLatch.countDown(); + + AsyncActions.getAll(allRequests, 30, TimeUnit.SECONDS).forEach((response) -> { response.assertStatusCode(HttpStatus.SC_OK); }); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java index f6b1672bbe..1e424ab115 100644 --- a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java @@ -40,7 +40,7 @@ public class BasicAuthTests { static final User TEST_USER = new User("test_user").password("s3cret"); public static final String CUSTOM_ATTRIBUTE_NAME = "superhero"; - static final User SUPER_USER = new User("super-user").password("super-password").attr(CUSTOM_ATTRIBUTE_NAME, true); + static final User SUPER_USER = new User("super-user").password("super-password").attr(CUSTOM_ATTRIBUTE_NAME, "true"); public static final String NOT_EXISTING_USER = "not-existing-user"; public static final String INVALID_PASSWORD = "secret-password"; diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 2fd3fc474d..71a8aad545 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -261,7 +261,9 @@ public static class User implements UserCredentialsHolder, ToXContentObject { String name; private String password; List roles = new ArrayList<>(); - private Map attributes = new HashMap<>(); + List backendRoles = new ArrayList<>(); + String requestedTenant; + private Map attributes = new HashMap<>(); public User(String name) { this.name = name; @@ -282,7 +284,12 @@ public User roles(Role... roles) { return this; } - public User attr(String key, Object value) { + public User backendRoles(String... backendRoles) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + return this; + } + + public User attr(String key, String value) { this.attributes.put(key, value); return this; } @@ -315,6 +322,10 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.field("opendistro_security_roles", roleNames); } + if (!backendRoles.isEmpty()) { + xContentBuilder.field("backend_roles", backendRoles); + } + if (attributes != null && attributes.size() != 0) { xContentBuilder.field("attributes", attributes); } From 3fbeec6c808be782f09c80a9afa8997b54d01058 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Fri, 3 Nov 2023 13:13:05 -0500 Subject: [PATCH 18/23] Remove the separate gradle install for bwc-tests (#3642) Signed-off-by: Peter Nied --- .github/actions/run-bwc-suite/action.yaml | 2 +- bwc-test/gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - bwc-test/gradlew | 249 ------------------ bwc-test/gradlew.bat | 92 ------- 5 files changed, 1 insertion(+), 349 deletions(-) delete mode 100644 bwc-test/gradle/wrapper/gradle-wrapper.jar delete mode 100644 bwc-test/gradle/wrapper/gradle-wrapper.properties delete mode 100755 bwc-test/gradlew delete mode 100644 bwc-test/gradlew.bat diff --git a/.github/actions/run-bwc-suite/action.yaml b/.github/actions/run-bwc-suite/action.yaml index ad87f7316c..f05696699c 100644 --- a/.github/actions/run-bwc-suite/action.yaml +++ b/.github/actions/run-bwc-suite/action.yaml @@ -41,6 +41,7 @@ runs: with: cache-disabled: true arguments: | + -p bwc-test bwcTestSuite -Dtests.security.manager=false -Dtests.opensearch.secure=true @@ -48,7 +49,6 @@ runs: -Dtests.opensearch.password=${{ inputs.password }} -Dbwc.version.previous=${{ steps.build-previous.outputs.built-version }} -Dbwc.version.next=${{ steps.build-next.outputs.built-version }} -i - build-root-directory: bwc-test - uses: alehechka/upload-tartifact@v2 if: always() diff --git a/bwc-test/gradle/wrapper/gradle-wrapper.jar b/bwc-test/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/bwc-test/gradle/wrapper/gradle-wrapper.properties b/bwc-test/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3999f7f3f6..0000000000 --- a/bwc-test/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae diff --git a/bwc-test/gradlew b/bwc-test/gradlew deleted file mode 100755 index 1aa94a4269..0000000000 --- a/bwc-test/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://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. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/bwc-test/gradlew.bat b/bwc-test/gradlew.bat deleted file mode 100644 index 6689b85bee..0000000000 --- a/bwc-test/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega From 8b51ad065a3e45ad6f96e78d7121ca3c1d3f601d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 07:52:59 -0500 Subject: [PATCH 19/23] Bump org.apache.httpcomponents:fluent-hc from 4.5.13 to 4.5.14 (#3657) Bumps org.apache.httpcomponents:fluent-hc from 4.5.13 to 4.5.14. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.httpcomponents:fluent-hc&package-manager=gradle&previous-version=4.5.13&new-version=4.5.14)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ad7c27980c..6ab453ae4b 100644 --- a/build.gradle +++ b/build.gradle @@ -728,7 +728,7 @@ dependencies { integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14' integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14" integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14" - integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.13" + integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14" integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16" integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" From 3b1edcc99b072f2e0b0eb34c239ccefd677b5aa9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:08:58 +0000 Subject: [PATCH 20/23] Bump io.dropwizard.metrics:metrics-core from 4.2.21 to 4.2.22 (#3653) Bumps [io.dropwizard.metrics:metrics-core](https://github.com/dropwizard/metrics) from 4.2.21 to 4.2.22.
Commits
  • f560b51 [maven-release-plugin] prepare release v4.2.22
  • d0cc116 Update dependency org.mockito:mockito-core to v5.7.0 (#3701)
  • c548012 Update dependency org.glassfish.jersey:jersey-bom to v3.0.12
  • fc36854 Update dependency org.checkerframework:checker-qual to v3.40.0 (#3697)
  • 09422d3 Update jetty12.version to v12.0.3
  • 5f9698b Update dependency org.eclipse.jetty:jetty-bom to v11.0.18
  • b8cca4e Update dependency org.eclipse.jetty:jetty-bom to v10.0.18
  • beff6b0 Update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.7.10
  • ae5860d Update github/codeql-action digest to 74483a3 (#3682)
  • 62f9965 Update dependency com.rabbitmq:amqp-client to v5.20.0 (#3678)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=io.dropwizard.metrics:metrics-core&package-manager=gradle&previous-version=4.2.21&new-version=4.2.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6ab453ae4b..2842c678b0 100644 --- a/build.gradle +++ b/build.gradle @@ -639,7 +639,7 @@ dependencies { runtimeOnly 'com.google.j2objc:j2objc-annotations:2.8' compileOnly 'com.google.code.findbugs:jsr305:3.0.2' runtimeOnly 'org.lz4:lz4-java:1.8.0' - runtimeOnly 'io.dropwizard.metrics:metrics-core:4.2.21' + runtimeOnly 'io.dropwizard.metrics:metrics-core:4.2.22' runtimeOnly 'org.slf4j:slf4j-api:1.7.36' runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}" runtimeOnly 'org.xerial.snappy:snappy-java:1.1.10.5' From efd4c6b38baabbe9e9c4e3922bf48234cb5b7ea3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:09:37 +0000 Subject: [PATCH 21/23] Bump org.checkerframework:checker-qual from 3.39.0 to 3.40.0 (#3654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) from 3.39.0 to 3.40.0.
Release notes

Sourced from org.checkerframework:checker-qual's releases.

Checker Framework 3.40.0

Version 3.40.0 (November 1, 2023)

User-visible changes:

Optional Checker: checker-util.jar defines OptionalUtil.castPresent() for suppressing false positive warnings from the Optional Checker.

Closed issues:

#4947, #6179, #6215, #6218, #6222, #6247, #6259, #6260.

Changelog

Sourced from org.checkerframework:checker-qual's changelog.

Version 3.40.0 (November 1, 2023)

User-visible changes:

Optional Checker: checker-util.jar defines OptionalUtil.castPresent() for suppressing false positive warnings from the Optional Checker.

Closed issues:

#4947, #6179, #6215, #6218, #6222, #6247, #6259, #6260.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.checkerframework:checker-qual&package-manager=gradle&previous-version=3.39.0&new-version=3.40.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2842c678b0..08514ab249 100644 --- a/build.gradle +++ b/build.gradle @@ -492,7 +492,7 @@ configurations { force "org.apache.httpcomponents:httpclient:4.5.14" force "org.apache.httpcomponents:httpcore:4.4.16" force "com.google.errorprone:error_prone_annotations:2.23.0" - force "org.checkerframework:checker-qual:3.39.0" + force "org.checkerframework:checker-qual:3.40.0" } } @@ -649,7 +649,7 @@ dependencies { runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.3.1' runtimeOnly 'org.apache.santuario:xmlsec:2.3.4' runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" - runtimeOnly 'org.checkerframework:checker-qual:3.39.0' + runtimeOnly 'org.checkerframework:checker-qual:3.40.0' runtimeOnly "org.bouncycastle:bcpkix-jdk15to18:${versions.bouncycastle}" runtimeOnly 'org.scala-lang.modules:scala-java8-compat_3:1.0.2' From 37f5521469a9d71e46b18837008586a2d0ef1578 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:48:10 -0500 Subject: [PATCH 22/23] Bump org.apache.camel:camel-xmlsecurity from 3.21.1 to 3.21.2 (#3655) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 08514ab249..71c8df6c93 100644 --- a/build.gradle +++ b/build.gradle @@ -610,7 +610,7 @@ dependencies { runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.1' runtimeOnly 'org.ow2.asm:asm:9.6' - testImplementation 'org.apache.camel:camel-xmlsecurity:3.21.1' + testImplementation 'org.apache.camel:camel-xmlsecurity:3.21.2' //OpenSAML implementation 'net.shibboleth.utilities:java-support:8.4.0' From f6f561e1d3892c34575d75c242f47420f7127407 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:54:50 +0000 Subject: [PATCH 23/23] Bump commons-io:commons-io from 2.14.0 to 2.15.0 (#3656) Bumps commons-io:commons-io from 2.14.0 to 2.15.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=commons-io:commons-io&package-manager=gradle&previous-version=2.14.0&new-version=2.15.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 71c8df6c93..c3ff4bfb4b 100644 --- a/build.gradle +++ b/build.gradle @@ -716,7 +716,7 @@ dependencies { integrationTestImplementation 'junit:junit:4.13.2' integrationTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}" integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" - integrationTestImplementation 'commons-io:commons-io:2.14.0' + integrationTestImplementation 'commons-io:commons-io:2.15.0' integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}" integrationTestImplementation 'org.hamcrest:hamcrest:2.2'