From 5582c5c027977ec4fc1342aef68bba91fd0c60bc Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:32:29 +0000 Subject: [PATCH 01/74] Fix build break Signed-off-by: Peter Nied --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15eaad0803..4e9e6d02cc 100644 --- a/build.gradle +++ b/build.gradle @@ -459,7 +459,7 @@ dependencies { runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.4.0' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.2.5' runtimeOnly 'org.apache.santuario:xmlsec:2.2.3' - runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" + runtimeOnly 'com.github.luben:zstd-jni:1.5.5-3' runtimeOnly 'org.checkerframework:checker-qual:3.5.0' runtimeOnly "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" From 8c8bb88ebfb4c23e34c47167666adb6543143ce4 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:33:43 +0000 Subject: [PATCH 02/74] Make authenticator name more concise Signed-off-by: Peter Nied --- .../security/OpenSearchSecurityPlugin.java | 8 ++++---- ...ator.java => OnBehalfOfAuthenticator.java} | 6 +++--- ....java => OnBehalfOfAuthenticatorTest.java} | 19 +++++-------------- 3 files changed, 12 insertions(+), 21 deletions(-) rename src/main/java/org/opensearch/security/http/{HTTPOnBehalfOfJwtAuthenticator.java => OnBehalfOfAuthenticator.java} (97%) rename src/test/java/org/opensearch/security/http/{HTTPOnBehalfOfJwtAuthenticatorTest.java => OnBehalfOfAuthenticatorTest.java} (93%) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 85b6798410..0a0726c0a7 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -141,7 +141,7 @@ import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; @@ -838,8 +838,8 @@ public Collection createComponents(Client localClient, ClusterService cl securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); - - HTTPOnBehalfOfJwtAuthenticator acInstance = new HTTPOnBehalfOfJwtAuthenticator(); + + final OnBehalfOfAuthenticator onBehalfOfAuthenticator = new OnBehalfOfAuthenticator(); final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); @@ -848,7 +848,7 @@ public Collection createComponents(Client localClient, ClusterService cl dcf.registerDCFListener(xffResolver); dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); - dcf.registerDCFListener(acInstance); + dcf.registerDCFListener(onBehalfOfAuthenticator); if (!(auditLog instanceof NullAuditLog)) { // Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog dcf.registerDCFListener(auditLog); diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java similarity index 97% rename from src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java rename to src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 1fabd0874c..c65c2c591a 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -44,7 +44,7 @@ import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.AuthCredentials; -public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { +public class OnBehalfOfAuthenticator implements HTTPAuthenticator { protected final Logger log = LogManager.getLogger(this.getClass()); @@ -58,13 +58,13 @@ public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { private String signingKey; private String encryptionKey; - public HTTPOnBehalfOfJwtAuthenticator() { + public OnBehalfOfAuthenticator() { super(); init(); } // FOR TESTING - public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){ + public OnBehalfOfAuthenticator(String signingKey, String encryptionKey){ this.signingKey = signingKey; this.encryptionKey = encryptionKey; init(); diff --git a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java similarity index 93% rename from src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java rename to src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 06f19a888a..a67ebf7a9c 100644 --- a/src/test/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -37,7 +37,7 @@ import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; -public class HTTPOnBehalfOfJwtAuthenticatorTest { +public class OnBehalfOfAuthenticatorTest { final static byte[] secretKeyBytes = new byte[1024]; final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); final static SecretKey secretKey; @@ -91,10 +91,7 @@ public void testBadKey() throws Exception { @Test public void testTokenMissing() throws Exception { - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey - ); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes),claimsEncryptionKey); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); @@ -107,10 +104,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey - ); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -127,10 +121,7 @@ public void testBearer() throws Exception { .signWith(secretKey, SignatureAlgorithm.HS512) .compact(); - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator( - BaseEncoding.base64().encode(secretKeyBytes), - claimsEncryptionKey - ); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -295,7 +286,7 @@ private AuthCredentials extractCredentialsFromJwtHeader( final Boolean bwcPluginCompatibilityMode ) { final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); - final HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, encryptionKey); + final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, encryptionKey); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); } From 0f7b4931b330ba2affe79f8e065ab06c4be8e672 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:47:25 +0000 Subject: [PATCH 03/74] Clean up how roles are (de)encrypted --- .../jwt/EncryptionDecryptionUtil.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 16d1248820..9fb407128c 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -19,38 +19,39 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.opensaml.xmlsec.encryption.P; + public class EncryptionDecryptionUtil { public static String encrypt(final String secret, final String data) { - - byte[] decodedKey = Base64.getDecoder().decode(secret); - - try { - Cipher cipher = Cipher.getInstance("AES"); - // rebuild key using SecretKeySpec - SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); - cipher.init(Cipher.ENCRYPT_MODE, originalKey); - byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); - return Base64.getEncoder().encodeToString(cipherText); - } catch (Exception e) { - throw new RuntimeException( - "Error occured while encrypting data", e); - } + final Cipher cipher = createCipherFromSecret(secret); + final byte[] cipherText = createCipherText(cipher, data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(cipherText); } public static String decrypt(final String secret, final String encryptedString) { + final Cipher cipher = createCipherFromSecret(secret); + final byte[] cipherText = createCipherText(cipher, Base64.getDecoder().decode(encryptedString)); + return new String(cipherText, StandardCharsets.UTF_8); + } - byte[] decodedKey = Base64.getDecoder().decode(secret); - + private static Cipher createCipherFromSecret(final String secret) { try { - Cipher cipher = Cipher.getInstance("AES"); - // rebuild key using SecretKeySpec - SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); + final byte[] decodedKey = Base64.getDecoder().decode(secret); + final Cipher cipher = Cipher.getInstance("AES"); + final SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); cipher.init(Cipher.DECRYPT_MODE, originalKey); - byte[] cipherText = cipher.doFinal(Base64.getDecoder().decode(encryptedString)); - return new String(cipherText, StandardCharsets.UTF_8); - } catch (Exception e) { - throw new RuntimeException("Error occured while decrypting data", e); + return cipher; + } catch (final Exception e) { + throw new RuntimeException("Error creating cipher from secret"); + } + } + + private static byte[] createCipherText(final Cipher cipher, final byte[] data) { + try { + return cipher.doFinal(data); + } catch (final Exception e) { + throw new RuntimeException("The cipher was unable to perform pass over data"); } } } From 4b5d48e60b8461ccad5fea0f43d196e215099d41 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:58:46 +0000 Subject: [PATCH 04/74] Single constructor Signed-off-by: Peter Nied --- .../security/authtoken/jwt/JwtVendor.java | 21 +++---------------- .../security/authtoken/jwt/JwtVendorTest.java | 13 ++++++------ 2 files changed, 10 insertions(+), 24 deletions(-) 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 5328453c98..99ef47b370 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -12,6 +12,7 @@ package org.opensearch.security.authtoken.jwt; import java.time.Instant; +import java.util.Optional; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,24 +54,8 @@ public class JwtVendor { private ConfigModel configModel; private ThreadContext threadContext; - public JwtVendor(Settings settings) { - JoseJwtProducer jwtProducer = new JoseJwtProducer(); - try { - this.signingKey = createJwkFromSettings(settings); - } catch (Exception e) { - throw new RuntimeException(e); - } - this.jwtProducer = jwtProducer; - if (settings.get("encryption_key") == null) { - throw new RuntimeException("encryption_key cannot be null"); - } else { - this.claimsEncryptionKey = settings.get("encryption_key"); - } - timeProvider = System::currentTimeMillis; - } - //For testing the expiration in the future - public JwtVendor(Settings settings, final LongSupplier timeProvider) { + public JwtVendor(Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { this.signingKey = createJwkFromSettings(settings); @@ -83,7 +68,7 @@ public JwtVendor(Settings settings, final LongSupplier timeProvider) { } else { this.claimsEncryptionKey = settings.get("encryption_key"); } - this.timeProvider = timeProvider; + this.timeProvider = timeProvider.orElseGet(() -> System::currentTimeMillis); } /* 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 55ce10df65..2edc7178b4 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -12,6 +12,7 @@ package org.opensearch.security.authtoken.jwt; import java.util.List; +import java.util.Optional; import java.util.function.LongSupplier; import org.apache.commons.lang3.RandomStringUtils; @@ -54,7 +55,7 @@ public void testCreateJwtWithRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); Long expectedExp = currentTime.getAsLong() + (expirySeconds * 1000); - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); @@ -79,8 +80,8 @@ public void testCreateJwtWithBadExpiry() throws Exception { Integer expirySeconds = -300; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); } @@ -93,8 +94,8 @@ public void testCreateJwtWithBadEncryptionKey() throws Exception { List roles = List.of("admin"); Integer expirySeconds = 300; - Settings settings = Settings.builder().put("signing_key", "abc123").build(); - JwtVendor jwtVendor = new JwtVendor(settings); + Settings settings = Settings.builder().put("signing_key", "abc123").build(); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); } @@ -110,7 +111,7 @@ public void testCreateJwtWithBadRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles); } From baa1312c2471e43d38915fa3724c30a221f71ed1 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 21:59:05 +0000 Subject: [PATCH 05/74] More JwtVendor cleanup - but not sure if this works? Signed-off-by: Peter Nied --- .../security/authtoken/jwt/JwtVendor.java | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) 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 99ef47b370..93efc93b06 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -29,12 +29,12 @@ import org.apache.cxf.rs.security.jose.jwt.JwtClaims; import org.apache.cxf.rs.security.jose.jwt.JwtToken; import org.apache.cxf.rs.security.jose.jwt.JwtUtils; +import org.apache.kafka.common.utils.SystemTime; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.common.settings.Settings; import org.opensearch.common.transport.TransportAddress; -import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -51,10 +51,8 @@ public class JwtVendor { private final LongSupplier timeProvider; //TODO: Relocate/Remove them at once we make the descisions about the `roles` - private ConfigModel configModel; - private ThreadContext threadContext; + private ConfigModel configModel; // This never gets assigned, how does this work at all? - //For testing the expiration in the future public JwtVendor(Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { @@ -108,22 +106,7 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { } } - //TODO:Getting roles from User - public Map prepareClaimsForUser(User user, ThreadPool threadPool) { - Map claims = new HashMap<>(); - this.threadContext = threadPool.getThreadContext(); - final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - Set mappedRoles = mapRoles(user, caller); - claims.put("sub", user.getName()); - claims.put("roles", String.join(",", mappedRoles)); - return claims; - } - - public Set mapRoles(final User user, final TransportAddress caller) { - return this.configModel.mapSecurityRoles(user, caller); - } - - public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { + String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { long timeMillis = timeProvider.getAsLong(); Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); From 2cb7cae9622498c0f4eb62c0f4fe8e8ffcb0e5fe Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 24 May 2023 12:02:30 -0700 Subject: [PATCH 06/74] Add OBO Authenticator into the Authc Backend list Signed-off-by: Ryan Liang --- .../security/auth/BackendRegistry.java | 2 -- .../security/http/OnBehalfOfAuthenticator.java | 17 +++++++++++++++-- .../securityconf/DynamicConfigModelV7.java | 8 ++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 9e8154230a..51e93978bd 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -224,8 +224,6 @@ public boolean authenticate(final RestRequest request, final RestChannel channel HTTPAuthenticator firstChallengingHttpAuthenticator = null; - //TODO: ADD OUR AUTHC BACKEND IN/BEFORE THIS LIST - //loop over all http/rest auth domains for (final AuthDomain authDomain: restAuthDomains) { if (isDebugEnabled) { diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index c65c2c591a..0010077871 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -36,6 +36,7 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; +import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; @@ -57,12 +58,19 @@ public class OnBehalfOfAuthenticator implements HTTPAuthenticator { private String signingKey; private String encryptionKey; + private volatile boolean initialized; public OnBehalfOfAuthenticator() { super(); init(); } + public HTTPOnBehalfOfJwtAuthenticator(Settings settings){ + this.signingKey = settings.get("signing_key"); + this.encryptionKey = settings.get("encryption_key"); + init(); + } + // FOR TESTING public OnBehalfOfAuthenticator(String signingKey, String encryptionKey){ this.signingKey = signingKey; @@ -70,6 +78,10 @@ public OnBehalfOfAuthenticator(String signingKey, String encryptionKey){ init(); } + public boolean isInitialized(){ + return initialized; + } + private void init() { try { @@ -265,8 +277,9 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { //TODO: #2615 FOR CONFIGURATION //For Testing - signingKey = "abcd1234"; - encryptionKey = RandomStringUtils.randomAlphanumeric(16); + signingKey = dcm.getDynamicOnBehalfOfSettings().get("signing_key"); + encryptionKey = dcm.getDynamicOnBehalfOfSettings().get("encryption_key"); + initialized = signingKey != null && encryptionKey != null; } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 0b66d9a306..56f65e24b7 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -54,6 +54,8 @@ import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; +import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; +import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator; import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.ConfigV7.Authc; import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthcDomain; @@ -312,6 +314,12 @@ private void buildAAA() { } } + Settings oboSettings = getDynamicOnBehalfOfSettings(); + if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { + final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new HTTPOnBehalfOfJwtAuthenticator(getDynamicOnBehalfOfSettings()), false, 0); + restAuthDomains0.add(_ad); + } + List originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); From 196fbea9e59b3d3c02c4fdf92922c71e8879bf8c Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 25 May 2023 16:15:54 -0700 Subject: [PATCH 07/74] Fix the logic of feching er/dr for rolesObject Signed-off-by: Ryan Liang --- .../http/OnBehalfOfAuthenticator.java | 20 +++---------- .../security/authtoken/jwt/JwtVendorTest.java | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 0010077871..9e7d66cd6b 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -28,7 +28,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.WeakKeyException; -import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -176,21 +176,9 @@ private AuthCredentials extractCredentials0(final RestRequest request) { final String audience = claims.getAudience(); - //TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr - Object rolesObject = null; String[] roles; - try { - rolesObject = claims.get("er"); - } catch (Throwable e) { - log.debug("No encrypted role founded in the claim, continue searching for decrypted roles."); - } - - try { - rolesObject = claims.get("dr"); - } catch (Throwable e) { - log.debug("No decrypted role founded in the claim."); - } + Object rolesObject = ObjectUtils.firstNonNull(claims.get("er"), claims.get("dr")); if (rolesObject == null) { log.warn( @@ -202,7 +190,6 @@ private AuthCredentials extractCredentials0(final RestRequest request) { // Extracting roles based on the compatbility mode String decryptedRoles = rolesClaim; if (rolesObject == claims.get("er")) { - //TODO: WHERE TO GET THE ENCRYTION KEY decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); } roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); @@ -226,9 +213,11 @@ private AuthCredentials extractCredentials0(final RestRequest request) { return ac; } catch (WeakKeyException e) { + System.out.println("Error MSG1!" + e.getMessage()); log.error("Cannot authenticate user with JWT because of ", e); return null; } catch (Exception e) { + System.out.println("Error MSG2!" + e.getMessage()); if(log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); } @@ -275,7 +264,6 @@ private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - //TODO: #2615 FOR CONFIGURATION //For Testing signingKey = dcm.getDynamicOnBehalfOfSettings().get("signing_key"); encryptionKey = dcm.getDynamicOnBehalfOfSettings().get("encryption_key"); 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 2edc7178b4..4247307614 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -11,6 +11,8 @@ package org.opensearch.security.authtoken.jwt; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.function.LongSupplier; @@ -24,6 +26,8 @@ import org.opensearch.common.settings.Settings; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class JwtVendorTest { @Test @@ -115,4 +119,30 @@ public void testCreateJwtWithBadRoles() throws Exception { jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles); } + + //For Manual Testing + @Test + public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { + String issuer = "cluster_0"; + String subject = "craig"; + String audience = "audience_0"; + List roles = List.of("admin", "HR"); + Integer expirySeconds = 10000; + LongSupplier currentTime = () -> (System.currentTimeMillis() / 1000); + String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); + String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + System.out.println("The encryptionkey is:" + encryptionKey); + Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); + + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); + JwtToken jwt = jwtConsumer.getJwtToken(); + + System.out.println("JWT: " + encodedJwt); + + assertTrue(true); + } } From 6e96de6ca6626723faa0cdd7eed82f6c7e154561 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Fri, 26 May 2023 11:54:19 -0700 Subject: [PATCH 08/74] Fix of the OBO unit tests of Bearer header Signed-off-by: Ryan Liang --- .../http/OnBehalfOfAuthenticator.java | 11 +--------- .../http/OnBehalfOfAuthenticatorTest.java | 22 +++++++------------ 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 9e7d66cd6b..fbf9a07954 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -161,7 +161,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } final int index; - if((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer + if(jwtToken != null && (index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer jwtToken = jwtToken.substring(index+BEARER_PREFIX.length()); } else { if(log.isDebugEnabled()) { @@ -261,13 +261,4 @@ private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) return kf.generatePublic(spec); } - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - - //For Testing - signingKey = dcm.getDynamicOnBehalfOfSettings().get("signing_key"); - encryptionKey = dcm.getDynamicOnBehalfOfSettings().get("encryption_key"); - initialized = signingKey != null && encryptionKey != null; - } - } diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index a67ebf7a9c..b3b81485c2 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -31,9 +31,6 @@ import org.junit.Assert; import org.junit.Test; -import com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator; - -import org.opensearch.common.settings.Settings; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; @@ -91,7 +88,7 @@ public void testBadKey() throws Exception { @Test public void testTokenMissing() throws Exception { - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes),claimsEncryptionKey); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey,claimsEncryptionKey); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); @@ -104,7 +101,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -121,7 +118,7 @@ public void testBearer() throws Exception { .signWith(secretKey, SignatureAlgorithm.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(BaseEncoding.base64().encode(secretKeyBytes), claimsEncryptionKey); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -136,11 +133,9 @@ public void testBearer() throws Exception { @Test public void testBearerWrongPosition() throws Exception { - Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(secretKeyBytes)).build(); - - String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); @@ -151,11 +146,10 @@ public void testBearerWrongPosition() throws Exception { @Test public void testBasicAuthHeader() throws Exception { - Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(secretKeyBytes)).build(); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); - String basicAuth = BaseEncoding.base64().encode("user:password".getBytes(StandardCharsets.UTF_8)); - Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth); + Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, Collections.emptyMap()), null); Assert.assertNull(credentials); From a85a3e997b1843f24ea3f702b7026dec7fdd96ca Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Fri, 2 Jun 2023 11:35:52 -0700 Subject: [PATCH 09/74] Set up oboconfig Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 239 +++ .../test/framework/OnBehalfOfConfig.java | 44 + .../test/framework/TestSecurityConfig.java | 1275 ++++++++--------- .../test/framework/cluster/LocalCluster.java | 865 ++++++----- 4 files changed, 1335 insertions(+), 1088 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java create mode 100644 src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java new file mode 100644 index 0000000000..9e78274c34 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -0,0 +1,239 @@ +package org.opensearch.security.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.test.framework.JwtConfigBuilder; +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 org.opensearch.test.framework.log.LogsRule; + +import java.io.IOException; +import java.security.KeyPair; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.rest.RestStatus.FORBIDDEN; +import static org.opensearch.security.Song.*; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.queryStringQueryRequest; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.*; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue; + + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class OnBehalfOfJwtAuthenticationTest { + + public static final String CLAIM_USERNAME = "test-user"; + public static final String CLAIM_ROLES = "backend-user-roles"; + + public static final String USER_SUPERHERO = "superhero"; + public static final String USERNAME_ROOT = "root"; + public static final String ROLE_ADMIN = "role_admin"; + public static final String ROLE_DEVELOPER = "role_developer"; + public static final String ROLE_QA = "role_qa"; + public static final String ROLE_CTO = "role_cto"; + public static final String ROLE_CEO = "role_ceo"; + public static final String ROLE_VP = "role_vp"; + public static final String POINTER_BACKEND_ROLES = "/backend_roles"; + public static final String POINTER_USERNAME = "/user_name"; + + public static final String QA_DEPARTMENT = "qa-department"; + + public static final String CLAIM_DEPARTMENT = "department"; + + public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); + + public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); + + private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final String JWT_AUTH_HEADER = "jwt-auth"; + + private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( + KEY_PAIR.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + JWT_AUTH_HEADER); + + public static final String SONG_ID_1 = "song-id-01"; + + public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") + .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() + .build(); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + } + try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ + client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_positive() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(username)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USERNAME_ROOT)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureLackingUserName() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("No subject found in JWT token"); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureExpiredToken() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { + Header header = new BasicHeader(AUTHORIZATION, "not.a.token"); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { + KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); + Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); + } + } + + @Test + public void shouldReadRolesFromToken_positiveFirstRoleSet() { + Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(3)); + assertThat(roles, containsInAnyOrder(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA)); + } + } + + @Test + public void shouldReadRolesFromToken_positiveSecondRoleSet() { + Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); + try(TestRestClient client = cluster.getRestClient(header)){ + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(3)); + assertThat(roles, containsInAnyOrder(ROLE_CTO, ROLE_CEO, ROLE_VP)); + } + } + + @Test + public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { + String[] roles = { ROLE_VP }; + Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); + Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ + SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse response = client.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + assertThat(response, searchHitsContainDocumentWithId(0, QA_SONG_INDEX_NAME, SONG_ID_1)); + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + } + + @Test + public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { + String[] roles = { ROLE_VP }; + Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); + Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ + SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java new file mode 100644 index 0000000000..d68c0d73d2 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java @@ -0,0 +1,44 @@ +/* + * 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.io.IOException; + +import org.apache.commons.lang3.StringUtils; + +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class OnBehalfOfConfig implements ToXContentObject { + private String signing_key; + private String encryption_key; + + public OnBehalfOfConfig signing_key(String signing_key) { + this.signing_key = signing_key; + return this; + } + + public OnBehalfOfConfig encryption_key(String encryption_key) { + this.encryption_key = encryption_key; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("signing_key", signing_key); + if (StringUtils.isNoneBlank(encryption_key)){ + xContentBuilder.field("encryption_key", encryption_key); + } + xContentBuilder.endObject(); + return xContentBuilder; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index a702102e6b..b47fbed32d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -76,642 +76,641 @@ */ public class TestSecurityConfig { - private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); - - private Config config = new Config(); - private Map internalUsers = new LinkedHashMap<>(); - private Map roles = new LinkedHashMap<>(); - private AuditConfiguration auditConfiguration; - private Map rolesMapping = new LinkedHashMap<>(); - - private String indexName = ".opendistro_security"; - - public TestSecurityConfig() { - - } - - public TestSecurityConfig configIndexName(String configIndexName) { - this.indexName = configIndexName; - return this; - } - - public TestSecurityConfig authFailureListeners(AuthFailureListeners listener) { - config.authFailureListeners(listener); - return this; - } - - public TestSecurityConfig anonymousAuth(boolean anonymousAuthEnabled) { - config.anonymousAuth(anonymousAuthEnabled); - return this; - } - - public TestSecurityConfig doNotFailOnForbidden(boolean doNotFailOnForbidden) { - config.doNotFailOnForbidden(doNotFailOnForbidden); - return this; - } - - public TestSecurityConfig xff(XffConfig xffConfig) { - config.xffConfig(xffConfig); - return this; - } - - public TestSecurityConfig authc(AuthcDomain authcDomain) { - config.authc(authcDomain); - return this; - } - - public TestSecurityConfig authz(AuthzDomain authzDomain) { - config.authz(authzDomain); - return this; - } - - public TestSecurityConfig user(User user) { - this.internalUsers.put(user.name, user); - - for (Role role : user.roles) { - this.roles.put(role.name, role); - } - - return this; - } - - public List getUsers() { - return new ArrayList<>(internalUsers.values()); - } - - public TestSecurityConfig roles(Role... roles) { - for (Role role : roles) { - if (this.roles.containsKey(role.name)) { - throw new IllegalStateException("Role with name " + role.name + " is already defined"); - } - this.roles.put(role.name, role); - } - - return this; - } - - public TestSecurityConfig audit(AuditConfiguration auditConfiguration) { - this.auditConfiguration = auditConfiguration; - return this; - } - - public TestSecurityConfig rolesMapping(RolesMapping... mappings) { - for (RolesMapping mapping : mappings) { - String roleName = mapping.getRoleName(); - if (rolesMapping.containsKey(roleName)) { - throw new IllegalArgumentException("Role mapping " + roleName + " already exists"); - } - this.rolesMapping.put(roleName, mapping); - } - return this; - } - - public static class Config implements ToXContentObject { - private boolean anonymousAuth; - - private Boolean doNotFailOnForbidden; - private XffConfig xffConfig; - private Map authcDomainMap = new LinkedHashMap<>(); - - private AuthFailureListeners authFailureListeners; - private Map authzDomainMap = new LinkedHashMap<>(); - - public Config anonymousAuth(boolean anonymousAuth) { - this.anonymousAuth = anonymousAuth; - return this; - } - - public Config doNotFailOnForbidden(Boolean doNotFailOnForbidden) { - this.doNotFailOnForbidden = doNotFailOnForbidden; - return this; - } - - public Config xffConfig(XffConfig xffConfig) { - this.xffConfig = xffConfig; - return this; - } - - public Config authc(AuthcDomain authcDomain) { - authcDomainMap.put(authcDomain.id, authcDomain); - return this; - } - - public Config authFailureListeners(AuthFailureListeners authFailureListeners) { - this.authFailureListeners = authFailureListeners; - return this; - } - - public Config authz(AuthzDomain authzDomain) { - authzDomainMap.put(authzDomain.getId(), authzDomain); - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - xContentBuilder.startObject("dynamic"); - - if (anonymousAuth || (xffConfig != null)) { - xContentBuilder.startObject("http"); - xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); - if (xffConfig != null) { - xContentBuilder.field("xff", xffConfig); - } - xContentBuilder.endObject(); - } - if (doNotFailOnForbidden != null) { - xContentBuilder.field("do_not_fail_on_forbidden", doNotFailOnForbidden); - } - - xContentBuilder.field("authc", authcDomainMap); - if (authzDomainMap.isEmpty() == false) { - xContentBuilder.field("authz", authzDomainMap); - } - - if (authFailureListeners != null) { - xContentBuilder.field("auth_failure_listeners", authFailureListeners); - } - - xContentBuilder.endObject(); - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class User implements UserCredentialsHolder, ToXContentObject { - - public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles( - new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*") - ); - - String name; - private String password; - List roles = new ArrayList<>(); - private Map attributes = new HashMap<>(); - - public User(String name) { - this.name = name; - this.password = "secret"; - } - - public User password(String password) { - this.password = password; - return this; - } - - public User roles(Role... roles) { - // We scope the role names by user to keep tests free of potential side effects - String roleNamePrefix = "user_" + this.getName() + "__"; - this.roles.addAll( - Arrays.asList(roles).stream().map((r) -> r.clone().name(roleNamePrefix + r.getName())).collect(Collectors.toSet()) - ); - return this; - } - - public User attr(String key, Object value) { - this.attributes.put(key, value); - return this; - } - - public String getName() { - return name; - } - - public String getPassword() { - return password; - } - - public Set getRoleNames() { - return roles.stream().map(Role::getName).collect(Collectors.toSet()); - } - - public Object getAttribute(String attributeName) { - return attributes.get(attributeName); - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("hash", hash(password.toCharArray())); - - Set roleNames = getRoleNames(); - - if (!roleNames.isEmpty()) { - xContentBuilder.field("opendistro_security_roles", roleNames); - } - - if (attributes != null && attributes.size() != 0) { - xContentBuilder.field("attributes", attributes); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class Role implements ToXContentObject { - public static Role ALL_ACCESS = new Role("all_access").clusterPermissions("*").indexPermissions("*").on("*"); - - private String name; - private List clusterPermissions = new ArrayList<>(); - - private List indexPermissions = new ArrayList<>(); - - public Role(String name) { - this.name = name; - } - - public Role clusterPermissions(String... clusterPermissions) { - this.clusterPermissions.addAll(Arrays.asList(clusterPermissions)); - return this; - } - - public IndexPermission indexPermissions(String... indexPermissions) { - return new IndexPermission(this, indexPermissions); - } - - public Role name(String name) { - this.name = name; - return this; - } - - public String getName() { - return name; - } - - public Role clone() { - Role role = new Role(this.name); - role.clusterPermissions.addAll(this.clusterPermissions); - role.indexPermissions.addAll(this.indexPermissions); - return role; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - if (!clusterPermissions.isEmpty()) { - xContentBuilder.field("cluster_permissions", clusterPermissions); - } - - if (!indexPermissions.isEmpty()) { - xContentBuilder.field("index_permissions", indexPermissions); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class IndexPermission implements ToXContentObject { - private List allowedActions; - private List indexPatterns; - private Role role; - private String dlsQuery; - private List fls; - private List maskedFields; - - IndexPermission(Role role, String... allowedActions) { - this.allowedActions = Arrays.asList(allowedActions); - this.role = role; - } - - public IndexPermission dls(String dlsQuery) { - this.dlsQuery = dlsQuery; - return this; - } - - public IndexPermission fls(String... fls) { - this.fls = Arrays.asList(fls); - return this; - } - - public IndexPermission maskedFields(String... maskedFields) { - this.maskedFields = Arrays.asList(maskedFields); - return this; - } - - public Role on(String... indexPatterns) { - this.indexPatterns = Arrays.asList(indexPatterns); - this.role.indexPermissions.add(this); - return this.role; - } - - public Role on(TestIndex... testindices) { - this.indexPatterns = Arrays.asList(testindices).stream().map(TestIndex::getName).collect(Collectors.toList()); - this.role.indexPermissions.add(this); - return this.role; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("index_patterns", indexPatterns); - xContentBuilder.field("allowed_actions", allowedActions); - - if (dlsQuery != null) { - xContentBuilder.field("dls", dlsQuery); - } - - if (fls != null) { - xContentBuilder.field("fls", fls); - } - - if (maskedFields != null) { - xContentBuilder.field("masked_fields", maskedFields); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class AuthcDomain implements ToXContentObject { - - private static String PUBLIC_KEY = - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqZbjLUAWc+DZTkinQAdvy1GFjPHPnxheU89hSiWoDD3NOW76H3u3T7cCDdOah2msdxSlBmCBH6wik8qLYkcV8owWukQg3PQmbEhrdPaKo0QCgomWs4nLgtmEYqcZ+QQldd82MdTlQ1QmoQmI9Uxqs1SuaKZASp3Gy19y8su5CV+FZ6BruUw9HELK055sAwl3X7j5ouabXGbcib2goBF3P52LkvbJLuWr5HDZEOeSkwIeqSeMojASM96K5SdotD+HwEyjaTjzRPL2Aa1BEQFWOQ6CFJLyLH7ZStDuPM1mJU1VxIVfMbZrhsUBjAnIhRynmWxML7YlNqkP9j6jyOIYQIDAQAB"; - - public static final int BASIC_AUTH_DOMAIN_ORDER = 0; - public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", BASIC_AUTH_DOMAIN_ORDER) - .httpAuthenticatorWithChallenge("basic") - .backend("internal"); - - public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE = new TestSecurityConfig.AuthcDomain( - "basic", - BASIC_AUTH_DOMAIN_ORDER - ).httpAuthenticator("basic").backend("internal"); - - public final static AuthcDomain DISABLED_AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain( - "basic", - BASIC_AUTH_DOMAIN_ORDER, - false - ).httpAuthenticator("basic").backend("internal"); - - public final static AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain("jwt", 1).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(PUBLIC_KEY) - ).backend("noop"); - - private final String id; - private boolean enabled = true; - private int order; - private List skipUsers = new ArrayList<>(); - private HttpAuthenticator httpAuthenticator; - private AuthenticationBackend authenticationBackend; - - public AuthcDomain(String id, int order, boolean enabled) { - this.id = id; - this.order = order; - this.enabled = enabled; - } - - public AuthcDomain(String id, int order) { - this(id, order, true); - } - - public AuthcDomain httpAuthenticator(String type) { - this.httpAuthenticator = new HttpAuthenticator(type); - return this; - } - - public AuthcDomain jwtHttpAuthenticator(JwtConfigBuilder builder) { - this.httpAuthenticator = new HttpAuthenticator("jwt").challenge(false).config(builder.build()); - return this; - } - - public AuthcDomain httpAuthenticatorWithChallenge(String type) { - this.httpAuthenticator = new HttpAuthenticator(type).challenge(true); - return this; - } - - public AuthcDomain httpAuthenticator(HttpAuthenticator httpAuthenticator) { - this.httpAuthenticator = httpAuthenticator; - return this; - } - - public AuthcDomain backend(String type) { - this.authenticationBackend = new AuthenticationBackend(type); - return this; - } - - public AuthcDomain backend(AuthenticationBackend authenticationBackend) { - this.authenticationBackend = authenticationBackend; - return this; - } - - public AuthcDomain skipUsers(String... users) { - this.skipUsers.addAll(Arrays.asList(users)); - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("http_enabled", enabled); - xContentBuilder.field("order", order); - - if (httpAuthenticator != null) { - xContentBuilder.field("http_authenticator", httpAuthenticator); - } - - if (authenticationBackend != null) { - xContentBuilder.field("authentication_backend", authenticationBackend); - } - - if (skipUsers != null && skipUsers.size() > 0) { - xContentBuilder.field("skip_users", skipUsers); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - - public static class HttpAuthenticator implements ToXContentObject { - private final String type; - private boolean challenge; - private Map config = new HashMap(); - - public HttpAuthenticator(String type) { - this.type = type; - } - - public HttpAuthenticator challenge(boolean challenge) { - this.challenge = challenge; - return this; - } - - public HttpAuthenticator config(Map config) { - this.config.putAll(config); - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("type", type); - xContentBuilder.field("challenge", challenge); - xContentBuilder.field("config", config); - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class AuthenticationBackend implements ToXContentObject { - private final String type; - private Supplier> config = () -> new HashMap(); - - public AuthenticationBackend(String type) { - this.type = type; - } - - public AuthenticationBackend config(Map config) { - Map configCopy = new HashMap<>(config); - this.config = () -> configCopy; - return this; - } - - public AuthenticationBackend config(Supplier> configSupplier) { - this.config = configSupplier; - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("type", type); - xContentBuilder.field("config", config.get()); - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - } - - public void initIndex(Client client) { - Map settings = new HashMap<>(); - if (indexName.startsWith(".")) { - settings.put("index.hidden", true); - } - client.admin().indices().create(new CreateIndexRequest(indexName).settings(settings)).actionGet(); - - writeSingleEntryConfigToIndex(client, CType.CONFIG, config); - if (auditConfiguration != null) { - writeSingleEntryConfigToIndex(client, CType.AUDIT, "config", auditConfiguration); - } - writeConfigToIndex(client, CType.ROLES, roles); - writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers); - writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); - writeEmptyConfigToIndex(client, CType.ACTIONGROUPS); - writeEmptyConfigToIndex(client, CType.TENANTS); - } - - public void updateInternalUsersConfiguration(Client client, List users) { - Map userMap = new HashMap<>(); - for (User user : users) { - userMap.put(user.getName(), user); - } - updateConfigInIndex(client, CType.INTERNALUSERS, userMap); - } - - static String hash(final char[] clearTextPassword) { - final byte[] salt = new byte[16]; - new SecureRandom().nextBytes(salt); - final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); - Arrays.fill(salt, (byte) 0); - Arrays.fill(clearTextPassword, '\0'); - return hash; - } - - private void writeEmptyConfigToIndex(Client client, CType configType) { - writeConfigToIndex(client, configType, Collections.emptyMap()); - } - - private void writeConfigToIndex(Client client, CType configType, Map config) { - try { - String json = configToJson(configType, config); - - log.info("Writing security configuration into index " + configType + ":\n" + json); - - BytesReference bytesReference = toByteReference(json); - client.index( - new IndexRequest(indexName).id(configType.toLCString()) - .setRefreshPolicy(IMMEDIATE) - .source(configType.toLCString(), bytesReference) - ).actionGet(); - } catch (Exception e) { - throw new RuntimeException("Error while initializing config for " + indexName, e); - } - } - - private static BytesReference toByteReference(String string) throws UnsupportedEncodingException { - return BytesReference.fromByteBuffer(ByteBuffer.wrap(string.getBytes("utf-8"))); - } - - private void updateConfigInIndex(Client client, CType configType, Map config) { - try { - String json = configToJson(configType, config); - BytesReference bytesReference = toByteReference(json); - log.info("Update configuration of type '{}' in index '{}', new value '{}'.", configType, indexName, json); - UpdateRequest upsert = new UpdateRequest(indexName, configType.toLCString()).doc(configType.toLCString(), bytesReference) - .setRefreshPolicy(IMMEDIATE); - client.update(upsert).actionGet(); - } catch (Exception e) { - throw new RuntimeException("Error while updating config for " + indexName, e); - } - } - - private static String configToJson(CType configType, Map config) throws IOException { - XContentBuilder builder = XContentFactory.jsonBuilder(); - - builder.startObject(); - builder.startObject("_meta"); - builder.field("type", configType.toLCString()); - builder.field("config_version", 2); - builder.endObject(); - - for (Map.Entry entry : config.entrySet()) { - builder.field(entry.getKey(), entry.getValue()); - } - - builder.endObject(); - - return Strings.toString(builder); - } - - private void writeSingleEntryConfigToIndex(Client client, CType configType, ToXContentObject config) { - writeSingleEntryConfigToIndex(client, configType, configType.toLCString(), config); - } - - private void writeSingleEntryConfigToIndex(Client client, CType configType, String configurationRoot, ToXContentObject config) { - try { - XContentBuilder builder = XContentFactory.jsonBuilder(); - - builder.startObject(); - builder.startObject("_meta"); - builder.field("type", configType.toLCString()); - builder.field("config_version", 2); - builder.endObject(); - - builder.field(configurationRoot, config); - - builder.endObject(); - - String json = Strings.toString(builder); - - log.info("Writing security plugin configuration into index " + configType + ":\n" + json); - - client.index( - new IndexRequest(indexName).id(configType.toLCString()) - .setRefreshPolicy(IMMEDIATE) - .source(configType.toLCString(), toByteReference(json)) - ).actionGet(); - } catch (Exception e) { - throw new RuntimeException("Error while initializing config for " + indexName, e); - } - } + private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); + + private Config config = new Config(); + private Map internalUsers = new LinkedHashMap<>(); + private Map roles = new LinkedHashMap<>(); + private AuditConfiguration auditConfiguration; + private Map rolesMapping = new LinkedHashMap<>(); + + private String indexName = ".opendistro_security"; + + public TestSecurityConfig() { + + } + + public TestSecurityConfig configIndexName(String configIndexName) { + this.indexName = configIndexName; + return this; + } + + public TestSecurityConfig authFailureListeners(AuthFailureListeners listener) { + config.authFailureListeners(listener); + return this; + } + + public TestSecurityConfig anonymousAuth(boolean anonymousAuthEnabled) { + config.anonymousAuth(anonymousAuthEnabled); + return this; + } + + public TestSecurityConfig doNotFailOnForbidden(boolean doNotFailOnForbidden) { + config.doNotFailOnForbidden(doNotFailOnForbidden); + return this; + } + + public TestSecurityConfig xff(XffConfig xffConfig) { + config.xffConfig(xffConfig); + return this; + } + + public TestSecurityConfig onBehalfOf(OnBehalfOfConfig onBehalfOfConfig){ + config.onBehalfOfConfig(onBehalfOfConfig); + return this; + } + + public TestSecurityConfig authc(AuthcDomain authcDomain) { + config.authc(authcDomain); + return this; + } + + public TestSecurityConfig authz(AuthzDomain authzDomain) { + config.authz(authzDomain); + return this; + } + public TestSecurityConfig user(User user) { + this.internalUsers.put(user.name, user); + + for (Role role : user.roles) { + this.roles.put(role.name, role); + } + + return this; + } + + public List getUsers() { + return new ArrayList<>(internalUsers.values()); + } + + public TestSecurityConfig roles(Role... roles) { + for (Role role : roles) { + if(this.roles.containsKey(role.name)) { + throw new IllegalStateException("Role with name " + role.name + " is already defined"); + } + this.roles.put(role.name, role); + } + + return this; + } + + public TestSecurityConfig audit(AuditConfiguration auditConfiguration) { + this.auditConfiguration = auditConfiguration; + return this; + } + + public TestSecurityConfig rolesMapping(RolesMapping...mappings) { + for (RolesMapping mapping : mappings) { + String roleName = mapping.getRoleName(); + if(rolesMapping.containsKey(roleName)) { + throw new IllegalArgumentException("Role mapping " + roleName + " already exists"); + } + this.rolesMapping.put(roleName, mapping); + } + return this; + } + + public static class Config implements ToXContentObject { + private boolean anonymousAuth; + + private Boolean doNotFailOnForbidden; + private XffConfig xffConfig; + private OnBehalfOfConfig onBehalfOfConfig; + private Map authcDomainMap = new LinkedHashMap<>(); + + private AuthFailureListeners authFailureListeners; + private Map authzDomainMap = new LinkedHashMap<>(); + + public Config anonymousAuth(boolean anonymousAuth) { + this.anonymousAuth = anonymousAuth; + return this; + } + + public Config doNotFailOnForbidden(Boolean doNotFailOnForbidden) { + this.doNotFailOnForbidden = doNotFailOnForbidden; + return this; + } + + public Config xffConfig(XffConfig xffConfig) { + this.xffConfig = xffConfig; + return this; + } + + public Config onBehalfOfConfig(OnBehalfOfConfig onBehalfOfConfig) { + this.onBehalfOfConfig = onBehalfOfConfig; + return this; + } + + public Config authc(AuthcDomain authcDomain) { + authcDomainMap.put(authcDomain.id, authcDomain); + return this; + } + + public Config authFailureListeners(AuthFailureListeners authFailureListeners) { + this.authFailureListeners = authFailureListeners; + return this; + } + + public Config authz(AuthzDomain authzDomain) { + authzDomainMap.put(authzDomain.getId(), authzDomain); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.startObject("dynamic"); + + if (anonymousAuth || (xffConfig != null)) { + xContentBuilder.startObject("http"); + xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); + if(xffConfig != null) { + xContentBuilder.field("xff", xffConfig); + } + xContentBuilder.endObject(); + } + if(doNotFailOnForbidden != null) { + xContentBuilder.field("do_not_fail_on_forbidden", doNotFailOnForbidden); + } + + xContentBuilder.field("authc", authcDomainMap); + if(authzDomainMap.isEmpty() == false) { + xContentBuilder.field("authz", authzDomainMap); + } + + if(authFailureListeners != null) { + xContentBuilder.field("auth_failure_listeners", authFailureListeners); + } + + xContentBuilder.endObject(); + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class User implements UserCredentialsHolder, ToXContentObject { + + public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin") + .roles(new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*")); + + String name; + private String password; + List roles = new ArrayList<>(); + private Map attributes = new HashMap<>(); + + public User(String name) { + this.name = name; + this.password = "secret"; + } + + public User password(String password) { + this.password = password; + return this; + } + + public User roles(Role... roles) { + // We scope the role names by user to keep tests free of potential side effects + String roleNamePrefix = "user_" + this.getName() + "__"; + this.roles.addAll(Arrays.asList(roles).stream().map((r) -> r.clone().name(roleNamePrefix + r.getName())).collect(Collectors.toSet())); + return this; + } + + public User attr(String key, Object value) { + this.attributes.put(key, value); + return this; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + + public Set getRoleNames() { + return roles.stream().map(Role::getName).collect(Collectors.toSet()); + } + + public Object getAttribute(String attributeName) { + return attributes.get(attributeName); + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("hash", hash(password.toCharArray())); + + Set roleNames = getRoleNames(); + + if (!roleNames.isEmpty()) { + xContentBuilder.field("opendistro_security_roles", roleNames); + } + + if (attributes != null && attributes.size() != 0) { + xContentBuilder.field("attributes", attributes); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class Role implements ToXContentObject { + public static Role ALL_ACCESS = new Role("all_access").clusterPermissions("*").indexPermissions("*").on("*"); + + private String name; + private List clusterPermissions = new ArrayList<>(); + + private List indexPermissions = new ArrayList<>(); + + public Role(String name) { + this.name = name; + } + + public Role clusterPermissions(String... clusterPermissions) { + this.clusterPermissions.addAll(Arrays.asList(clusterPermissions)); + return this; + } + + public IndexPermission indexPermissions(String... indexPermissions) { + return new IndexPermission(this, indexPermissions); + } + + public Role name(String name) { + this.name = name; + return this; + } + + public String getName() { + return name; + } + + public Role clone() { + Role role = new Role(this.name); + role.clusterPermissions.addAll(this.clusterPermissions); + role.indexPermissions.addAll(this.indexPermissions); + return role; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + if (!clusterPermissions.isEmpty()) { + xContentBuilder.field("cluster_permissions", clusterPermissions); + } + + if (!indexPermissions.isEmpty()) { + xContentBuilder.field("index_permissions", indexPermissions); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class IndexPermission implements ToXContentObject { + private List allowedActions; + private List indexPatterns; + private Role role; + private String dlsQuery; + private List fls; + private List maskedFields; + + IndexPermission(Role role, String... allowedActions) { + this.allowedActions = Arrays.asList(allowedActions); + this.role = role; + } + + public IndexPermission dls(String dlsQuery) { + this.dlsQuery = dlsQuery; + return this; + } + + public IndexPermission fls(String... fls) { + this.fls = Arrays.asList(fls); + return this; + } + + public IndexPermission maskedFields(String... maskedFields) { + this.maskedFields = Arrays.asList(maskedFields); + return this; + } + + public Role on(String... indexPatterns) { + this.indexPatterns = Arrays.asList(indexPatterns); + this.role.indexPermissions.add(this); + return this.role; + } + + public Role on(TestIndex... testindices) { + this.indexPatterns = Arrays.asList(testindices).stream().map(TestIndex::getName).collect(Collectors.toList()); + this.role.indexPermissions.add(this); + return this.role; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("index_patterns", indexPatterns); + xContentBuilder.field("allowed_actions", allowedActions); + + if (dlsQuery != null) { + xContentBuilder.field("dls", dlsQuery); + } + + if (fls != null) { + xContentBuilder.field("fls", fls); + } + + if (maskedFields != null) { + xContentBuilder.field("masked_fields", maskedFields); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class AuthcDomain implements ToXContentObject { + + private static String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqZbjLUAWc+DZTkinQAdvy1GFjPHPnxheU89hSiWoDD3NOW76H3u3T7cCDdOah2msdxSlBmCBH6wik8qLYkcV8owWukQg3PQmbEhrdPaKo0QCgomWs4nLgtmEYqcZ+QQldd82MdTlQ1QmoQmI9Uxqs1SuaKZASp3Gy19y8su5CV+FZ6BruUw9HELK055sAwl3X7j5ouabXGbcib2goBF3P52LkvbJLuWr5HDZEOeSkwIeqSeMojASM96K5SdotD+HwEyjaTjzRPL2Aa1BEQFWOQ6CFJLyLH7ZStDuPM1mJU1VxIVfMbZrhsUBjAnIhRynmWxML7YlNqkP9j6jyOIYQIDAQAB"; + + public static final int BASIC_AUTH_DOMAIN_ORDER = 0; + public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", BASIC_AUTH_DOMAIN_ORDER) + .httpAuthenticatorWithChallenge("basic").backend("internal"); + + public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE = new TestSecurityConfig.AuthcDomain("basic", + BASIC_AUTH_DOMAIN_ORDER) + .httpAuthenticator("basic").backend("internal"); + + public final static AuthcDomain DISABLED_AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig + .AuthcDomain("basic", BASIC_AUTH_DOMAIN_ORDER, false).httpAuthenticator("basic").backend("internal"); + + public final static AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig + .AuthcDomain("jwt", 1) + .jwtHttpAuthenticator(new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(PUBLIC_KEY)).backend("noop"); + + private final String id; + private boolean enabled = true; + private int order; + private List skipUsers = new ArrayList<>(); + private HttpAuthenticator httpAuthenticator; + private AuthenticationBackend authenticationBackend; + + public AuthcDomain(String id, int order, boolean enabled) { + this.id = id; + this.order = order; + this.enabled = enabled; + } + + public AuthcDomain(String id, int order) { + this(id, order, true); + } + + public AuthcDomain httpAuthenticator(String type) { + this.httpAuthenticator = new HttpAuthenticator(type); + return this; + } + + public AuthcDomain jwtHttpAuthenticator(JwtConfigBuilder builder) { + this.httpAuthenticator = new HttpAuthenticator("jwt") + .challenge(false).config(builder.build()); + return this; + } + + public AuthcDomain httpAuthenticatorWithChallenge(String type) { + this.httpAuthenticator = new HttpAuthenticator(type).challenge(true); + return this; + } + + public AuthcDomain httpAuthenticator(HttpAuthenticator httpAuthenticator) { + this.httpAuthenticator = httpAuthenticator; + return this; + } + + public AuthcDomain backend(String type) { + this.authenticationBackend = new AuthenticationBackend(type); + return this; + } + + public AuthcDomain backend(AuthenticationBackend authenticationBackend) { + this.authenticationBackend = authenticationBackend; + return this; + } + + public AuthcDomain skipUsers(String... users) { + this.skipUsers.addAll(Arrays.asList(users)); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("http_enabled", enabled); + xContentBuilder.field("order", order); + + if (httpAuthenticator != null) { + xContentBuilder.field("http_authenticator", httpAuthenticator); + } + + if (authenticationBackend != null) { + xContentBuilder.field("authentication_backend", authenticationBackend); + } + + if (skipUsers != null && skipUsers.size() > 0) { + xContentBuilder.field("skip_users", skipUsers); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + + public static class HttpAuthenticator implements ToXContentObject { + private final String type; + private boolean challenge; + private Map config = new HashMap(); + + public HttpAuthenticator(String type) { + this.type = type; + } + + public HttpAuthenticator challenge(boolean challenge) { + this.challenge = challenge; + return this; + } + + public HttpAuthenticator config(Map config) { + this.config.putAll(config); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("type", type); + xContentBuilder.field("challenge", challenge); + xContentBuilder.field("config", config); + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class AuthenticationBackend implements ToXContentObject { + private final String type; + private Supplier> config = () -> new HashMap(); + + public AuthenticationBackend(String type) { + this.type = type; + } + + public AuthenticationBackend config(Map config) { + Map configCopy = new HashMap<>(config); + this.config = () -> configCopy; + return this; + } + + public AuthenticationBackend config(Supplier> configSupplier) { + this.config = configSupplier; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("type", type); + xContentBuilder.field("config", config.get()); + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + } + + public void initIndex(Client client) { + Map settings = new HashMap<>(); + if (indexName.startsWith(".")) { + settings.put("index.hidden", true); + } + client.admin().indices().create(new CreateIndexRequest(indexName).settings(settings)).actionGet(); + + writeSingleEntryConfigToIndex(client, CType.CONFIG, config); + if(auditConfiguration != null) { + writeSingleEntryConfigToIndex(client, CType.AUDIT, "config", auditConfiguration); + } + writeConfigToIndex(client, CType.ROLES, roles); + writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers); + writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); + writeEmptyConfigToIndex(client, CType.ACTIONGROUPS); + writeEmptyConfigToIndex(client, CType.TENANTS); + } + + public void updateInternalUsersConfiguration(Client client, List users) { + Map userMap = new HashMap<>(); + for(User user : users) { + userMap.put(user.getName(), user); + } + updateConfigInIndex(client, CType.INTERNALUSERS, userMap); + } + + + static String hash(final char[] clearTextPassword) { + final byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); + Arrays.fill(salt, (byte) 0); + Arrays.fill(clearTextPassword, '\0'); + return hash; + } + + private void writeEmptyConfigToIndex(Client client, CType configType) { + writeConfigToIndex(client, configType, Collections.emptyMap()); + } + + private void writeConfigToIndex(Client client, CType configType, Map config) { + try { + String json = configToJson(configType, config); + + log.info("Writing security configuration into index " + configType + ":\n" + json); + + BytesReference bytesReference = toByteReference(json); + client.index(new IndexRequest(indexName).id(configType.toLCString()) + .setRefreshPolicy(IMMEDIATE).source(configType.toLCString(), bytesReference)) + .actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while initializing config for " + indexName, e); + } + } + + private static BytesReference toByteReference(String string) throws UnsupportedEncodingException { + return BytesReference.fromByteBuffer(ByteBuffer.wrap(string.getBytes("utf-8"))); + } + + private void updateConfigInIndex(Client client, CType configType, Map config) { + try { + String json = configToJson(configType, config); + BytesReference bytesReference = toByteReference(json); + log.info("Update configuration of type '{}' in index '{}', new value '{}'.", configType, indexName, json); + UpdateRequest upsert = new UpdateRequest(indexName, configType.toLCString()).doc(configType.toLCString(), bytesReference) + .setRefreshPolicy(IMMEDIATE); + client.update(upsert).actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while updating config for " + indexName, e); + } + } + + private static String configToJson(CType configType, Map config) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + + builder.startObject(); + builder.startObject("_meta"); + builder.field("type", configType.toLCString()); + builder.field("config_version", 2); + builder.endObject(); + + for (Map.Entry entry : config.entrySet()) { + builder.field(entry.getKey(), entry.getValue()); + } + + builder.endObject(); + + return Strings.toString(builder); + } + + private void writeSingleEntryConfigToIndex(Client client, CType configType, ToXContentObject config) { + writeSingleEntryConfigToIndex(client, configType, configType.toLCString(), config); + } + + private void writeSingleEntryConfigToIndex(Client client, CType configType, String configurationRoot, ToXContentObject config) { + try { + XContentBuilder builder = XContentFactory.jsonBuilder(); + + builder.startObject(); + builder.startObject("_meta"); + builder.field("type", configType.toLCString()); + builder.field("config_version", 2); + builder.endObject(); + + builder.field(configurationRoot, config); + + builder.endObject(); + + String json = Strings.toString(builder); + + log.info("Writing security plugin configuration into index " + configType + ":\n" + json); + + client.index(new IndexRequest(indexName).id(configType.toLCString()) + .setRefreshPolicy(IMMEDIATE).source(configType.toLCString(), toByteReference(json))) + .actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while initializing config for " + indexName, e); + } + } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 539e15fb57..13465294e5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -55,6 +55,7 @@ import org.opensearch.test.framework.AuditConfiguration; import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.AuthzDomain; +import org.opensearch.test.framework.OnBehalfOfConfig; import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; @@ -74,455 +75,419 @@ */ public class LocalCluster extends ExternalResource implements AutoCloseable, OpenSearchClientProvider { - private static final Logger log = LogManager.getLogger(LocalCluster.class); - - public static final String INIT_CONFIGURATION_DIR = "security.default_init.dir"; - - protected static final AtomicLong num = new AtomicLong(); - - private boolean sslOnly; - - private final List> plugins; - private final ClusterManager clusterManager; - private final TestSecurityConfig testSecurityConfig; - private Settings nodeOverride; - private final String clusterName; - private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; - private final TestCertificates testCertificates; - private final List clusterDependencies; - private final Map remotes; - private volatile LocalOpenSearchCluster localOpenSearchCluster; - private final List testIndices; - - private boolean loadConfigurationIntoIndex; - - private LocalCluster( - String clusterName, - TestSecurityConfig testSgConfig, - boolean sslOnly, - Settings nodeOverride, - ClusterManager clusterManager, - List> plugins, - TestCertificates testCertificates, - List clusterDependencies, - Map remotes, - List testIndices, - boolean loadConfigurationIntoIndex, - String defaultConfigurationInitDirectory - ) { - this.plugins = plugins; - this.testCertificates = testCertificates; - this.clusterManager = clusterManager; - this.testSecurityConfig = testSgConfig; - this.sslOnly = sslOnly; - this.nodeOverride = nodeOverride; - this.clusterName = clusterName; - this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); - this.remotes = remotes; - this.clusterDependencies = clusterDependencies; - this.testIndices = testIndices; - this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; - if (StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { - System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); - } - } - - public String getSnapshotDirPath() { - return localOpenSearchCluster.getSnapshotDirPath(); - } - - @Override - public void before() throws Throwable { - if (localOpenSearchCluster == null) { - for (LocalCluster dependency : clusterDependencies) { - if (!dependency.isStarted()) { - dependency.before(); - } - } - - for (Map.Entry entry : remotes.entrySet()) { - @SuppressWarnings("resource") - InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress(); - String key = "cluster.remote." + entry.getKey() + ".seeds"; - String value = transportAddress.getHostString() + ":" + transportAddress.getPort(); - log.info("Remote cluster '{}' added to configuration with the following seed '{}'", key, value); - nodeOverride = Settings.builder().put(nodeOverride).putList(key, value).build(); - } - start(); - } - } - - @Override - protected void after() { - System.clearProperty(INIT_CONFIGURATION_DIR); - close(); - } - - @Override - public void close() { - if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { - try { - localOpenSearchCluster.destroy(); - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - localOpenSearchCluster = null; - } - } - } - - @Override - public String getClusterName() { - return clusterName; - } - - @Override - public InetSocketAddress getHttpAddress() { - return localOpenSearchCluster.clientNode().getHttpAddress(); - } - - public int getHttpPort() { - return getHttpAddress().getPort(); - } - - @Override - public InetSocketAddress getTransportAddress() { - return localOpenSearchCluster.clientNode().getTransportAddress(); - } - - /** - * Returns a Client object that performs cluster-internal requests. As these requests are regard as cluster-internal, - * no authentication is performed and no user-information is attached to these requests. Thus, this client should - * be only used for preparing test environments, but not as a test subject. - */ - public Client getInternalNodeClient() { - return localOpenSearchCluster.clientNode().getInternalNodeClient(); - } - - /** - * Returns a random node of this cluster. - */ - public PluginAwareNode node() { - return this.localOpenSearchCluster.clusterManagerNode().esNode(); - } - - /** - * Returns all nodes of this cluster. - */ - public List nodes() { - return this.localOpenSearchCluster.getNodes(); - } - - public LocalOpenSearchCluster.Node getNodeByName(String name) { - return this.localOpenSearchCluster.getNodeByName(name); - } - - public boolean isStarted() { - return localOpenSearchCluster != null; - } - - public List getConfiguredUsers() { - return testSecurityConfig.getUsers(); - } - - public Random getRandom() { - return localOpenSearchCluster.getRandom(); - } - - private void start() { - try { - NodeSettingsSupplier nodeSettingsSupplier = minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings( - sslOnly, - nodeOverride - ); - localOpenSearchCluster = new LocalOpenSearchCluster( - clusterName, - clusterManager, - nodeSettingsSupplier, - plugins, - testCertificates - ); - - localOpenSearchCluster.start(); - - if (loadConfigurationIntoIndex) { - initSecurityIndex(testSecurityConfig); - } - - try (Client client = getInternalNodeClient()) { - for (TestIndex index : this.testIndices) { - index.create(client); - } - } - - } catch (Exception e) { - log.error("Local ES cluster start failed", e); - throw new RuntimeException(e); - } - } - - private void initSecurityIndex(TestSecurityConfig testSecurityConfig) { - log.info("Initializing OpenSearch Security index"); - try ( - Client client = new ContextHeaderDecoratorClient( - this.getInternalNodeClient(), - Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true") - ) - ) { - testSecurityConfig.initIndex(client); - triggerConfigurationReload(client); - } - } - - public void updateUserConfiguration(List users) { - try ( - Client client = new ContextHeaderDecoratorClient( - this.getInternalNodeClient(), - Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true") - ) - ) { - testSecurityConfig.updateInternalUsersConfiguration(client, users); - triggerConfigurationReload(client); - } - } - - private static void triggerConfigurationReload(Client client) { - ConfigUpdateResponse configUpdateResponse = client.execute( - ConfigUpdateAction.INSTANCE, - new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])) - ).actionGet(); - if (configUpdateResponse.hasFailures()) { - throw new RuntimeException("ConfigUpdateResponse produced failures: " + configUpdateResponse.failures()); - } - } - - public CertificateData getAdminCertificate() { - return testCertificates.getAdminCertificateData(); - } - - public static class Builder { - - private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); - - private boolean sslOnly = false; - private final List> plugins = new ArrayList<>(); - private Map remoteClusters = new HashMap<>(); - private List clusterDependencies = new ArrayList<>(); - private List testIndices = new ArrayList<>(); - private ClusterManager clusterManager = ClusterManager.DEFAULT; - private TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); - private String clusterName = "local_cluster"; - private TestCertificates testCertificates; - - private boolean loadConfigurationIntoIndex = true; - - private String defaultConfigurationInitDirectory = null; - - public Builder() {} - - public Builder dependsOn(Object object) { - // We just want to make sure that the object is already done - if (object == null) { - throw new IllegalStateException("Dependency not fulfilled"); - } - return this; - } - - public Builder clusterManager(ClusterManager clusterManager) { - this.clusterManager = clusterManager; - return this; - } - - /** - * Starts a cluster with only one node and thus saves some resources during startup. This shall be only used - * for tests where the node interactions are not relevant to the test. An example for this would be - * authentication tests, as authentication is always done on the directly connected node. - */ - public Builder singleNode() { - this.clusterManager = ClusterManager.SINGLENODE; - return this; - } - - /** - * Specifies the configuration of the security plugin that shall be used by this cluster. - */ - public Builder config(TestSecurityConfig testSecurityConfig) { - this.testSecurityConfig = testSecurityConfig; - return this; - } - - public Builder sslOnly(boolean sslOnly) { - this.sslOnly = sslOnly; - return this; - } - - public Builder nodeSettings(Map settings) { - settings.forEach((key, value) -> { - if (value instanceof List) { - List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); - nodeOverrideSettingsBuilder.putList(key, values); - } else { - nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); - } - }); - - return this; - } - - /** - * Adds additional plugins to the cluster - */ - public Builder plugin(Class plugin) { - this.plugins.add(plugin); - - return this; - } - - public Builder authFailureListeners(AuthFailureListeners listener) { - testSecurityConfig.authFailureListeners(listener); - return this; - } - - /** - * Specifies a remote cluster and its name. The remote cluster can be then used in Cross Cluster Search - * operations with the specified name. - */ - public Builder remote(String name, LocalCluster anotherCluster) { - remoteClusters.put(name, anotherCluster); - - clusterDependencies.add(anotherCluster); - - return this; - } - - /** - * Specifies test indices that shall be created upon startup of the cluster. - */ - public Builder indices(TestIndex... indices) { - this.testIndices.addAll(Arrays.asList(indices)); - return this; - } - - public Builder users(TestSecurityConfig.User... users) { - for (TestSecurityConfig.User user : users) { - testSecurityConfig.user(user); - } - return this; - } - - public Builder audit(AuditConfiguration auditConfiguration) { - if (auditConfiguration != null) { - testSecurityConfig.audit(auditConfiguration); - } - if (auditConfiguration.isEnabled()) { - nodeOverrideSettingsBuilder.put("plugins.security.audit.type", TestRuleAuditLogSink.class.getName()); - } else { - nodeOverrideSettingsBuilder.put("plugins.security.audit.type", "noop"); - } - return this; - } - - public List getUsers() { - return testSecurityConfig.getUsers(); - } - - public Builder roles(Role... roles) { - testSecurityConfig.roles(roles); - return this; - } - - public Builder rolesMapping(RolesMapping... mappings) { - testSecurityConfig.rolesMapping(mappings); - return this; - } - - public Builder authc(TestSecurityConfig.AuthcDomain authc) { - testSecurityConfig.authc(authc); - return this; - } - - public Builder authz(AuthzDomain authzDomain) { - testSecurityConfig.authz(authzDomain); - return this; - } - - public Builder clusterName(String clusterName) { - this.clusterName = clusterName; - return this; - } - - public Builder configIndexName(String configIndexName) { - testSecurityConfig.configIndexName(configIndexName); - return this; - } - - public Builder testCertificates(TestCertificates certificates) { - this.testCertificates = certificates; - return this; - } - - public Builder anonymousAuth(boolean anonAuthEnabled) { - testSecurityConfig.anonymousAuth(anonAuthEnabled); - return this; - } - - public Builder xff(XffConfig xffConfig) { - testSecurityConfig.xff(xffConfig); - return this; - } - - public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { - this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; - return this; - } - - public Builder certificates(TestCertificates certificates) { - this.testCertificates = certificates; - return this; - } - - public Builder doNotFailOnForbidden(boolean doNotFailOnForbidden) { - testSecurityConfig.doNotFailOnForbidden(doNotFailOnForbidden); - return this; - } - - public Builder defaultConfigurationInitDirectory(String defaultConfigurationInitDirectory) { - this.defaultConfigurationInitDirectory = defaultConfigurationInitDirectory; - return this; - } - - public LocalCluster build() { - try { - if (testCertificates == null) { - testCertificates = new TestCertificates(); - } - clusterName += "_" + num.incrementAndGet(); - Settings settings = nodeOverrideSettingsBuilder.build(); - return new LocalCluster( - clusterName, - testSecurityConfig, - sslOnly, - settings, - clusterManager, - plugins, - testCertificates, - clusterDependencies, - remoteClusters, - testIndices, - loadConfigurationIntoIndex, - defaultConfigurationInitDirectory - ); - } catch (Exception e) { - log.error("Failed to build LocalCluster", e); - throw new RuntimeException(e); - } - } - - } - - @Override - public TestCertificates getTestCertificates() { - return testCertificates; - } + private static final Logger log = LogManager.getLogger(LocalCluster.class); + + public static final String INIT_CONFIGURATION_DIR = "security.default_init.dir"; + + protected static final AtomicLong num = new AtomicLong(); + + private boolean sslOnly; + + private final List> plugins; + private final ClusterManager clusterManager; + private final TestSecurityConfig testSecurityConfig; + private Settings nodeOverride; + private final String clusterName; + private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; + private final TestCertificates testCertificates; + private final List clusterDependencies; + private final Map remotes; + private volatile LocalOpenSearchCluster localOpenSearchCluster; + private final List testIndices; + + private boolean loadConfigurationIntoIndex; + + private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, boolean sslOnly, Settings nodeOverride, + ClusterManager clusterManager, List> plugins, TestCertificates testCertificates, + List clusterDependencies, Map remotes, List testIndices, + boolean loadConfigurationIntoIndex, String defaultConfigurationInitDirectory) { + this.plugins = plugins; + this.testCertificates = testCertificates; + this.clusterManager = clusterManager; + this.testSecurityConfig = testSgConfig; + this.sslOnly = sslOnly; + this.nodeOverride = nodeOverride; + this.clusterName = clusterName; + this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); + this.remotes = remotes; + this.clusterDependencies = clusterDependencies; + this.testIndices = testIndices; + this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; + if(StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { + System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); + } + } + + public String getSnapshotDirPath() { + return localOpenSearchCluster.getSnapshotDirPath(); + } + + @Override + public void before() throws Throwable { + if (localOpenSearchCluster == null) { + for (LocalCluster dependency : clusterDependencies) { + if (!dependency.isStarted()) { + dependency.before(); + } + } + + for (Map.Entry entry : remotes.entrySet()) { + @SuppressWarnings("resource") + InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress(); + String key = "cluster.remote." + entry.getKey() + ".seeds"; + String value = transportAddress.getHostString() + ":" + transportAddress.getPort(); + log.info("Remote cluster '{}' added to configuration with the following seed '{}'", key, value); + nodeOverride = Settings.builder().put(nodeOverride) + .putList(key, value) + .build(); + } + start(); + } + } + + @Override + protected void after() { + System.clearProperty(INIT_CONFIGURATION_DIR); + close(); + } + + @Override + public void close() { + if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { + try { + localOpenSearchCluster.destroy(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + localOpenSearchCluster = null; + } + } + } + + @Override + public String getClusterName() { + return clusterName; + } + + @Override + public InetSocketAddress getHttpAddress() { + return localOpenSearchCluster.clientNode().getHttpAddress(); + } + + public int getHttpPort() { + return getHttpAddress().getPort(); + } + + @Override + public InetSocketAddress getTransportAddress() { + return localOpenSearchCluster.clientNode().getTransportAddress(); + } + + /** + * Returns a Client object that performs cluster-internal requests. As these requests are regard as cluster-internal, + * no authentication is performed and no user-information is attached to these requests. Thus, this client should + * be only used for preparing test environments, but not as a test subject. + */ + public Client getInternalNodeClient() { + return localOpenSearchCluster.clientNode().getInternalNodeClient(); + } + + /** + * Returns a random node of this cluster. + */ + public PluginAwareNode node() { + return this.localOpenSearchCluster.clusterManagerNode().esNode(); + } + + /** + * Returns all nodes of this cluster. + */ + public List nodes() { + return this.localOpenSearchCluster.getNodes(); + } + + public LocalOpenSearchCluster.Node getNodeByName(String name) { + return this.localOpenSearchCluster.getNodeByName(name); + } + + public boolean isStarted() { + return localOpenSearchCluster != null; + } + + public List getConfiguredUsers() { + return testSecurityConfig.getUsers(); + } + + public Random getRandom() { + return localOpenSearchCluster.getRandom(); + } + + private void start() { + try { + NodeSettingsSupplier nodeSettingsSupplier = minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings(sslOnly, nodeOverride); + localOpenSearchCluster = new LocalOpenSearchCluster(clusterName, clusterManager, nodeSettingsSupplier, plugins, testCertificates); + + localOpenSearchCluster.start(); + + + if (loadConfigurationIntoIndex) { + initSecurityIndex(testSecurityConfig); + } + + try (Client client = getInternalNodeClient()) { + for (TestIndex index : this.testIndices) { + index.create(client); + } + } + + } catch (Exception e) { + log.error("Local ES cluster start failed", e); + throw new RuntimeException(e); + } + } + + private void initSecurityIndex(TestSecurityConfig testSecurityConfig) { + log.info("Initializing OpenSearch Security index"); + try(Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true"))) { + testSecurityConfig.initIndex(client); + triggerConfigurationReload(client); + } + } + + public void updateUserConfiguration(List users) { + try(Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true"))) { + testSecurityConfig.updateInternalUsersConfiguration(client, users); + triggerConfigurationReload(client); + } + } + + private static void triggerConfigurationReload(Client client) { + ConfigUpdateResponse configUpdateResponse = client.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0]))).actionGet(); + if (configUpdateResponse.hasFailures()) { + throw new RuntimeException("ConfigUpdateResponse produced failures: " + configUpdateResponse.failures()); + } + } + + public CertificateData getAdminCertificate() { + return testCertificates.getAdminCertificateData(); + } + + public static class Builder { + + private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); + + private boolean sslOnly = false; + private final List> plugins = new ArrayList<>(); + private Map remoteClusters = new HashMap<>(); + private List clusterDependencies = new ArrayList<>(); + private List testIndices = new ArrayList<>(); + private ClusterManager clusterManager = ClusterManager.DEFAULT; + private TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); + private String clusterName = "local_cluster"; + private TestCertificates testCertificates; + + private boolean loadConfigurationIntoIndex = true; + + private String defaultConfigurationInitDirectory = null; + + public Builder() { + } + + public Builder dependsOn(Object object) { + // We just want to make sure that the object is already done + if (object == null) { + throw new IllegalStateException("Dependency not fulfilled"); + } + return this; + } + + public Builder clusterManager(ClusterManager clusterManager) { + this.clusterManager = clusterManager; + return this; + } + + /** + * Starts a cluster with only one node and thus saves some resources during startup. This shall be only used + * for tests where the node interactions are not relevant to the test. An example for this would be + * authentication tests, as authentication is always done on the directly connected node. + */ + public Builder singleNode() { + this.clusterManager = ClusterManager.SINGLENODE; + return this; + } + + /** + * Specifies the configuration of the security plugin that shall be used by this cluster. + */ + public Builder config(TestSecurityConfig testSecurityConfig) { + this.testSecurityConfig = testSecurityConfig; + return this; + } + + public Builder sslOnly(boolean sslOnly) { + this.sslOnly = sslOnly; + return this; + } + + public Builder nodeSettings(Map settings) { + settings.forEach((key, value) -> { + if (value instanceof List) { + List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); + nodeOverrideSettingsBuilder.putList(key, values); + } else { + nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); + } + }); + + return this; + } + + /** + * Adds additional plugins to the cluster + */ + public Builder plugin(Class plugin) { + this.plugins.add(plugin); + + return this; + } + + public Builder authFailureListeners(AuthFailureListeners listener) { + testSecurityConfig.authFailureListeners(listener); + return this; + } + + /** + * Specifies a remote cluster and its name. The remote cluster can be then used in Cross Cluster Search + * operations with the specified name. + */ + public Builder remote(String name, LocalCluster anotherCluster) { + remoteClusters.put(name, anotherCluster); + + clusterDependencies.add(anotherCluster); + + return this; + } + + /** + * Specifies test indices that shall be created upon startup of the cluster. + */ + public Builder indices(TestIndex... indices) { + this.testIndices.addAll(Arrays.asList(indices)); + return this; + } + + public Builder users(TestSecurityConfig.User... users) { + for (TestSecurityConfig.User user : users) { + testSecurityConfig.user(user); + } + return this; + } + + public Builder audit(AuditConfiguration auditConfiguration) { + if (auditConfiguration != null) { + testSecurityConfig.audit(auditConfiguration); + } + if (auditConfiguration.isEnabled()) { + nodeOverrideSettingsBuilder.put("plugins.security.audit.type", TestRuleAuditLogSink.class.getName()); + } else { + nodeOverrideSettingsBuilder.put("plugins.security.audit.type", "noop"); + } + return this; + } + + public List getUsers() { + return testSecurityConfig.getUsers(); + } + + public Builder roles(Role... roles) { + testSecurityConfig.roles(roles); + return this; + } + + public Builder rolesMapping(RolesMapping...mappings) { + testSecurityConfig.rolesMapping(mappings); + return this; + } + + public Builder authc(TestSecurityConfig.AuthcDomain authc) { + testSecurityConfig.authc(authc); + return this; + } + + public Builder authz(AuthzDomain authzDomain) { + testSecurityConfig.authz(authzDomain); + return this; + } + + public Builder clusterName(String clusterName) { + this.clusterName = clusterName; + return this; + } + + public Builder configIndexName(String configIndexName) { + testSecurityConfig.configIndexName(configIndexName); + return this; + } + + public Builder testCertificates(TestCertificates certificates) { + this.testCertificates = certificates; + return this; + } + + public Builder anonymousAuth(boolean anonAuthEnabled) { + testSecurityConfig.anonymousAuth(anonAuthEnabled); + return this; + } + + public Builder xff(XffConfig xffConfig){ + testSecurityConfig.xff(xffConfig); + return this; + } + + public Builder onBehalfOf(OnBehalfOfConfig onBehalfOfConfig){ + testSecurityConfig.onBehalfOf(onBehalfOfConfig); + return this; + } + + public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { + this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; + return this; + } + public Builder certificates(TestCertificates certificates) { + this.testCertificates = certificates; + return this; + } + + public Builder doNotFailOnForbidden(boolean doNotFailOnForbidden) { + testSecurityConfig.doNotFailOnForbidden(doNotFailOnForbidden); + return this; + } + + public Builder defaultConfigurationInitDirectory(String defaultConfigurationInitDirectory){ + this.defaultConfigurationInitDirectory = defaultConfigurationInitDirectory; + return this; + } + + public LocalCluster build() { + try { + if(testCertificates == null) { + testCertificates = new TestCertificates(); + } + clusterName += "_" + num.incrementAndGet(); + Settings settings = nodeOverrideSettingsBuilder.build(); + return new LocalCluster(clusterName, testSecurityConfig, sslOnly, settings, clusterManager, plugins, testCertificates, + clusterDependencies, remoteClusters, testIndices, loadConfigurationIntoIndex, defaultConfigurationInitDirectory); + } catch (Exception e) { + log.error("Failed to build LocalCluster", e); + throw new RuntimeException(e); + } + } + + } + + @Override + public TestCertificates getTestCertificates() { + return testCertificates; + } } From 98674b8017b01f4ed9880d4bed209bf7481fc1d9 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 7 Jun 2023 13:24:13 -0700 Subject: [PATCH 10/74] Adding this obo config to xcontent builder and remove unused imports Signed-off-by: Ryan Liang --- .../org/opensearch/test/framework/TestSecurityConfig.java | 4 ++++ .../org/opensearch/security/http/OnBehalfOfAuthenticator.java | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index b47fbed32d..9ab1e70e8c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -221,6 +221,10 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.startObject(); xContentBuilder.startObject("dynamic"); + if (onBehalfOfConfig != null) { + xContentBuilder.field("on_behalf_of", onBehalfOfConfig); + } + if (anonymousAuth || (xffConfig != null)) { xContentBuilder.startObject("http"); xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index fbf9a07954..f82ee788f5 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -32,7 +32,6 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.greenrobot.eventbus.Subscribe; import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; @@ -42,7 +41,6 @@ import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; -import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.AuthCredentials; public class OnBehalfOfAuthenticator implements HTTPAuthenticator { From aa4e5994f96d9d2a68f8292da17e0908ab443677 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 10:15:13 -0700 Subject: [PATCH 11/74] Adding this obo config to xcontent builder and create obo authz header factory Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 241 +++++------------- ...BehalfOfJwtAuthorizationHeaderFactory.java | 58 +++++ .../test/framework/OnBehalfOfConfig.java | 40 +-- 3 files changed, 141 insertions(+), 198 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 9e78274c34..d9702b2ec8 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -1,5 +1,11 @@ package org.opensearch.security.http; +import java.io.IOException; +import java.security.KeyPair; +import java.util.Base64; +import java.util.List; +import java.util.Map; + import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @@ -10,6 +16,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; + import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Client; @@ -21,12 +28,6 @@ import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.log.LogsRule; -import java.io.IOException; -import java.security.KeyPair; -import java.util.Base64; -import java.util.List; -import java.util.Map; - import static java.nio.charset.StandardCharsets.US_ASCII; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.hamcrest.MatcherAssert.assertThat; @@ -49,191 +50,75 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class OnBehalfOfJwtAuthenticationTest { - public static final String CLAIM_USERNAME = "test-user"; - public static final String CLAIM_ROLES = "backend-user-roles"; + public static final String CLAIM_USERNAME = "test-user"; + public static final String CLAIM_ROLES = "backend-user-roles"; + + public static final String USER_SUPERHERO = "superhero"; + public static final String USERNAME_ROOT = "root"; + public static final String ROLE_ADMIN = "role_admin"; + public static final String ROLE_DEVELOPER = "role_developer"; + public static final String ROLE_QA = "role_qa"; + public static final String ROLE_CTO = "role_cto"; + public static final String ROLE_CEO = "role_ceo"; + public static final String ROLE_VP = "role_vp"; + public static final String POINTER_BACKEND_ROLES = "/backend_roles"; + public static final String POINTER_USERNAME = "/user_name"; + + public static final String QA_DEPARTMENT = "qa-department"; - public static final String USER_SUPERHERO = "superhero"; - public static final String USERNAME_ROOT = "root"; - public static final String ROLE_ADMIN = "role_admin"; - public static final String ROLE_DEVELOPER = "role_developer"; - public static final String ROLE_QA = "role_qa"; - public static final String ROLE_CTO = "role_cto"; - public static final String ROLE_CEO = "role_ceo"; - public static final String ROLE_VP = "role_vp"; - public static final String POINTER_BACKEND_ROLES = "/backend_roles"; - public static final String POINTER_USERNAME = "/user_name"; + public static final String CLAIM_DEPARTMENT = "department"; - public static final String QA_DEPARTMENT = "qa-department"; + public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); - public static final String CLAIM_DEPARTMENT = "department"; + public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); - public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); + private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); - public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + private static final String JWT_AUTH_HEADER = "jwt-auth"; - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( + KEY_PAIR.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + JWT_AUTH_HEADER); - private static final String JWT_AUTH_HEADER = "jwt-auth"; + public static final String SONG_ID_1 = "song-id-01"; - private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( - KEY_PAIR.getPrivate(), - CLAIM_USERNAME, - CLAIM_ROLES, - JWT_AUTH_HEADER); + public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") + .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); - public static final String SONG_ID_1 = "song-id-01"; + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder() + .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) + .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) + .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() + .build(); - public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") - .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() - .build(); + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + } + try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ + client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); + } + } - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); + @Test + public void shouldAuthenticateWithJwtToken_positive() { + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ - @BeforeClass - public static void createTestData() { - try (Client client = cluster.getInternalNodeClient()) { - client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - } - try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ - client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); - } - } + TestRestClient.HttpResponse response = client.getAuthInfo(); - @Test - public void shouldAuthenticateWithJwtToken_positive() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(username)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USERNAME_ROOT)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureLackingUserName() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("No subject found in JWT token"); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureExpiredToken() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("Invalid or expired JWT token."); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { - Header header = new BasicHeader(AUTHORIZATION, "not.a.token"); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { - KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); - Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("Invalid or expired JWT token."); - } - } - - @Test - public void shouldReadRolesFromToken_positiveFirstRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(roles, hasSize(3)); - assertThat(roles, containsInAnyOrder(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA)); - } - } - - @Test - public void shouldReadRolesFromToken_positiveSecondRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); - try(TestRestClient client = cluster.getRestClient(header)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(roles, hasSize(3)); - assertThat(roles, containsInAnyOrder(ROLE_CTO, ROLE_CEO, ROLE_VP)); - } - } - - @Test - public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { - String[] roles = { ROLE_VP }; - Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ - SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse response = client.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - assertThat(response, searchHitsContainDocumentWithId(0, QA_SONG_INDEX_NAME, SONG_ID_1)); - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - } - - @Test - public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { - String[] roles = { ROLE_VP }; - Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ - SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); - - assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(username)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java new file mode 100644 index 0000000000..7516cf4a5e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -0,0 +1,58 @@ +/* + * 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 java.util.List; +import java.util.function.LongSupplier; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.authtoken.jwt.JwtVendor; + +import static java.util.Objects.requireNonNull; + +class OnBehalfOfJwtAuthorizationHeaderFactory { + + private final String issuer; + private final String subject; + private final String audience; + private final List roles; + private final String encryption_key; + private final String signing_key; + private final String headerName; + private final Integer expirySeconds; + + + public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer, String subject, String audience, List roles ,Integer expirySeconds, String headerName, String encryption_key) { + this.signing_key = requireNonNull(signing_key, "signing key is required"); + this.issuer = requireNonNull(issuer, "Issuer is required"); + this.subject = requireNonNull(subject, "Subject is required"); + this.audience = requireNonNull(audience, "Audience is required."); + this.roles = requireNonNull(roles, "Roles claim is required"); + this.expirySeconds = requireNonNull(expirySeconds, "Expiry is required"); + this.headerName = requireNonNull(headerName, "Header name is required"); + this.encryption_key = encryption_key; + } + + Header generateValidToken() throws Exception { + LongSupplier currentTime = () -> (System.currentTimeMillis() / 1000); + Settings settings = Settings.builder().put("signing_key", signing_key).put("encryption_key", encryption_key).build(); + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + + return toHeader(encodedJwt); + } + + private BasicHeader toHeader(String token) { + return new BasicHeader(headerName, token); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java index d68c0d73d2..cf9be42114 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java @@ -18,27 +18,27 @@ import org.opensearch.core.xcontent.XContentBuilder; public class OnBehalfOfConfig implements ToXContentObject { - private String signing_key; - private String encryption_key; + private String signing_key; + private String encryption_key; - public OnBehalfOfConfig signing_key(String signing_key) { - this.signing_key = signing_key; - return this; - } + public OnBehalfOfConfig signing_key(String signing_key) { + this.signing_key = signing_key; + return this; + } - public OnBehalfOfConfig encryption_key(String encryption_key) { - this.encryption_key = encryption_key; - return this; - } + public OnBehalfOfConfig encryption_key(String encryption_key) { + this.encryption_key = encryption_key; + return this; + } - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { - xContentBuilder.startObject(); - xContentBuilder.field("signing_key", signing_key); - if (StringUtils.isNoneBlank(encryption_key)){ - xContentBuilder.field("encryption_key", encryption_key); - } - xContentBuilder.endObject(); - return xContentBuilder; - } + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("signing_key", signing_key); + if (StringUtils.isNoneBlank(encryption_key)){ + xContentBuilder.field("encryption_key", encryption_key); + } + xContentBuilder.endObject(); + return xContentBuilder; + } } From 82048749cbed49322389cf52e1e8b724a3725242 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 11:56:12 -0700 Subject: [PATCH 12/74] Done happy testing case Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 43 ++++++++++++------- ...BehalfOfJwtAuthorizationHeaderFactory.java | 2 +- .../securityconf/DynamicConfigModelV7.java | 2 +- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index d9702b2ec8..a63230c5f2 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -1,6 +1,7 @@ package org.opensearch.security.http; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.util.Base64; import java.util.List; @@ -22,6 +23,7 @@ import org.opensearch.client.Client; import org.opensearch.client.RestHighLevelClient; import org.opensearch.test.framework.JwtConfigBuilder; +import org.opensearch.test.framework.OnBehalfOfConfig; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -54,14 +56,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final String CLAIM_ROLES = "backend-user-roles"; public static final String USER_SUPERHERO = "superhero"; - public static final String USERNAME_ROOT = "root"; - public static final String ROLE_ADMIN = "role_admin"; - public static final String ROLE_DEVELOPER = "role_developer"; - public static final String ROLE_QA = "role_qa"; - public static final String ROLE_CTO = "role_cto"; - public static final String ROLE_CEO = "role_ceo"; public static final String ROLE_VP = "role_vp"; - public static final String POINTER_BACKEND_ROLES = "/backend_roles"; public static final String POINTER_USERNAME = "/user_name"; public static final String QA_DEPARTMENT = "qa-department"; @@ -73,17 +68,31 @@ public class OnBehalfOfJwtAuthenticationTest { public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); private static final String JWT_AUTH_HEADER = "jwt-auth"; - private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( - KEY_PAIR.getPrivate(), - CLAIM_USERNAME, - CLAIM_ROLES, - JWT_AUTH_HEADER); + public static final String issuer = "cluster_0"; + public static final String subject = "testUser"; + public static final String audience = "audience_0"; + public static final Integer expirySeconds = 100000; + public static final String headerName = "Bearer"; + public static final List roles = List.of("admin", "HR"); + + private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); + private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + + private static final OnBehalfOfJwtAuthorizationHeaderFactory tokenFactory = new OnBehalfOfJwtAuthorizationHeaderFactory( + signingKey, + issuer, + subject, + audience, + roles, + expirySeconds, + headerName, + encryptionKey + ); public static final String SONG_ID_1 = "song-id-01"; @@ -94,7 +103,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE).config() + .authc(AUTHC_HTTPBASIC_INTERNAL).onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) .build(); @Rule @@ -112,13 +121,15 @@ public static void createTestData() { @Test public void shouldAuthenticateWithJwtToken_positive() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ + try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ TestRestClient.HttpResponse response = client.getAuthInfo(); response.assertStatusCode(200); String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(username)); + assertThat("testUser", equalTo(username)); + } catch (Exception e) { + throw new RuntimeException(e); } } } diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java index 7516cf4a5e..3d5fcf91aa 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -33,7 +33,7 @@ class OnBehalfOfJwtAuthorizationHeaderFactory { public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer, String subject, String audience, List roles ,Integer expirySeconds, String headerName, String encryption_key) { - this.signing_key = requireNonNull(signing_key, "signing key is required"); + this.signing_key = requireNonNull(signing_key, "Signing key is required"); this.issuer = requireNonNull(issuer, "Issuer is required"); this.subject = requireNonNull(subject, "Subject is required"); this.audience = requireNonNull(audience, "Audience is required."); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 56f65e24b7..d366f86bed 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -316,7 +316,7 @@ private void buildAAA() { Settings oboSettings = getDynamicOnBehalfOfSettings(); if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { - final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new HTTPOnBehalfOfJwtAuthenticator(getDynamicOnBehalfOfSettings()), false, 0); + final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new HTTPOnBehalfOfJwtAuthenticator(getDynamicOnBehalfOfSettings()), false, -1); restAuthDomains0.add(_ad); } From 293791e2f62017cb60d1ea6a7b1d09e2c77ef311 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 13:32:43 -0700 Subject: [PATCH 13/74] Fix the header Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 66 +------------------ ...BehalfOfJwtAuthorizationHeaderFactory.java | 2 +- .../http/OnBehalfOfAuthenticator.java | 1 + 3 files changed, 5 insertions(+), 64 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index a63230c5f2..dcc6323f97 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -1,83 +1,40 @@ package org.opensearch.security.http; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.KeyPair; import java.util.Base64; import java.util.List; import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.message.BasicHeader; -import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.Client; -import org.opensearch.client.RestHighLevelClient; -import org.opensearch.test.framework.JwtConfigBuilder; import org.opensearch.test.framework.OnBehalfOfConfig; 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 org.opensearch.test.framework.log.LogsRule; -import static java.nio.charset.StandardCharsets.US_ASCII; -import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; -import static org.opensearch.client.RequestOptions.DEFAULT; -import static org.opensearch.rest.RestStatus.FORBIDDEN; -import static org.opensearch.security.Song.*; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; -import static org.opensearch.test.framework.cluster.SearchRequestFactory.queryStringQueryRequest; -import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; -import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; -import static org.opensearch.test.framework.matcher.SearchResponseMatchers.*; -import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class OnBehalfOfJwtAuthenticationTest { - public static final String CLAIM_USERNAME = "test-user"; - public static final String CLAIM_ROLES = "backend-user-roles"; - - public static final String USER_SUPERHERO = "superhero"; - public static final String ROLE_VP = "role_vp"; public static final String POINTER_USERNAME = "/user_name"; - public static final String QA_DEPARTMENT = "qa-department"; - - public static final String CLAIM_DEPARTMENT = "department"; - - public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); - - public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); - - private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - private static final String JWT_AUTH_HEADER = "jwt-auth"; - public static final String issuer = "cluster_0"; public static final String subject = "testUser"; public static final String audience = "audience_0"; public static final Integer expirySeconds = 100000; - public static final String headerName = "Bearer"; + public static final String headerName = "Authorization"; public static final List roles = List.of("admin", "HR"); private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); @@ -94,31 +51,14 @@ public class OnBehalfOfJwtAuthenticationTest { encryptionKey ); - public static final String SONG_ID_1 = "song-id-01"; - - public static final TestSecurityConfig.Role DEPARTMENT_SONG_LISTENER_ROLE = new TestSecurityConfig.Role("department-song-listener-role") - .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); - @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL).onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) .build(); - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); - - @BeforeClass - public static void createTestData() { - try (Client client = cluster.getInternalNodeClient()) { - client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - } - try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ - client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); - } - } - @Test public void shouldAuthenticateWithJwtToken_positive() { try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java index 3d5fcf91aa..108806cd24 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -53,6 +53,6 @@ Header generateValidToken() throws Exception { } private BasicHeader toHeader(String token) { - return new BasicHeader(headerName, token); + return new BasicHeader(headerName, "Bearer " + token); } } diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index f82ee788f5..bc848a5e66 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -216,6 +216,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { return null; } catch (Exception e) { System.out.println("Error MSG2!" + e.getMessage()); + e.printStackTrace(); if(log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); } From 847f551c41e0ca8eac2f11b43b592f04f23004d2 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 8 Jun 2023 13:38:59 -0700 Subject: [PATCH 14/74] Revert the temorary fix of zstd library Signed-off-by: Ryan Liang --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4e9e6d02cc..789184d77e 100644 --- a/build.gradle +++ b/build.gradle @@ -336,7 +336,6 @@ configurations { force "io.netty:netty-transport:${versions.netty}" force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "org.apache.bcel:bcel:6.6.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 - force "com.github.luben:zstd-jni:${versions.zstd}" } } From 10477299fba71c93be0c4d5d8d7f95334f733e05 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 22:40:22 +0000 Subject: [PATCH 15/74] Misc cleanup Signed-off-by: Peter Nied --- .../http/OnBehalfOfAuthenticator.java | 124 ++++++------------ .../http/OnBehalfOfAuthenticatorTest.java | 22 +++- 2 files changed, 55 insertions(+), 91 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index bc848a5e66..eb5fa434df 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -20,6 +20,7 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; +import java.util.Objects; import java.util.Map.Entry; import java.util.regex.Pattern; @@ -49,75 +50,51 @@ public class OnBehalfOfAuthenticator implements HTTPAuthenticator { private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER_PREFIX = "bearer "; + private static final String SUBJECT_CLAIM = "sub"; - //TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING - private JwtParser jwtParser; - private String subjectKey; + private final JwtParser jwtParser; + private final String encryptionKey; - private String signingKey; - private String encryptionKey; - private volatile boolean initialized; - - public OnBehalfOfAuthenticator() { - super(); - init(); - } - - public HTTPOnBehalfOfJwtAuthenticator(Settings settings){ - this.signingKey = settings.get("signing_key"); - this.encryptionKey = settings.get("encryption_key"); - init(); - } - - // FOR TESTING - public OnBehalfOfAuthenticator(String signingKey, String encryptionKey){ - this.signingKey = signingKey; - this.encryptionKey = encryptionKey; - init(); + public OnBehalfOfAuthenticator(Settings settings) { + encryptionKey = settings.get("encryption_key"); + jwtParser = initParser(settings.get("signing_key")); } - public boolean isInitialized(){ - return initialized; - } - - private void init() { + private JwtParser initParser(final String signingKey) { + if (signingKey == null || signingKey.length() == 0) { + throw new RuntimeException("Unable to find on behalf of authenticator signing key"); + } try { - if(signingKey == null || signingKey.length() == 0) { - log.error("signingKey must not be null or empty. JWT authentication will not work"); - } else { + final String minmalKeyFormat = signingKey + .replace("-----BEGIN PUBLIC KEY-----\n", "") + .replace("-----END PUBLIC KEY-----", ""); - signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", ""); - signingKey = signingKey.replace("-----END PUBLIC KEY-----", ""); + final byte[] decoded = Decoders.BASE64.decode(minmalKeyFormat); + Key key = null; - byte[] decoded = Decoders.BASE64.decode(signingKey); - Key key = null; - - try { - key = getPublicKey(decoded, "RSA"); - } catch (Exception e) { - log.debug("No public RSA key, try other algos ({})", e.toString()); - } - - try { - key = getPublicKey(decoded, "EC"); - } catch (Exception e) { - log.debug("No public ECDSA key, try other algos ({})", e.toString()); - } + try { + key = getPublicKey(decoded, "RSA"); + } catch (Exception e) { + log.debug("No public RSA key, try other algos ({})", e.toString()); + } - if(key != null) { - jwtParser = Jwts.parser().setSigningKey(key); - } else { - jwtParser = Jwts.parser().setSigningKey(decoded); - } + try { + key = getPublicKey(decoded, "EC"); + } catch (Exception e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } + if (Objects.nonNull(key)) { + return Jwts.parser().setSigningKey(key); } + // Fallback to the decoded signing key + // TODO: Should we ever do this, I think no?? + return Jwts.parser().setSigningKey(decoded); } catch (Throwable e) { log.error("Error while creating JWT authenticator", e); throw new RuntimeException(e); } - - subjectKey = "sub"; } @Override @@ -170,9 +147,17 @@ private AuthCredentials extractCredentials0(final RestRequest request) { try { final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); - final String subject = extractSubject(claims, request); + final String subject = claims.getSubject(); + if (Objects.isNull(subject)) { + log.error("Valid jwt on behalf of token with no subject"); + return null; + } final String audience = claims.getAudience(); + if (Objects.isNull(subject)) { + log.error("Valid jwt on behalf of token with no audience"); + return null; + } String[] roles; @@ -193,15 +178,6 @@ private AuthCredentials extractCredentials0(final RestRequest request) { roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); } - if (subject == null) { - log.error("No subject found in JWT token"); - return null; - } - - if (audience == null) { - log.error("No audience found in JWT token"); - } - final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); for(Entry claim: claims.entrySet()) { @@ -234,26 +210,6 @@ public String getType() { return "onbehalfof_jwt"; } - //TODO: Extract the audience (ext_id) and inject it into thread context - - protected String extractSubject(final Claims claims, final RestRequest request) { - String subject = claims.getSubject(); - if(subjectKey != null) { - // try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException - Object subjectObject = claims.get(subjectKey, Object.class); - if(subjectObject == null) { - log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); - return null; - } - // We expect a String. If we find something else, convert to String but issue a warning - if(!(subjectObject instanceof String)) { - log.warn("Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", subjectKey, subjectObject, subjectObject.getClass()); - } - subject = String.valueOf(subjectObject); - } - return subject; - } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, InvalidKeySpecException { X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance(algo); diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index b3b81485c2..6de931b712 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -31,6 +31,7 @@ import org.junit.Assert; import org.junit.Test; +import org.opensearch.common.settings.Settings; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; @@ -43,9 +44,9 @@ public class OnBehalfOfAuthenticatorTest { new SecureRandom().nextBytes(secretKeyBytes); secretKey = Keys.hmacShaKeyFor(secretKeyBytes); } - final static String signingKey = BaseEncoding.base64().encode(secretKeyBytes); + @Test public void testNoKey() throws Exception { @@ -88,7 +89,7 @@ public void testBadKey() throws Exception { @Test public void testTokenMissing() throws Exception { - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey,claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()), null); @@ -101,7 +102,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -118,7 +119,7 @@ public void testBearer() throws Exception { .signWith(secretKey, SignatureAlgorithm.HS512) .compact(); - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -134,7 +135,7 @@ public void testBearer() throws Exception { public void testBearerWrongPosition() throws Exception { String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); @@ -147,7 +148,7 @@ public void testBearerWrongPosition() throws Exception { @Test public void testBasicAuthHeader() throws Exception { String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); - HTTPOnBehalfOfJwtAuthenticator jwtAuth = new HTTPOnBehalfOfJwtAuthenticator(signingKey, claimsEncryptionKey); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); @@ -280,8 +281,15 @@ private AuthCredentials extractCredentialsFromJwtHeader( final Boolean bwcPluginCompatibilityMode ) { final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); - final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(signingKey, encryptionKey); + final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); } + + private Settings defaultSettings() { + return Settings.builder() + .put("signing_key", signingKey) + .put("encryption_key", claimsEncryptionKey) + .build(); + } } From d00e2e7081aef042d728e5fac38a02619a9261cf Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Thu, 8 Jun 2023 22:41:29 +0000 Subject: [PATCH 16/74] Remove bad import Signed-off-by: Peter Nied --- .../security/authtoken/jwt/EncryptionDecryptionUtil.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 9fb407128c..b2e2102edd 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -19,8 +19,6 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import org.opensaml.xmlsec.encryption.P; - public class EncryptionDecryptionUtil { public static String encrypt(final String secret, final String data) { From f46070f12426a391a625d150d1faa65467eda072 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Fri, 9 Jun 2023 20:52:11 +0000 Subject: [PATCH 17/74] Add endpoint and test that interactions with JwtVendor Signed-off-by: Peter Nied --- .../http/OnBehalfOfJwtAuthenticationTest.java | 1 + .../security/OpenSearchSecurityPlugin.java | 2 + .../onbehalf/CreateOnBehalfOfToken.java | 141 ++++++++++++++++++ .../security/authtoken/jwt/JwtVendor.java | 11 +- .../dlic/rest/api/AccountApiTest.java | 5 +- 5 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index dcc6323f97..82cb286043 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -61,6 +61,7 @@ public class OnBehalfOfJwtAuthenticationTest { @Test public void shouldAuthenticateWithJwtToken_positive() { + // TODO: This integration test should use an endpoint to get an OnBehalfOf token, not generate it try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ TestRestClient.HttpResponse response = client.getAuthInfo(); diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 0a0726c0a7..261ef98374 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -117,6 +117,7 @@ import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.TransportConfigUpdateAction; +import org.opensearch.security.action.onbehalf.CreateOnBehalfOfToken; import org.opensearch.security.action.whoami.TransportWhoAmIAction; import org.opensearch.security.action.whoami.WhoAmIAction; import org.opensearch.security.auditlog.AuditLog; @@ -477,6 +478,7 @@ public List getRestHandlers(Settings settings, RestController restC Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.add(new CreateOnBehalfOfToken(settings, threadPool)); handlers.addAll( SecurityRestApiActions.getHandler( settings, diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java new file mode 100644 index 0000000000..aa1557cd7e --- /dev/null +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -0,0 +1,141 @@ +package org.opensearch.security.action.onbehalf; + +import java.io.IOException; +import java.util.List; + +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.nodes.TransportNodesAction; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.inject.Provider; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.authtoken.jwt.JwtVendor; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportRequest; +import org.opensearch.transport.TransportService; +import org.opensearch.rest.BaseRestHandler; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.rest.RestStatus; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.client.node.NodeClient; +import org.opensearch.security.user.User; + +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class CreateOnBehalfOfToken extends BaseRestHandler { + + private final JwtVendor vendor; + private final ThreadPool threadPool; + + public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { + Settings testSettings = Settings.builder() + .put("signing_key", "1234567890123456") + .put("encryption_key", "1234567890123456").build(); + + this.vendor = new JwtVendor(testSettings, Optional.empty()); + this.threadPool = threadPool; + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public List routes() { + return addRoutesPrefix( + ImmutableList.of( + new Route(Method.POST, "/user/onbehalfof") + ) + ); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + switch (request.method()) { + case POST: + return handlePost(request, client); + default: + throw new IllegalArgumentException(request.method() + " not supported"); + } + } + + private RestChannelConsumer handlePost(RestRequest request, NodeClient client) throws IOException { + return new RestChannelConsumer() { + @Override + public void accept(RestChannel channel) throws Exception { + final XContentBuilder builder = channel.newBuilder(); + BytesRestResponse response; + + try { + builder.startObject(); + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + builder.field("user_name", "nothing"); + builder.field("user", user.toString()); + final String token = vendor.createJwt("me", user.getName(), "self-issued", 60, List.of("1", "2", "3")); + builder.field("field_token", token); + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + } catch (final Exception exception) { + System.out.println(exception.toString()); + builder.startObject() + .field("error", exception.toString()) + .endObject(); + + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + } + builder.close(); + channel.sendResponse(response); + } + }; + } + +} 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 93efc93b06..1587dcab8c 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -45,15 +45,15 @@ public class JwtVendor { private static JsonMapObjectReaderWriter jsonMapReaderWriter = new JsonMapObjectReaderWriter(); - private String claimsEncryptionKey; - private JsonWebKey signingKey; - private JoseJwtProducer jwtProducer; + private final String claimsEncryptionKey; + private final JsonWebKey signingKey; + private final JoseJwtProducer jwtProducer; private final LongSupplier timeProvider; //TODO: Relocate/Remove them at once we make the descisions about the `roles` private ConfigModel configModel; // This never gets assigned, how does this work at all? - public JwtVendor(Settings settings, final Optional timeProvider) { + public JwtVendor(final Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { this.signingKey = createJwkFromSettings(settings); @@ -67,6 +67,7 @@ public JwtVendor(Settings settings, final Optional timeProvider) { this.claimsEncryptionKey = settings.get("encryption_key"); } this.timeProvider = timeProvider.orElseGet(() -> System::currentTimeMillis); + this.configModel = null; } /* @@ -106,7 +107,7 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { } } - String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { + public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { long timeMillis = timeProvider.getAsLong(); Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index c07b0f1333..e381c73d08 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -40,7 +40,7 @@ public AccountApiTest() { BASE_ENDPOINT = getEndpointPrefix() + "/api/"; ENDPOINT = getEndpointPrefix() + "/api/account"; } - + @Test public void testGetAccount() throws Exception { // arrange @@ -74,6 +74,9 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsList("custom_attribute_names").size()); assertNotNull(body.getAsSettings("tenants")); assertNotNull(body.getAsList("roles")); + + response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "", encodeBasicHeader(testUser, testPass)); + System.out.println(response.getBody()); } @Test From 4896a4bda054c595b624e5f91a78903069214ba5 Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Mon, 12 Jun 2023 16:10:01 +0000 Subject: [PATCH 18/74] AlFix to do both decrypt and encrypt tokens Signed-off-by: Peter Nied --- .../jwt/EncryptionDecryptionUtil.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index b2e2102edd..c0e2429948 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -22,26 +22,26 @@ public class EncryptionDecryptionUtil { public static String encrypt(final String secret, final String data) { - final Cipher cipher = createCipherFromSecret(secret); + final Cipher cipher = createCipherFromSecret(secret, CipherMode.ENCRYPT); final byte[] cipherText = createCipherText(cipher, data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(cipherText); } public static String decrypt(final String secret, final String encryptedString) { - final Cipher cipher = createCipherFromSecret(secret); + final Cipher cipher = createCipherFromSecret(secret, CipherMode.DECRYPT); final byte[] cipherText = createCipherText(cipher, Base64.getDecoder().decode(encryptedString)); return new String(cipherText, StandardCharsets.UTF_8); } - private static Cipher createCipherFromSecret(final String secret) { + private static Cipher createCipherFromSecret(final String secret, final CipherMode mode) { try { final byte[] decodedKey = Base64.getDecoder().decode(secret); final Cipher cipher = Cipher.getInstance("AES"); final SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); - cipher.init(Cipher.DECRYPT_MODE, originalKey); + cipher.init(mode.opmode, originalKey); return cipher; } catch (final Exception e) { - throw new RuntimeException("Error creating cipher from secret"); + throw new RuntimeException("Error creating cipher from secret in mode " + mode.name()); } } @@ -52,4 +52,13 @@ private static byte[] createCipherText(final Cipher cipher, final byte[] data) { throw new RuntimeException("The cipher was unable to perform pass over data"); } } + + private enum CipherMode { + ENCRYPT(Cipher.ENCRYPT_MODE), + DECRYPT(Cipher.DECRYPT_MODE); + private final int opmode; + private CipherMode(final int opmode) { + this.opmode = opmode; + } + } } From 71dd17f519f604cd93bbf9c7c00cda5e7bd4a2fb Mon Sep 17 00:00:00 2001 From: Peter Nied Date: Mon, 12 Jun 2023 16:40:37 +0000 Subject: [PATCH 19/74] Add basic parameters and update test to use them Signed-off-by: Peter Nied --- .../onbehalf/CreateOnBehalfOfToken.java | 26 ++++++++++++++----- .../dlic/rest/api/AccountApiTest.java | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index aa1557cd7e..219594ad98 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -113,14 +113,28 @@ private RestChannelConsumer handlePost(RestRequest request, NodeClient client) t public void accept(RestChannel channel) throws Exception { final XContentBuilder builder = channel.newBuilder(); BytesRestResponse response; - try { - builder.startObject(); + final Map requestBody = request.contentOrSourceParamParser().map(); + final String reason = (String)requestBody.getOrDefault("reason", null); + + final Integer tokenDuration = Optional.ofNullable(requestBody.get("duration")) + .map(value -> (String)value) + .map(Integer::parseInt) + .map(value -> Math.min(value, 72)) // Max duration is 72 hours + .orElse(24); // Fallback to default; + + final String source = "self-issued"; final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - builder.field("user_name", "nothing"); - builder.field("user", user.toString()); - final String token = vendor.createJwt("me", user.getName(), "self-issued", 60, List.of("1", "2", "3")); - builder.field("field_token", token); + + builder.startObject(); + builder.field("user", user.getName()); + final String token = vendor.createJwt(/* TODO: Update the issuer to represent the cluster */"OpenSearch", + user.getName(), + source, + tokenDuration, + user.getSecurityRoles().stream().collect(Collectors.toList())); + builder.field("onBehalfOfToken", token); + builder.field("duration", tokenDuration); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index e381c73d08..dba9e7a63d 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -75,7 +75,7 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsSettings("tenants")); assertNotNull(body.getAsList("roles")); - response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "", encodeBasicHeader(testUser, testPass)); + response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "{\"reason\":\"Test generation\"}", encodeBasicHeader(testUser, testPass)); System.out.println(response.getBody()); } From 8c0d750877aae14079226285470b100d61eb5ebc Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 13 Jun 2023 14:54:23 -0700 Subject: [PATCH 20/74] Change ConfigModelV7's imports Signed-off-by: Ryan Liang --- .../security/securityconf/DynamicConfigModelV7.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index d366f86bed..b84ddf9a40 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -55,7 +55,7 @@ import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; -import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.ConfigV7.Authc; import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthcDomain; @@ -316,8 +316,7 @@ private void buildAAA() { Settings oboSettings = getDynamicOnBehalfOfSettings(); if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { - final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new HTTPOnBehalfOfJwtAuthenticator(getDynamicOnBehalfOfSettings()), false, -1); - restAuthDomains0.add(_ad); + final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings()), false, -1); restAuthDomains0.add(_ad); } List originalDestroyableComponents = destroyableComponents; From b36bde0e33980c0d1f820c290adc6d44480a7b34 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 13 Jun 2023 15:20:59 -0700 Subject: [PATCH 21/74] Re-apply zstd dependency fix Signed-off-by: Ryan Liang --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 789184d77e..15eaad0803 100644 --- a/build.gradle +++ b/build.gradle @@ -336,6 +336,7 @@ configurations { force "io.netty:netty-transport:${versions.netty}" force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "org.apache.bcel:bcel:6.6.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 + force "com.github.luben:zstd-jni:${versions.zstd}" } } @@ -458,7 +459,7 @@ dependencies { runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.4.0' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.2.5' runtimeOnly 'org.apache.santuario:xmlsec:2.2.3' - runtimeOnly 'com.github.luben:zstd-jni:1.5.5-3' + runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" runtimeOnly 'org.checkerframework:checker-qual:3.5.0' runtimeOnly "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" From a6bc8c00e45660b7af3a802c592505858c4c9253 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 13 Jun 2023 15:32:54 -0700 Subject: [PATCH 22/74] Update OSSecurityPlugin class Signed-off-by: Ryan Liang --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 261ef98374..a756ce0b5d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -840,9 +840,6 @@ public Collection createComponents(Client localClient, ClusterService cl securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); - - final OnBehalfOfAuthenticator onBehalfOfAuthenticator = new OnBehalfOfAuthenticator(); - final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); @@ -850,7 +847,6 @@ public Collection createComponents(Client localClient, ClusterService cl dcf.registerDCFListener(xffResolver); dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); - dcf.registerDCFListener(onBehalfOfAuthenticator); if (!(auditLog instanceof NullAuditLog)) { // Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog dcf.registerDCFListener(auditLog); From 4351752919170e4c6785c2feb784297faee2832c Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 14 Jun 2023 11:32:06 -0700 Subject: [PATCH 23/74] Fix the optional longsupplier for current time Signed-off-by: Ryan Liang --- ...BehalfOfJwtAuthorizationHeaderFactory.java | 3 +- .../security/authtoken/jwt/JwtVendorTest.java | 48 +++++++++---------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java index 108806cd24..57897477ac 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -10,6 +10,7 @@ package org.opensearch.security.http; import java.util.List; +import java.util.Optional; import java.util.function.LongSupplier; import org.apache.hc.core5.http.Header; @@ -44,7 +45,7 @@ public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer } Header generateValidToken() throws Exception { - LongSupplier currentTime = () -> (System.currentTimeMillis() / 1000); + Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); Settings settings = Settings.builder().put("signing_key", signing_key).put("encryption_key", encryption_key).build(); JwtVendor jwtVendor = new JwtVendor(settings, currentTime); String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); 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 4247307614..a253f11655 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -121,28 +121,28 @@ public void testCreateJwtWithBadRoles() throws Exception { } //For Manual Testing - @Test - public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { - String issuer = "cluster_0"; - String subject = "craig"; - String audience = "audience_0"; - List roles = List.of("admin", "HR"); - Integer expirySeconds = 10000; - LongSupplier currentTime = () -> (System.currentTimeMillis() / 1000); - String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); - String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); - System.out.println("The encryptionkey is:" + encryptionKey); - Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); - - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); - String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); - - JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); - JwtToken jwt = jwtConsumer.getJwtToken(); - - System.out.println("JWT: " + encodedJwt); - - assertTrue(true); - } +// @Test +// public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { +// String issuer = "cluster_0"; +// String subject = "craig"; +// String audience = "audience_0"; +// List roles = List.of("admin", "HR"); +// Integer expirySeconds = 10000; +// Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); +// String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); +// String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); +// String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); +// System.out.println("The encryptionkey is:" + encryptionKey); +// Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); +// +// JwtVendor jwtVendor = new JwtVendor(settings, currentTime); +// String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); +// +// JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); +// JwtToken jwt = jwtConsumer.getJwtToken(); +// +// System.out.println("JWT: " + encodedJwt); +// +// assertTrue(true); +// } } From 2a129af046de6c3a9c71d928d06368aa19f4b3a0 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 14 Jun 2023 15:22:56 -0700 Subject: [PATCH 24/74] Set up the subscriber to grab signingkey and encryptionkey from config, todo: dcf listener Signed-off-by: Ryan Liang --- .../onbehalf/CreateOnBehalfOfToken.java | 25 ++++++--- .../security/authtoken/jwt/JwtVendor.java | 6 ++- .../http/OnBehalfOfAuthenticator.java | 1 + .../security/authtoken/jwt/JwtVendorTest.java | 51 ++++++++++--------- .../dlic/rest/api/AccountApiTest.java | 2 +- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index 219594ad98..2f313a4963 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -7,6 +7,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.greenrobot.eventbus.Subscribe; import org.opensearch.action.FailedNodeException; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.nodes.TransportNodesAction; @@ -20,6 +21,7 @@ import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportRequest; @@ -33,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.LongSupplier; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; @@ -71,15 +74,23 @@ public class CreateOnBehalfOfToken extends BaseRestHandler { - private final JwtVendor vendor; + private JwtVendor vendor; private final ThreadPool threadPool; + private DynamicConfigModel dcm; + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); + //TODO: NULL CHECK\ + } public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { - Settings testSettings = Settings.builder() - .put("signing_key", "1234567890123456") - .put("encryption_key", "1234567890123456").build(); +// Settings testSettings = Settings.builder() +// .put("signing_key", "VGhpcyBpcyB0aGUgc2lnbmluZyBrZXkgZm9yIHRlc3Rpbmc===") +// .put("encryption_key", "ZW5jcnlwdGlvbktleQ==").build(); - this.vendor = new JwtVendor(testSettings, Optional.empty()); + //this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); this.threadPool = threadPool; } @@ -120,8 +131,8 @@ public void accept(RestChannel channel) throws Exception { final Integer tokenDuration = Optional.ofNullable(requestBody.get("duration")) .map(value -> (String)value) .map(Integer::parseInt) - .map(value -> Math.min(value, 72)) // Max duration is 72 hours - .orElse(24); // Fallback to default; + .map(value -> Math.min(value, 72 * 3600)) // Max duration is 72 hours + .orElse(24 * 3600); // Fallback to default; final String source = "self-issued"; final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); 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 1587dcab8c..cebcc8236c 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -66,7 +66,11 @@ public JwtVendor(final Settings settings, final Optional timeProvi } else { this.claimsEncryptionKey = settings.get("encryption_key"); } - this.timeProvider = timeProvider.orElseGet(() -> System::currentTimeMillis); + if (timeProvider.isPresent()) { + this.timeProvider = timeProvider.get(); + } else { + this.timeProvider = () -> System.currentTimeMillis() / 1000; + } this.configModel = null; } diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index eb5fa434df..723b52dd66 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -174,6 +174,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { String decryptedRoles = rolesClaim; if (rolesObject == claims.get("er")) { decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); + System.out.println("This is the decrypted roles: " + decryptedRoles); } roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); } 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 a253f11655..7e83c42010 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -121,28 +121,31 @@ public void testCreateJwtWithBadRoles() throws Exception { } //For Manual Testing -// @Test -// public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { -// String issuer = "cluster_0"; -// String subject = "craig"; -// String audience = "audience_0"; -// List roles = List.of("admin", "HR"); -// Integer expirySeconds = 10000; -// Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); -// String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); -// String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); -// String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); -// System.out.println("The encryptionkey is:" + encryptionKey); -// Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); -// -// JwtVendor jwtVendor = new JwtVendor(settings, currentTime); -// String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); -// -// JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); -// JwtToken jwt = jwtConsumer.getJwtToken(); -// -// System.out.println("JWT: " + encodedJwt); -// -// assertTrue(true); -// } + @Test + public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { + String issuer = "cluster_0"; + String subject = "Ryan"; + String audience = "audience_0"; + List roles = List.of("admin", "HR"); + Integer expirySeconds = 100000; + Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); + + String signingKey = Base64.getEncoder().encodeToString("This is the signing key for testing".getBytes(StandardCharsets.UTF_8)); + System.out.println("The signingKey is: " + signingKey); + + String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + System.out.println("The encryptionkey is: " + encryptionKey); + + Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); + + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); + JwtToken jwt = jwtConsumer.getJwtToken(); + + System.out.println("Encoded OBO JWT is: " + encodedJwt); + + assertTrue(true); + } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index dba9e7a63d..d8b53fddab 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -76,7 +76,7 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsList("roles")); response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "{\"reason\":\"Test generation\"}", encodeBasicHeader(testUser, testPass)); - System.out.println(response.getBody()); + System.out.println("This is the response body: " + response.getBody()); } @Test From 61eaee026d2c7437883889b52b7b6f168c2d0294 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 14 Jun 2023 15:49:38 -0700 Subject: [PATCH 25/74] Set up manual testing with stronger signing key Signed-off-by: Ryan Liang --- .../security/action/onbehalf/CreateOnBehalfOfToken.java | 8 ++++---- .../opensearch/security/authtoken/jwt/JwtVendorTest.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index 2f313a4963..51c661d9ee 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -86,11 +86,11 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { } public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { -// Settings testSettings = Settings.builder() -// .put("signing_key", "VGhpcyBpcyB0aGUgc2lnbmluZyBrZXkgZm9yIHRlc3Rpbmc===") -// .put("encryption_key", "ZW5jcnlwdGlvbktleQ==").build(); + Settings testSettings = Settings.builder() + .put("signing_key", "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z") + .put("encryption_key", "ZW5jcnlwdGlvbktleQ==").build(); - //this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); + this.vendor = new JwtVendor(testSettings, Optional.empty()); this.threadPool = threadPool; } 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 7e83c42010..5b193d5e18 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -130,7 +130,7 @@ public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { Integer expirySeconds = 100000; Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); - String signingKey = Base64.getEncoder().encodeToString("This is the signing key for testing".getBytes(StandardCharsets.UTF_8)); + String signingKey = Base64.getEncoder().encodeToString("This is the jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); System.out.println("The signingKey is: " + signingKey); String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); From 640bc33e4ea7b96e734c47af627bc64df3aa3818 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 09:36:39 -0700 Subject: [PATCH 26/74] Modify JwtVendor to grab mapped roles Signed-off-by: Ryan Liang --- ...BehalfOfJwtAuthorizationHeaderFactory.java | 7 +++-- .../security/OpenSearchSecurityPlugin.java | 12 ++++++- .../onbehalf/CreateOnBehalfOfToken.java | 21 ++++++------- .../security/authtoken/jwt/JwtVendor.java | 31 +++++++++++++------ .../http/OnBehalfOfAuthenticator.java | 3 ++ .../security/authtoken/jwt/JwtVendorTest.java | 11 ++++--- .../dlic/rest/api/AccountApiTest.java | 4 +++ 7 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java index 57897477ac..82092f46fa 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -47,10 +47,11 @@ public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer Header generateValidToken() throws Exception { Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); Settings settings = Settings.builder().put("signing_key", signing_key).put("encryption_key", encryption_key).build(); - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); - String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); +// JwtVendor jwtVendor = new JwtVendor(settings, currentTime); +// String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); - return toHeader(encodedJwt); +// return toHeader(encodedJwt); + return null; } private BasicHeader toHeader(String token) { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index a756ce0b5d..aedb270250 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -45,6 +45,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; @@ -127,6 +128,7 @@ import org.opensearch.security.auditlog.config.AuditConfig.Filter.FilterEntries; import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; import org.opensearch.security.configuration.AdminDNs; @@ -206,6 +208,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SecurityRestFilter securityRestHandler; private volatile SecurityInterceptor si; private volatile PrivilegesEvaluator evaluator; + private volatile JwtVendor vendor; private volatile UserService userService; private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; @@ -478,7 +481,7 @@ public List getRestHandlers(Settings settings, RestController restC Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - handlers.add(new CreateOnBehalfOfToken(settings, threadPool)); + handlers.add(new CreateOnBehalfOfToken(settings, threadPool, vendor)); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -828,6 +831,13 @@ public Collection createComponents(Client localClient, ClusterService cl evaluator = new PrivilegesEvaluator(clusterService, threadPool, cr, resolver, auditLog, settings, privilegesInterceptor, cih, irr, dlsFlsEnabled, namedXContentRegistry.get()); + Settings testSettings = Settings.builder() + .put("signing_key", "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z") + .put("encryption_key", "ZW5jcnlwdGlvbktleQ==").build(); + + vendor = new JwtVendor(testSettings, Optional.empty(), threadPool, evaluator); + + sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver); final String principalExtractorClass = settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PRINCIPAL_EXTRACTOR_CLASS, null); diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index 51c661d9ee..8e84cfad7a 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -78,19 +78,16 @@ public class CreateOnBehalfOfToken extends BaseRestHandler { private final ThreadPool threadPool; private DynamicConfigModel dcm; - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - this.dcm = dcm; - this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); - //TODO: NULL CHECK\ - } +// @Subscribe +// public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { +// this.dcm = dcm; +// this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); +// //TODO: NULL CHECK\ +// } - public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { - Settings testSettings = Settings.builder() - .put("signing_key", "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z") - .put("encryption_key", "ZW5jcnlwdGlvbktleQ==").build(); + public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool, final JwtVendor vendor) { - this.vendor = new JwtVendor(testSettings, Optional.empty()); + this.vendor = vendor; this.threadPool = threadPool; } @@ -139,6 +136,8 @@ public void accept(RestChannel channel) throws Exception { builder.startObject(); builder.field("user", user.getName()); + System.out.println("Ljl19970123"); + System.out.println(user.getRoles().stream().collect(Collectors.toList())); final String token = vendor.createJwt(/* TODO: Update the issuer to represent the cluster */"OpenSearch", user.getName(), source, 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 cebcc8236c..d2f49327b7 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -20,6 +20,7 @@ import java.util.function.LongSupplier; import com.google.common.base.Strings; +import org.apache.commons.lang3.tuple.Pair; import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; import org.apache.cxf.rs.security.jose.jwk.KeyType; @@ -35,6 +36,8 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.transport.TransportAddress; +import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -49,11 +52,13 @@ public class JwtVendor { private final JsonWebKey signingKey; private final JoseJwtProducer jwtProducer; private final LongSupplier timeProvider; + private final ThreadPool threadPool; + private final PrivilegesEvaluator privilegesEvaluator; //TODO: Relocate/Remove them at once we make the descisions about the `roles` private ConfigModel configModel; // This never gets assigned, how does this work at all? - public JwtVendor(final Settings settings, final Optional timeProvider) { + public JwtVendor(final Settings settings, final Optional timeProvider, ThreadPool threadPool, PrivilegesEvaluator privilegesEvaluator) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { this.signingKey = createJwkFromSettings(settings); @@ -72,6 +77,8 @@ public JwtVendor(final Settings settings, final Optional timeProvi this.timeProvider = () -> System.currentTimeMillis() / 1000; } this.configModel = null; + this.threadPool = threadPool; + this.privilegesEvaluator = privilegesEvaluator; } /* @@ -123,8 +130,6 @@ public String createJwt(String issuer, String subject, String audience, Integer jwtClaims.setIssuedAt(timeMillis); - jwtClaims.setSubject(subject); - jwtClaims.setAudience(audience); jwtClaims.setNotBefore(timeMillis); @@ -140,12 +145,20 @@ public String createJwt(String issuer, String subject, String audience, Integer } //TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr` - if (roles != null) { - String listOfRoles = String.join(",", roles); - jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); - } else { - throw new Exception("Roles cannot be null"); - } + + final Pair userAndRemoteAddress = + Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); + final User user = userAndRemoteAddress.getLeft(); + final TransportAddress remoteAddress = userAndRemoteAddress.getRight(); + + jwtClaims.setSubject(user.getName()); + + // map the users Security roles + Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); + // Ensure security role is populated + String listOfRoles = String.join(",", userRoles); + jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); + String encodedJwt = jwtProducer.processJwt(jwt); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 723b52dd66..e5240f609a 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -101,6 +101,7 @@ private JwtParser initParser(final String signingKey) { @SuppressWarnings("removal") public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { final SecurityManager sm = System.getSecurityManager(); + log.info("Trying to extractCredentials"); if (sm != null) { sm.checkPermission(new SpecialPermission()); @@ -117,6 +118,8 @@ public AuthCredentials run() { } private AuthCredentials extractCredentials0(final RestRequest request) { + log.info("Trying to extractCredentials 0"); + if (jwtParser == null) { log.error("Missing Signing Key. JWT authentication will not work"); return null; 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 5b193d5e18..fa0ec5d7c7 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -59,7 +59,7 @@ public void testCreateJwtWithRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); Long expectedExp = currentTime.getAsLong() + (expirySeconds * 1000); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime), null, null); String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); @@ -85,7 +85,7 @@ public void testCreateJwtWithBadExpiry() throws Exception { String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty(), null, null); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); } @@ -99,7 +99,7 @@ public void testCreateJwtWithBadEncryptionKey() throws Exception { Integer expirySeconds = 300; Settings settings = Settings.builder().put("signing_key", "abc123").build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty(),null, null); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); } @@ -115,7 +115,7 @@ public void testCreateJwtWithBadRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty(),null, null); jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles); } @@ -138,7 +138,7 @@ public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + JwtVendor jwtVendor = new JwtVendor(settings, currentTime,null, null); String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); @@ -146,6 +146,7 @@ public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { System.out.println("Encoded OBO JWT is: " + encodedJwt); + System.out.println("Decrypted role claim is: " + EncryptionDecryptionUtil.decrypt(encryptionKey, "T/XlNJfL852emYUv9/y06w==")); assertTrue(true); } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index d8b53fddab..a9988071b7 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -77,6 +77,10 @@ public void testGetAccount() throws Exception { response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "{\"reason\":\"Test generation\"}", encodeBasicHeader(testUser, testPass)); System.out.println("This is the response body: " + response.getBody()); + final String token = response.findValueInJson("onBehalfOfToken"); + final Header header = new org.apache.hc.core5.http.message.BasicHeader("Authorization", "Bearer " + token); + response = rh.executeGetRequest("/", header); + System.out.println("This is the response body: " + response.getBody()); } @Test From 9148a982899838b110a896a52ddc6dae9f34bf79 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 12:06:24 -0700 Subject: [PATCH 27/74] Add backend roles + dcf listener for obo config + Fix the role grabbing issue in obo endpoint Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 4 +- ...BehalfOfJwtAuthorizationHeaderFactory.java | 11 +- .../security/OpenSearchSecurityPlugin.java | 19 +- .../onbehalf/CreateOnBehalfOfToken.java | 79 ++++---- .../internal/NoOpAuthenticationBackend.java | 4 +- .../security/authtoken/jwt/JwtVendor.java | 46 ++--- .../http/OnBehalfOfAuthenticator.java | 89 ++++++--- .../security/user/AuthCredentials.java | 26 +++ .../security/authtoken/jwt/JwtVendorTest.java | 24 +-- .../dlic/rest/api/AccountApiTest.java | 32 ++-- .../http/OnBehalfOfAuthenticatorTest.java | 174 ++++++++++-------- 11 files changed, 289 insertions(+), 219 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 82cb286043..01bea15075 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -36,6 +36,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final Integer expirySeconds = 100000; public static final String headerName = "Authorization"; public static final List roles = List.of("admin", "HR"); + public static final List backendRoles = List.of("IT"); private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); @@ -46,6 +47,7 @@ public class OnBehalfOfJwtAuthenticationTest { subject, audience, roles, + backendRoles, expirySeconds, headerName, encryptionKey @@ -61,7 +63,7 @@ public class OnBehalfOfJwtAuthenticationTest { @Test public void shouldAuthenticateWithJwtToken_positive() { - // TODO: This integration test should use an endpoint to get an OnBehalfOf token, not generate it + // TODO: This integration test should use an endpoint to get an OnBehalfOf token, not generate it try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ TestRestClient.HttpResponse response = client.getAuthInfo(); diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java index 82092f46fa..966594ec4d 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java @@ -27,18 +27,20 @@ class OnBehalfOfJwtAuthorizationHeaderFactory { private final String subject; private final String audience; private final List roles; + private final List backendRoles; private final String encryption_key; private final String signing_key; private final String headerName; private final Integer expirySeconds; - public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer, String subject, String audience, List roles ,Integer expirySeconds, String headerName, String encryption_key) { + public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer, String subject, String audience, List roles, List backendRoles, Integer expirySeconds, String headerName, String encryption_key) { this.signing_key = requireNonNull(signing_key, "Signing key is required"); this.issuer = requireNonNull(issuer, "Issuer is required"); this.subject = requireNonNull(subject, "Subject is required"); this.audience = requireNonNull(audience, "Audience is required."); this.roles = requireNonNull(roles, "Roles claim is required"); + this.backendRoles = requireNonNull(backendRoles, "Backend roles claim is required"); this.expirySeconds = requireNonNull(expirySeconds, "Expiry is required"); this.headerName = requireNonNull(headerName, "Header name is required"); this.encryption_key = encryption_key; @@ -47,11 +49,10 @@ public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer Header generateValidToken() throws Exception { Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); Settings settings = Settings.builder().put("signing_key", signing_key).put("encryption_key", encryption_key).build(); -// JwtVendor jwtVendor = new JwtVendor(settings, currentTime); -// String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles); -// return toHeader(encodedJwt); - return null; + return toHeader(encodedJwt); } private BasicHeader toHeader(String token) { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index aedb270250..278112718b 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -45,7 +45,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; @@ -128,7 +127,6 @@ import org.opensearch.security.auditlog.config.AuditConfig.Filter.FilterEntries; import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; import org.opensearch.security.configuration.AdminDNs; @@ -208,7 +206,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SecurityRestFilter securityRestHandler; private volatile SecurityInterceptor si; private volatile PrivilegesEvaluator evaluator; - private volatile JwtVendor vendor; private volatile UserService userService; private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; @@ -219,6 +216,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SslExceptionHandler sslExceptionHandler; private volatile Client localClient; private final boolean disabled; + private volatile DynamicConfigFactory dcf; private final List demoCertHashes = new ArrayList(3); private volatile SecurityFilter sf; private volatile IndexResolverReplacer irr; @@ -481,7 +479,9 @@ public List getRestHandlers(Settings settings, RestController restC Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - handlers.add(new CreateOnBehalfOfToken(settings, threadPool, vendor)); + CreateOnBehalfOfToken cobot = new CreateOnBehalfOfToken(settings, threadPool); + dcf.registerDCFListener(cobot); + handlers.add(cobot); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -831,13 +831,6 @@ public Collection createComponents(Client localClient, ClusterService cl evaluator = new PrivilegesEvaluator(clusterService, threadPool, cr, resolver, auditLog, settings, privilegesInterceptor, cih, irr, dlsFlsEnabled, namedXContentRegistry.get()); - Settings testSettings = Settings.builder() - .put("signing_key", "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z") - .put("encryption_key", "ZW5jcnlwdGlvbktleQ==").build(); - - vendor = new JwtVendor(testSettings, Optional.empty(), threadPool, evaluator); - - sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver); final String principalExtractorClass = settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PRINCIPAL_EXTRACTOR_CLASS, null); @@ -850,7 +843,7 @@ public Collection createComponents(Client localClient, ClusterService cl securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); - final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); + dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); dcf.registerDCFListener(irr); @@ -1211,7 +1204,7 @@ public static class GuiceHolder implements LifecycleComponent { @Inject public GuiceHolder(final RepositoriesService repositoriesService, - final TransportService remoteClusterService, IndicesService indicesService, PitService pitService, ExtensionsManager extensionsManager) { + final TransportService remoteClusterService, IndicesService indicesService, PitService pitService, ExtensionsManager extensionsManager) { GuiceHolder.repositoriesService = repositoriesService; GuiceHolder.remoteClusterService = remoteClusterService.getRemoteClusterService(); GuiceHolder.indicesService = indicesService; diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index 8e84cfad7a..a7106845aa 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -17,9 +17,11 @@ import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.io.stream.StreamOutput; import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.TransportAddress; import org.opensearch.security.auth.BackendRegistry; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.impl.CType; @@ -35,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.LongSupplier; import java.util.stream.Collectors; @@ -76,34 +79,39 @@ public class CreateOnBehalfOfToken extends BaseRestHandler { private JwtVendor vendor; private final ThreadPool threadPool; + + private ConfigModel configModel; + private DynamicConfigModel dcm; -// @Subscribe -// public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { -// this.dcm = dcm; -// this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); -// //TODO: NULL CHECK\ -// } + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } - public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool, final JwtVendor vendor) { + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); + } - this.vendor = vendor; + public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { this.threadPool = threadPool; } - @Override - public String getName() { - return getClass().getSimpleName(); - } - - @Override - public List routes() { - return addRoutesPrefix( - ImmutableList.of( - new Route(Method.POST, "/user/onbehalfof") - ) + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public List routes() { + return addRoutesPrefix( + ImmutableList.of( + new Route(Method.POST, "/user/onbehalfof") + ) ); - } + } @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { @@ -126,34 +134,35 @@ public void accept(RestChannel channel) throws Exception { final String reason = (String)requestBody.getOrDefault("reason", null); final Integer tokenDuration = Optional.ofNullable(requestBody.get("duration")) - .map(value -> (String)value) - .map(Integer::parseInt) - .map(value -> Math.min(value, 72 * 3600)) // Max duration is 72 hours - .orElse(24 * 3600); // Fallback to default; + .map(value -> (String)value) + .map(Integer::parseInt) + .map(value -> Math.min(value, 72 * 3600)) // Max duration is 72 hours + .orElse(24 * 3600); // Fallback to default; final String source = "self-issued"; final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - + final TransportAddress caller = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + Set mappedRoles = mapRoles(user, caller); + builder.startObject(); builder.field("user", user.getName()); - System.out.println("Ljl19970123"); - System.out.println(user.getRoles().stream().collect(Collectors.toList())); final String token = vendor.createJwt(/* TODO: Update the issuer to represent the cluster */"OpenSearch", - user.getName(), - source, - tokenDuration, - user.getSecurityRoles().stream().collect(Collectors.toList())); + user.getName(), + source, + tokenDuration, + mappedRoles.stream().collect(Collectors.toList()), + user.getRoles().stream().collect(Collectors.toList())); builder.field("onBehalfOfToken", token); builder.field("duration", tokenDuration); builder.endObject(); - + response = new BytesRestResponse(RestStatus.OK, builder); } catch (final Exception exception) { System.out.println(exception.toString()); builder.startObject() .field("error", exception.toString()) .endObject(); - + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); } builder.close(); @@ -162,4 +171,8 @@ public void accept(RestChannel channel) throws Exception { }; } + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } + } diff --git a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java index 1f149aabcf..299a1a4577 100644 --- a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java @@ -46,7 +46,9 @@ public String getType() { @Override public User authenticate(final AuthCredentials credentials) { - return new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + User user = new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + user.addSecurityRoles(credentials.getSecurityRoles()); + return user; } @Override 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 d2f49327b7..4b18c6e3ca 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -20,7 +20,6 @@ import java.util.function.LongSupplier; import com.google.common.base.Strings; -import org.apache.commons.lang3.tuple.Pair; import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; import org.apache.cxf.rs.security.jose.jwk.KeyType; @@ -36,8 +35,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.transport.TransportAddress; -import org.opensearch.security.dlic.rest.support.Utils; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -52,13 +49,11 @@ public class JwtVendor { private final JsonWebKey signingKey; private final JoseJwtProducer jwtProducer; private final LongSupplier timeProvider; - private final ThreadPool threadPool; - private final PrivilegesEvaluator privilegesEvaluator; //TODO: Relocate/Remove them at once we make the descisions about the `roles` private ConfigModel configModel; // This never gets assigned, how does this work at all? - public JwtVendor(final Settings settings, final Optional timeProvider, ThreadPool threadPool, PrivilegesEvaluator privilegesEvaluator) { + public JwtVendor(final Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { this.signingKey = createJwkFromSettings(settings); @@ -77,16 +72,14 @@ public JwtVendor(final Settings settings, final Optional timeProvi this.timeProvider = () -> System.currentTimeMillis() / 1000; } this.configModel = null; - this.threadPool = threadPool; - this.privilegesEvaluator = privilegesEvaluator; } /* - * The default configuration of this web key should be: - * KeyType: OCTET - * PublicKeyUse: SIGN - * Encryption Algorithm: HS512 - * */ + * The default configuration of this web key should be: + * KeyType: OCTET + * PublicKeyUse: SIGN + * Encryption Algorithm: HS512 + * */ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { String signingKey = settings.get("signing_key"); @@ -118,7 +111,7 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { } } - public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles) throws Exception { + public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles, List backendRoles) throws Exception { long timeMillis = timeProvider.getAsLong(); Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); @@ -130,6 +123,8 @@ public String createJwt(String issuer, String subject, String audience, Integer jwtClaims.setIssuedAt(timeMillis); + jwtClaims.setSubject(subject); + jwtClaims.setAudience(audience); jwtClaims.setNotBefore(timeMillis); @@ -145,20 +140,17 @@ public String createJwt(String issuer, String subject, String audience, Integer } //TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr` + if (roles != null) { + String listOfRoles = String.join(",", roles); + jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); + } else { + throw new Exception("Roles cannot be null"); + } - final Pair userAndRemoteAddress = - Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); - final User user = userAndRemoteAddress.getLeft(); - final TransportAddress remoteAddress = userAndRemoteAddress.getRight(); - - jwtClaims.setSubject(user.getName()); - - // map the users Security roles - Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); - // Ensure security role is populated - String listOfRoles = String.join(",", userRoles); - jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); - + if (backendRoles != null) { + String listOfBackendRoles = String.join(",", backendRoles); + jwtClaims.setProperty("ebr", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfBackendRoles)); + } String encodedJwt = jwtProducer.processJwt(jwt); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index e5240f609a..2bdb78088f 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -20,9 +20,11 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.Map.Entry; import java.util.regex.Pattern; +import java.util.stream.Collectors; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; @@ -67,8 +69,8 @@ private JwtParser initParser(final String signingKey) { try { final String minmalKeyFormat = signingKey - .replace("-----BEGIN PUBLIC KEY-----\n", "") - .replace("-----END PUBLIC KEY-----", ""); + .replace("-----BEGIN PUBLIC KEY-----\n", "") + .replace("-----END PUBLIC KEY-----", ""); final byte[] decoded = Decoders.BASE64.decode(minmalKeyFormat); Key key = null; @@ -90,18 +92,64 @@ private JwtParser initParser(final String signingKey) { } // Fallback to the decoded signing key // TODO: Should we ever do this, I think no?? - return Jwts.parser().setSigningKey(decoded); + return Jwts.parser().setSigningKey(signingKey); } catch (Throwable e) { log.error("Error while creating JWT authenticator", e); throw new RuntimeException(e); } } + private List extractSecurityRolesFromClaims(Claims claims) { + Object rolesObject = ObjectUtils.firstNonNull(claims.get("er"), claims.get("dr")); + List roles; + + if (rolesObject == null) { + log.warn( + "Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload."); + roles = List.of(); + } else { + final String rolesClaim = rolesObject.toString(); + + // Extracting roles based on the compatbility mode + String decryptedRoles = rolesClaim; + if (rolesObject == claims.get("er")) { + //TODO: WHERE TO GET THE ENCRYTION KEY + decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); + } + roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).collect(Collectors.toList()); + } + + return roles; + } + + private String[] extractBackendRolesFromClaims(Claims claims) { + //TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr + Object backendRolesObject = ObjectUtils.firstNonNull(claims.get("ebr"), claims.get("dbr")); + String[] backendRoles; + + if (backendRolesObject == null) { + log.warn( + "Failed to get backend roles from JWT claims. Check if this key is correct and available in the JWT payload."); + backendRoles = new String[0]; + } else { + final String backendRolesClaim = backendRolesObject.toString(); + + // Extracting roles based on the compatibility mode + String decryptedBackendRoles = backendRolesClaim; + if (backendRolesObject == claims.get("ebr")) { + //TODO: WHERE TO GET THE ENCRYTION KEY + decryptedBackendRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, backendRolesClaim); + } + backendRoles = Arrays.stream(decryptedBackendRoles.split(",")).map(String::trim).toArray(String[]::new); + } + + return backendRoles; + } + @Override @SuppressWarnings("removal") public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException { final SecurityManager sm = System.getSecurityManager(); - log.info("Trying to extractCredentials"); if (sm != null) { sm.checkPermission(new SpecialPermission()); @@ -118,8 +166,6 @@ public AuthCredentials run() { } private AuthCredentials extractCredentials0(final RestRequest request) { - log.info("Trying to extractCredentials 0"); - if (jwtParser == null) { log.error("Missing Signing Key. JWT authentication will not work"); return null; @@ -147,6 +193,10 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } } + if (jwtToken == null) { + return null; + } + try { final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); @@ -157,32 +207,15 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } final String audience = claims.getAudience(); - if (Objects.isNull(subject)) { + if (Objects.isNull(audience)) { log.error("Valid jwt on behalf of token with no audience"); return null; } - String[] roles; - - Object rolesObject = ObjectUtils.firstNonNull(claims.get("er"), claims.get("dr")); - - if (rolesObject == null) { - log.warn( - "Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload."); - roles = new String[0]; - } else { - final String rolesClaim = rolesObject.toString(); - - // Extracting roles based on the compatbility mode - String decryptedRoles = rolesClaim; - if (rolesObject == claims.get("er")) { - decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); - System.out.println("This is the decrypted roles: " + decryptedRoles); - } - roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); - } + List roles = extractSecurityRolesFromClaims(claims); + String[] backendRoles = extractBackendRolesFromClaims(claims); - final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); + final AuthCredentials ac = new AuthCredentials(subject, roles, backendRoles).markComplete(); for(Entry claim: claims.entrySet()) { ac.addAttribute("attr.jwt."+claim.getKey(), String.valueOf(claim.getValue())); @@ -191,11 +224,9 @@ private AuthCredentials extractCredentials0(final RestRequest request) { return ac; } catch (WeakKeyException e) { - System.out.println("Error MSG1!" + e.getMessage()); log.error("Cannot authenticate user with JWT because of ", e); return null; } catch (Exception e) { - System.out.println("Error MSG2!" + e.getMessage()); e.printStackTrace(); if(log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); diff --git a/src/main/java/org/opensearch/security/user/AuthCredentials.java b/src/main/java/org/opensearch/security/user/AuthCredentials.java index e0dfa37dab..079a57f0b0 100644 --- a/src/main/java/org/opensearch/security/user/AuthCredentials.java +++ b/src/main/java/org/opensearch/security/user/AuthCredentials.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -48,6 +49,7 @@ public final class AuthCredentials { private final String username; private byte[] password; private Object nativeCredentials; + private final Set securityRoles = new HashSet(); private final Set backendRoles = new HashSet(); private boolean complete; private final byte[] internalPasswordHash; @@ -94,6 +96,22 @@ public AuthCredentials(final String username, String... backendRoles) { this(username, null, null, backendRoles); } + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + * @param username The username, must not be null or empty + * @param securityRoles The internal roles the user has been mapped to + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, List securityRoles, String... backendRoles) { + this(username, null, null, backendRoles); + this.securityRoles.addAll(securityRoles); + } + + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, List securityRoles, String... backendRoles) { + this(username, null, null, backendRoles); + } + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { super(); @@ -200,6 +218,14 @@ public Set getBackendRoles() { return new HashSet(backendRoles); } + /** + * + * @return Defensive copy of the security roles this user is member of. + */ + public Set getSecurityRoles() { + return new HashSet(securityRoles); + } + public boolean isComplete() { return complete; } 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 fa0ec5d7c7..ae87f3f2a0 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -52,15 +52,17 @@ public void testCreateJwtWithRoles() throws Exception { String subject = "admin"; String audience = "audience_0"; List roles = List.of("IT", "HR"); + List backendRoles = List.of("Sales"); String expectedRoles = "IT,HR"; + String expectedBackendRoles = "Sales"; Integer expirySeconds = 300; LongSupplier currentTime = () -> (int) 100; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); Long expectedExp = currentTime.getAsLong() + (expirySeconds * 1000); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime), null, null); - String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles); JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); JwtToken jwt = jwtConsumer.getJwtToken(); @@ -73,6 +75,7 @@ public void testCreateJwtWithRoles() throws Exception { Assert.assertEquals(expectedExp, jwt.getClaim("exp")); Assert.assertNotEquals(expectedRoles, jwt.getClaim("er")); Assert.assertEquals(expectedRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("er").toString())); + Assert.assertEquals(expectedBackendRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("ebr").toString())); } @Test(expected = Exception.class) @@ -85,9 +88,9 @@ public void testCreateJwtWithBadExpiry() throws Exception { String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty(), null, null); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); - jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); } @Test(expected = Exception.class) @@ -99,9 +102,9 @@ public void testCreateJwtWithBadEncryptionKey() throws Exception { Integer expirySeconds = 300; Settings settings = Settings.builder().put("signing_key", "abc123").build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty(),null, null); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); - jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); } @Test(expected = Exception.class) @@ -115,9 +118,9 @@ public void testCreateJwtWithBadRoles() throws Exception { Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty(),null, null); + JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); - jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles); + jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles, List.of()); } //For Manual Testing @@ -138,15 +141,14 @@ public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); - JwtVendor jwtVendor = new JwtVendor(settings, currentTime,null, null); - String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles); + JwtVendor jwtVendor = new JwtVendor(settings, currentTime); + String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); JwtToken jwt = jwtConsumer.getJwtToken(); System.out.println("Encoded OBO JWT is: " + encodedJwt); - System.out.println("Decrypted role claim is: " + EncryptionDecryptionUtil.decrypt(encryptionKey, "T/XlNJfL852emYUv9/y06w==")); assertTrue(true); } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index a9988071b7..ae5bb7280e 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -40,7 +40,7 @@ public AccountApiTest() { BASE_ENDPOINT = getEndpointPrefix() + "/api/"; ENDPOINT = getEndpointPrefix() + "/api/account"; } - + @Test public void testGetAccount() throws Exception { // arrange @@ -74,13 +74,9 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsList("custom_attribute_names").size()); assertNotNull(body.getAsSettings("tenants")); assertNotNull(body.getAsList("roles")); - + response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "{\"reason\":\"Test generation\"}", encodeBasicHeader(testUser, testPass)); System.out.println("This is the response body: " + response.getBody()); - final String token = response.findValueInJson("onBehalfOfToken"); - final Header header = new org.apache.hc.core5.http.message.BasicHeader("Authorization", "Bearer " + token); - response = rh.executeGetRequest("/", header); - System.out.println("This is the response body: " + response.getBody()); } @Test @@ -198,15 +194,15 @@ public void testPutAccountRetainsAccountInformation() throws Exception { final String testPassword = "test-password"; final String newPassword = "new-password"; final String createInternalUserPayload = "{\n" - + " \"password\": \"" - + testPassword - + "\",\n" - + " \"backend_roles\": [\"test-backend-role-1\"],\n" - + " \"opendistro_security_roles\": [\"opendistro_security_all_access\"],\n" - + " \"attributes\": {\n" - + " \"attribute1\": \"value1\"\n" - + " }\n" - + "}"; + + " \"password\": \"" + + testPassword + + "\",\n" + + " \"backend_roles\": [\"test-backend-role-1\"],\n" + + " \"opendistro_security_roles\": [\"opendistro_security_all_access\"],\n" + + " \"attributes\": {\n" + + " \"attribute1\": \"value1\"\n" + + " }\n" + + "}"; final String changePasswordPayload = "{\"password\":\"" + newPassword + "\", \"current_password\":\"" + testPassword + "\"}"; final String internalUserEndpoint = BASE_ENDPOINT + "internalusers/" + testUsername; @@ -225,9 +221,9 @@ public void testPutAccountRetainsAccountInformation() throws Exception { response = rh.executeGetRequest(internalUserEndpoint); assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Settings responseBody = Settings.builder() - .loadFromSource(response.getBody(), XContentType.JSON) - .build() - .getAsSettings(testUsername); + .loadFromSource(response.getBody(), XContentType.JSON) + .build() + .getAsSettings(testUsername); assertTrue(responseBody.getAsList("backend_roles").contains("test-backend-role-1")); assertTrue(responseBody.getAsList("opendistro_security_roles").contains("opendistro_security_all_access")); assertEquals(responseBody.getAsSettings("attributes").get("attribute1"), "value1"); diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 6de931b712..38a66a4fde 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -12,7 +12,7 @@ package org.opensearch.security.http; import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -26,6 +26,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import io.netty.handler.codec.base64.Base64Decoder; import org.apache.commons.lang3.RandomStringUtils; import org.apache.hc.core5.http.HttpHeaders; import org.junit.Assert; @@ -36,54 +37,58 @@ import org.opensearch.security.util.FakeRestRequest; public class OnBehalfOfAuthenticatorTest { - final static byte[] secretKeyBytes = new byte[1024]; final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - final static SecretKey secretKey; - - static { - new SecureRandom().nextBytes(secretKeyBytes); - secretKey = Keys.hmacShaKeyFor(secretKeyBytes); - } - final static String signingKey = BaseEncoding.base64().encode(secretKeyBytes); + final static String signingKey = "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; + final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); + final static SecretKey secretKey = Keys.hmacShaKeyFor(signingKeyB64Encoded.getBytes(StandardCharsets.UTF_8)); @Test public void testNoKey() throws Exception { - final AuthCredentials credentials = extractCredentialsFromJwtHeader( - null, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false - ); - - Assert.assertNull(credentials); + try { + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + Assert.fail("Expected a RuntimeException"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("Unable to find on behalf of authenticator signing key")); + } } @Test public void testEmptyKey() throws Exception { - final AuthCredentials credentials = extractCredentialsFromJwtHeader( - "", - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false - ); - - Assert.assertNull(credentials); + try { + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + Assert.fail("Expected a RuntimeException"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("Unable to find on behalf of authenticator signing key")); + } } @Test public void testBadKey() throws Exception { - final AuthCredentials credentials = extractCredentialsFromJwtHeader( - BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false - ); - - Assert.assertNull(credentials); + try { + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false + ); + Assert.fail("Expected a WeakKeyException"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("The specified key byte array is 80 bits")); + } } @Test @@ -114,10 +119,10 @@ public void testInvalid() throws Exception { public void testBearer() throws Exception { String jwsToken = Jwts.builder() - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(secretKey, SignatureAlgorithm.HS512) - .compact(); + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .compact(); OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); @@ -127,6 +132,7 @@ public void testBearer() throws Exception { Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getSecurityRoles().size()); Assert.assertEquals(0, credentials.getBackendRoles().size()); Assert.assertEquals(2, credentials.getAttributes().size()); } @@ -161,25 +167,26 @@ public void testRoles() throws Exception { List roles = List.of("IT", "HR"); final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2"), - true + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2").setAudience("svc1"), + true ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(2, credentials.getBackendRoles().size()); + Assert.assertEquals(2, credentials.getSecurityRoles().size()); + Assert.assertEquals(0, credentials.getBackendRoles().size()); } @Test public void testNullClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", null), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", null).setAudience("svc1"), + false ); Assert.assertNotNull(credentials); @@ -191,30 +198,31 @@ public void testNullClaim() throws Exception { public void testNonStringClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L), - true + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L).setAudience("svc1"), + true ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); - Assert.assertEquals(1, credentials.getBackendRoles().size()); - Assert.assertTrue(credentials.getBackendRoles().contains("123")); + Assert.assertEquals(1, credentials.getSecurityRoles().size()); + Assert.assertTrue(credentials.getSecurityRoles().contains("123")); } @Test public void testRolesMissing() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").setAudience("svc1"), + false ); Assert.assertNotNull(credentials); Assert.assertEquals("Leonard McCoy", credentials.getUsername()); + Assert.assertEquals(0, credentials.getSecurityRoles().size()); Assert.assertEquals(0, credentials.getBackendRoles().size()); } @@ -222,10 +230,10 @@ public void testRolesMissing() throws Exception { public void testWrongSubjectKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who"), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who").setAudience("svc1"), + false ); Assert.assertNull(credentials); @@ -235,10 +243,10 @@ public void testWrongSubjectKey() throws Exception { public void testExp() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), + false ); Assert.assertNull(credentials); @@ -248,10 +256,10 @@ public void testExp() throws Exception { public void testNbf() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKey, - claimsEncryptionKey, - Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), + false ); Assert.assertNull(credentials); @@ -261,34 +269,38 @@ public void testNbf() throws Exception { public void testRolesArray() throws Exception { JwtBuilder builder = Jwts.builder() - .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); + .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); - final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKey, claimsEncryptionKey, builder, true); + final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKeyB64Encoded, claimsEncryptionKey, builder, true); Assert.assertNotNull(credentials); Assert.assertEquals("Cluster_0", credentials.getUsername()); - Assert.assertEquals(3, credentials.getBackendRoles().size()); - Assert.assertTrue(credentials.getBackendRoles().contains("a")); - Assert.assertTrue(credentials.getBackendRoles().contains("b")); - Assert.assertTrue(credentials.getBackendRoles().contains("3rd")); + Assert.assertEquals(3, credentials.getSecurityRoles().size()); + Assert.assertTrue(credentials.getSecurityRoles().contains("a")); + Assert.assertTrue(credentials.getSecurityRoles().contains("b")); + Assert.assertTrue(credentials.getSecurityRoles().contains("3rd")); } /** extracts a default user credential from a request header */ private AuthCredentials extractCredentialsFromJwtHeader( - final String signingKey, - final String encryptionKey, - final JwtBuilder jwtBuilder, - final Boolean bwcPluginCompatibilityMode + final String signingKeyB64Encoded, + final String encryptionKey, + final JwtBuilder jwtBuilder, + final Boolean bwcPluginCompatibilityMode ) { - final String jwsToken = jwtBuilder.signWith(secretKey, SignatureAlgorithm.HS512).compact(); - final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); + final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(Settings.builder() + .put("signing_key", signingKeyB64Encoded) + .put("encryption_key", encryptionKey) + .build()); + + final String jwsToken = jwtBuilder.signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512).compact(); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); } private Settings defaultSettings() { return Settings.builder() - .put("signing_key", signingKey) + .put("signing_key", signingKeyB64Encoded) .put("encryption_key", claimsEncryptionKey) .build(); } From a455250391b6504454b653877abf86ea74e3ca0a Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:00:47 -0700 Subject: [PATCH 28/74] Add obo auth integ test case with endopoint + remove manual testing case in jwtVendorTest Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 52 ++++++++++++++++++- .../security/authtoken/jwt/JwtVendorTest.java | 28 ---------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 01bea15075..859e1391e4 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -6,6 +6,8 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,6 +37,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final String audience = "audience_0"; public static final Integer expirySeconds = 100000; public static final String headerName = "Authorization"; + public static final String headerNameContentType = "Content-Type"; public static final List roles = List.of("admin", "HR"); public static final List backendRoles = List.of("IT"); @@ -53,16 +56,28 @@ public class OnBehalfOfJwtAuthenticationTest { encryptionKey ); + //private final static Path configurationFolder = OBOConfigurationFiles.createConfigurationDirectory(); + public static final String ADMIN_USER_NAME = "admin"; + public static final String DEFAULT_PASSWORD = "secret"; + public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; + + public static final String NEW_USER = "new-user"; + public static final String LIMITED_USER = "limited-user"; + @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) + .users(ADMIN_USER) + .nodeSettings(Map.of( + "plugins.security.allow_default_init_securityindex", true, + "plugins.security.restapi.roles_enabled", List.of("user_admin__all_access") + )) .authc(AUTHC_HTTPBASIC_INTERNAL) .onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) .build(); @Test - public void shouldAuthenticateWithJwtToken_positive() { + public void shouldAuthenticateWithOBOToken() { // TODO: This integration test should use an endpoint to get an OnBehalfOf token, not generate it try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ @@ -75,4 +90,37 @@ public void shouldAuthenticateWithJwtToken_positive() { throw new RuntimeException(e); } } + + @Test + public void shouldAuthenticateWithOBOTokenEndPoint() { + //Header contentTypeHeader = new BasicHeader(headerNameContentType, "json"); + Header adminOboAuthHeader; + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + + client.assertCorrectCredentials(ADMIN_USER_NAME); + + TestRestClient.HttpResponse response = client.postJson("_plugins/_security/api/user/onbehalfof", OBO_TOKEN_REASON); + response.assertStatusCode(200); + + Map oboEndPointResponse = response.getBodyAs(Map.class); + assertThat(oboEndPointResponse, allOf( + aMapWithSize(3), + hasKey("user"), + hasKey("onBehalfOfToken"), + hasKey("duration"))); + + String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); + + adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + encodedOboTokenStr); + } + + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(200); + + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(ADMIN_USER_NAME)); + } + } } 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 ae87f3f2a0..c65b92431a 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -123,32 +123,4 @@ public void testCreateJwtWithBadRoles() throws Exception { jwtVendor.createJwt(issuer, subject, audience, expirySecond, roles, List.of()); } - //For Manual Testing - @Test - public void testCreateJwtWithRolesAndEncyptionKey() throws Exception { - String issuer = "cluster_0"; - String subject = "Ryan"; - String audience = "audience_0"; - List roles = List.of("admin", "HR"); - Integer expirySeconds = 100000; - Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); - - String signingKey = Base64.getEncoder().encodeToString("This is the jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); - System.out.println("The signingKey is: " + signingKey); - - String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); - System.out.println("The encryptionkey is: " + encryptionKey); - - Settings settings = Settings.builder().put("signing_key", signingKey).put("encryption_key", encryptionKey).build(); - - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); - String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); - - JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedJwt); - JwtToken jwt = jwtConsumer.getJwtToken(); - - System.out.println("Encoded OBO JWT is: " + encodedJwt); - - assertTrue(true); - } } From 07aa2929b430bf42fa1e128f06182fc7791fbcc2 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:05:01 -0700 Subject: [PATCH 29/74] Add obo auth integ test case with endopoint + remove manual testing case in jwtVendorTest + remove debugging in AccApiTest Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 5 - .../test/framework/OnBehalfOfConfig.java | 40 +- .../test/framework/TestSecurityConfig.java | 1294 +++++++++-------- .../test/framework/cluster/LocalCluster.java | 869 +++++------ .../onbehalf/CreateOnBehalfOfToken.java | 52 +- .../jwt/EncryptionDecryptionUtil.java | 2 +- .../security/authtoken/jwt/JwtVendor.java | 2 +- .../http/OnBehalfOfAuthenticator.java | 2 +- .../security/authtoken/jwt/JwtVendorTest.java | 8 +- .../dlic/rest/api/AccountApiTest.java | 26 +- .../http/OnBehalfOfAuthenticatorTest.java | 131 +- 11 files changed, 1234 insertions(+), 1197 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 859e1391e4..7cd130873f 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -37,7 +37,6 @@ public class OnBehalfOfJwtAuthenticationTest { public static final String audience = "audience_0"; public static final Integer expirySeconds = 100000; public static final String headerName = "Authorization"; - public static final String headerNameContentType = "Content-Type"; public static final List roles = List.of("admin", "HR"); public static final List backendRoles = List.of("IT"); @@ -56,14 +55,10 @@ public class OnBehalfOfJwtAuthenticationTest { encryptionKey ); - //private final static Path configurationFolder = OBOConfigurationFiles.createConfigurationDirectory(); public static final String ADMIN_USER_NAME = "admin"; public static final String DEFAULT_PASSWORD = "secret"; public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; - public static final String NEW_USER = "new-user"; - public static final String LIMITED_USER = "limited-user"; - @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder() .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) diff --git a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java index cf9be42114..2061e21c23 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/OnBehalfOfConfig.java @@ -18,27 +18,27 @@ import org.opensearch.core.xcontent.XContentBuilder; public class OnBehalfOfConfig implements ToXContentObject { - private String signing_key; - private String encryption_key; + private String signing_key; + private String encryption_key; - public OnBehalfOfConfig signing_key(String signing_key) { - this.signing_key = signing_key; - return this; - } + public OnBehalfOfConfig signing_key(String signing_key) { + this.signing_key = signing_key; + return this; + } - public OnBehalfOfConfig encryption_key(String encryption_key) { - this.encryption_key = encryption_key; - return this; - } + public OnBehalfOfConfig encryption_key(String encryption_key) { + this.encryption_key = encryption_key; + return this; + } - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { - xContentBuilder.startObject(); - xContentBuilder.field("signing_key", signing_key); - if (StringUtils.isNoneBlank(encryption_key)){ - xContentBuilder.field("encryption_key", encryption_key); - } - xContentBuilder.endObject(); - return xContentBuilder; - } + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, ToXContent.Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.field("signing_key", signing_key); + if (StringUtils.isNoneBlank(encryption_key)) { + xContentBuilder.field("encryption_key", encryption_key); + } + xContentBuilder.endObject(); + return xContentBuilder; + } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 9ab1e70e8c..160d8455dc 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -76,645 +76,657 @@ */ public class TestSecurityConfig { - private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); - - private Config config = new Config(); - private Map internalUsers = new LinkedHashMap<>(); - private Map roles = new LinkedHashMap<>(); - private AuditConfiguration auditConfiguration; - private Map rolesMapping = new LinkedHashMap<>(); - - private String indexName = ".opendistro_security"; - - public TestSecurityConfig() { - - } - - public TestSecurityConfig configIndexName(String configIndexName) { - this.indexName = configIndexName; - return this; - } - - public TestSecurityConfig authFailureListeners(AuthFailureListeners listener) { - config.authFailureListeners(listener); - return this; - } - - public TestSecurityConfig anonymousAuth(boolean anonymousAuthEnabled) { - config.anonymousAuth(anonymousAuthEnabled); - return this; - } - - public TestSecurityConfig doNotFailOnForbidden(boolean doNotFailOnForbidden) { - config.doNotFailOnForbidden(doNotFailOnForbidden); - return this; - } - - public TestSecurityConfig xff(XffConfig xffConfig) { - config.xffConfig(xffConfig); - return this; - } - - public TestSecurityConfig onBehalfOf(OnBehalfOfConfig onBehalfOfConfig){ - config.onBehalfOfConfig(onBehalfOfConfig); - return this; - } - - public TestSecurityConfig authc(AuthcDomain authcDomain) { - config.authc(authcDomain); - return this; - } - - public TestSecurityConfig authz(AuthzDomain authzDomain) { - config.authz(authzDomain); - return this; - } - public TestSecurityConfig user(User user) { - this.internalUsers.put(user.name, user); - - for (Role role : user.roles) { - this.roles.put(role.name, role); - } - - return this; - } - - public List getUsers() { - return new ArrayList<>(internalUsers.values()); - } - - public TestSecurityConfig roles(Role... roles) { - for (Role role : roles) { - if(this.roles.containsKey(role.name)) { - throw new IllegalStateException("Role with name " + role.name + " is already defined"); - } - this.roles.put(role.name, role); - } - - return this; - } - - public TestSecurityConfig audit(AuditConfiguration auditConfiguration) { - this.auditConfiguration = auditConfiguration; - return this; - } - - public TestSecurityConfig rolesMapping(RolesMapping...mappings) { - for (RolesMapping mapping : mappings) { - String roleName = mapping.getRoleName(); - if(rolesMapping.containsKey(roleName)) { - throw new IllegalArgumentException("Role mapping " + roleName + " already exists"); - } - this.rolesMapping.put(roleName, mapping); - } - return this; - } - - public static class Config implements ToXContentObject { - private boolean anonymousAuth; - - private Boolean doNotFailOnForbidden; - private XffConfig xffConfig; - private OnBehalfOfConfig onBehalfOfConfig; - private Map authcDomainMap = new LinkedHashMap<>(); - - private AuthFailureListeners authFailureListeners; - private Map authzDomainMap = new LinkedHashMap<>(); - - public Config anonymousAuth(boolean anonymousAuth) { - this.anonymousAuth = anonymousAuth; - return this; - } - - public Config doNotFailOnForbidden(Boolean doNotFailOnForbidden) { - this.doNotFailOnForbidden = doNotFailOnForbidden; - return this; - } - - public Config xffConfig(XffConfig xffConfig) { - this.xffConfig = xffConfig; - return this; - } - - public Config onBehalfOfConfig(OnBehalfOfConfig onBehalfOfConfig) { - this.onBehalfOfConfig = onBehalfOfConfig; - return this; - } - - public Config authc(AuthcDomain authcDomain) { - authcDomainMap.put(authcDomain.id, authcDomain); - return this; - } - - public Config authFailureListeners(AuthFailureListeners authFailureListeners) { - this.authFailureListeners = authFailureListeners; - return this; - } - - public Config authz(AuthzDomain authzDomain) { - authzDomainMap.put(authzDomain.getId(), authzDomain); - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - xContentBuilder.startObject("dynamic"); - - if (onBehalfOfConfig != null) { - xContentBuilder.field("on_behalf_of", onBehalfOfConfig); - } - - if (anonymousAuth || (xffConfig != null)) { - xContentBuilder.startObject("http"); - xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); - if(xffConfig != null) { - xContentBuilder.field("xff", xffConfig); - } - xContentBuilder.endObject(); - } - if(doNotFailOnForbidden != null) { - xContentBuilder.field("do_not_fail_on_forbidden", doNotFailOnForbidden); - } - - xContentBuilder.field("authc", authcDomainMap); - if(authzDomainMap.isEmpty() == false) { - xContentBuilder.field("authz", authzDomainMap); - } - - if(authFailureListeners != null) { - xContentBuilder.field("auth_failure_listeners", authFailureListeners); - } - - xContentBuilder.endObject(); - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class User implements UserCredentialsHolder, ToXContentObject { - - public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin") - .roles(new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*")); - - String name; - private String password; - List roles = new ArrayList<>(); - private Map attributes = new HashMap<>(); - - public User(String name) { - this.name = name; - this.password = "secret"; - } - - public User password(String password) { - this.password = password; - return this; - } - - public User roles(Role... roles) { - // We scope the role names by user to keep tests free of potential side effects - String roleNamePrefix = "user_" + this.getName() + "__"; - this.roles.addAll(Arrays.asList(roles).stream().map((r) -> r.clone().name(roleNamePrefix + r.getName())).collect(Collectors.toSet())); - return this; - } - - public User attr(String key, Object value) { - this.attributes.put(key, value); - return this; - } - - public String getName() { - return name; - } - - public String getPassword() { - return password; - } - - public Set getRoleNames() { - return roles.stream().map(Role::getName).collect(Collectors.toSet()); - } - - public Object getAttribute(String attributeName) { - return attributes.get(attributeName); - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("hash", hash(password.toCharArray())); - - Set roleNames = getRoleNames(); - - if (!roleNames.isEmpty()) { - xContentBuilder.field("opendistro_security_roles", roleNames); - } - - if (attributes != null && attributes.size() != 0) { - xContentBuilder.field("attributes", attributes); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class Role implements ToXContentObject { - public static Role ALL_ACCESS = new Role("all_access").clusterPermissions("*").indexPermissions("*").on("*"); - - private String name; - private List clusterPermissions = new ArrayList<>(); - - private List indexPermissions = new ArrayList<>(); - - public Role(String name) { - this.name = name; - } - - public Role clusterPermissions(String... clusterPermissions) { - this.clusterPermissions.addAll(Arrays.asList(clusterPermissions)); - return this; - } - - public IndexPermission indexPermissions(String... indexPermissions) { - return new IndexPermission(this, indexPermissions); - } - - public Role name(String name) { - this.name = name; - return this; - } - - public String getName() { - return name; - } - - public Role clone() { - Role role = new Role(this.name); - role.clusterPermissions.addAll(this.clusterPermissions); - role.indexPermissions.addAll(this.indexPermissions); - return role; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - if (!clusterPermissions.isEmpty()) { - xContentBuilder.field("cluster_permissions", clusterPermissions); - } - - if (!indexPermissions.isEmpty()) { - xContentBuilder.field("index_permissions", indexPermissions); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class IndexPermission implements ToXContentObject { - private List allowedActions; - private List indexPatterns; - private Role role; - private String dlsQuery; - private List fls; - private List maskedFields; - - IndexPermission(Role role, String... allowedActions) { - this.allowedActions = Arrays.asList(allowedActions); - this.role = role; - } - - public IndexPermission dls(String dlsQuery) { - this.dlsQuery = dlsQuery; - return this; - } - - public IndexPermission fls(String... fls) { - this.fls = Arrays.asList(fls); - return this; - } - - public IndexPermission maskedFields(String... maskedFields) { - this.maskedFields = Arrays.asList(maskedFields); - return this; - } - - public Role on(String... indexPatterns) { - this.indexPatterns = Arrays.asList(indexPatterns); - this.role.indexPermissions.add(this); - return this.role; - } - - public Role on(TestIndex... testindices) { - this.indexPatterns = Arrays.asList(testindices).stream().map(TestIndex::getName).collect(Collectors.toList()); - this.role.indexPermissions.add(this); - return this.role; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("index_patterns", indexPatterns); - xContentBuilder.field("allowed_actions", allowedActions); - - if (dlsQuery != null) { - xContentBuilder.field("dls", dlsQuery); - } - - if (fls != null) { - xContentBuilder.field("fls", fls); - } - - if (maskedFields != null) { - xContentBuilder.field("masked_fields", maskedFields); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class AuthcDomain implements ToXContentObject { - - private static String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqZbjLUAWc+DZTkinQAdvy1GFjPHPnxheU89hSiWoDD3NOW76H3u3T7cCDdOah2msdxSlBmCBH6wik8qLYkcV8owWukQg3PQmbEhrdPaKo0QCgomWs4nLgtmEYqcZ+QQldd82MdTlQ1QmoQmI9Uxqs1SuaKZASp3Gy19y8su5CV+FZ6BruUw9HELK055sAwl3X7j5ouabXGbcib2goBF3P52LkvbJLuWr5HDZEOeSkwIeqSeMojASM96K5SdotD+HwEyjaTjzRPL2Aa1BEQFWOQ6CFJLyLH7ZStDuPM1mJU1VxIVfMbZrhsUBjAnIhRynmWxML7YlNqkP9j6jyOIYQIDAQAB"; - - public static final int BASIC_AUTH_DOMAIN_ORDER = 0; - public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", BASIC_AUTH_DOMAIN_ORDER) - .httpAuthenticatorWithChallenge("basic").backend("internal"); - - public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE = new TestSecurityConfig.AuthcDomain("basic", - BASIC_AUTH_DOMAIN_ORDER) - .httpAuthenticator("basic").backend("internal"); - - public final static AuthcDomain DISABLED_AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig - .AuthcDomain("basic", BASIC_AUTH_DOMAIN_ORDER, false).httpAuthenticator("basic").backend("internal"); - - public final static AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig - .AuthcDomain("jwt", 1) - .jwtHttpAuthenticator(new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(PUBLIC_KEY)).backend("noop"); - - private final String id; - private boolean enabled = true; - private int order; - private List skipUsers = new ArrayList<>(); - private HttpAuthenticator httpAuthenticator; - private AuthenticationBackend authenticationBackend; - - public AuthcDomain(String id, int order, boolean enabled) { - this.id = id; - this.order = order; - this.enabled = enabled; - } - - public AuthcDomain(String id, int order) { - this(id, order, true); - } - - public AuthcDomain httpAuthenticator(String type) { - this.httpAuthenticator = new HttpAuthenticator(type); - return this; - } - - public AuthcDomain jwtHttpAuthenticator(JwtConfigBuilder builder) { - this.httpAuthenticator = new HttpAuthenticator("jwt") - .challenge(false).config(builder.build()); - return this; - } - - public AuthcDomain httpAuthenticatorWithChallenge(String type) { - this.httpAuthenticator = new HttpAuthenticator(type).challenge(true); - return this; - } - - public AuthcDomain httpAuthenticator(HttpAuthenticator httpAuthenticator) { - this.httpAuthenticator = httpAuthenticator; - return this; - } - - public AuthcDomain backend(String type) { - this.authenticationBackend = new AuthenticationBackend(type); - return this; - } - - public AuthcDomain backend(AuthenticationBackend authenticationBackend) { - this.authenticationBackend = authenticationBackend; - return this; - } - - public AuthcDomain skipUsers(String... users) { - this.skipUsers.addAll(Arrays.asList(users)); - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("http_enabled", enabled); - xContentBuilder.field("order", order); - - if (httpAuthenticator != null) { - xContentBuilder.field("http_authenticator", httpAuthenticator); - } - - if (authenticationBackend != null) { - xContentBuilder.field("authentication_backend", authenticationBackend); - } - - if (skipUsers != null && skipUsers.size() > 0) { - xContentBuilder.field("skip_users", skipUsers); - } - - xContentBuilder.endObject(); - return xContentBuilder; - } - - public static class HttpAuthenticator implements ToXContentObject { - private final String type; - private boolean challenge; - private Map config = new HashMap(); - - public HttpAuthenticator(String type) { - this.type = type; - } - - public HttpAuthenticator challenge(boolean challenge) { - this.challenge = challenge; - return this; - } - - public HttpAuthenticator config(Map config) { - this.config.putAll(config); - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("type", type); - xContentBuilder.field("challenge", challenge); - xContentBuilder.field("config", config); - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - - public static class AuthenticationBackend implements ToXContentObject { - private final String type; - private Supplier> config = () -> new HashMap(); - - public AuthenticationBackend(String type) { - this.type = type; - } - - public AuthenticationBackend config(Map config) { - Map configCopy = new HashMap<>(config); - this.config = () -> configCopy; - return this; - } - - public AuthenticationBackend config(Supplier> configSupplier) { - this.config = configSupplier; - return this; - } - - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - - xContentBuilder.field("type", type); - xContentBuilder.field("config", config.get()); - - xContentBuilder.endObject(); - return xContentBuilder; - } - } - } - - public void initIndex(Client client) { - Map settings = new HashMap<>(); - if (indexName.startsWith(".")) { - settings.put("index.hidden", true); - } - client.admin().indices().create(new CreateIndexRequest(indexName).settings(settings)).actionGet(); - - writeSingleEntryConfigToIndex(client, CType.CONFIG, config); - if(auditConfiguration != null) { - writeSingleEntryConfigToIndex(client, CType.AUDIT, "config", auditConfiguration); - } - writeConfigToIndex(client, CType.ROLES, roles); - writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers); - writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); - writeEmptyConfigToIndex(client, CType.ACTIONGROUPS); - writeEmptyConfigToIndex(client, CType.TENANTS); - } - - public void updateInternalUsersConfiguration(Client client, List users) { - Map userMap = new HashMap<>(); - for(User user : users) { - userMap.put(user.getName(), user); - } - updateConfigInIndex(client, CType.INTERNALUSERS, userMap); - } - - - static String hash(final char[] clearTextPassword) { - final byte[] salt = new byte[16]; - new SecureRandom().nextBytes(salt); - final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); - Arrays.fill(salt, (byte) 0); - Arrays.fill(clearTextPassword, '\0'); - return hash; - } - - private void writeEmptyConfigToIndex(Client client, CType configType) { - writeConfigToIndex(client, configType, Collections.emptyMap()); - } - - private void writeConfigToIndex(Client client, CType configType, Map config) { - try { - String json = configToJson(configType, config); - - log.info("Writing security configuration into index " + configType + ":\n" + json); - - BytesReference bytesReference = toByteReference(json); - client.index(new IndexRequest(indexName).id(configType.toLCString()) - .setRefreshPolicy(IMMEDIATE).source(configType.toLCString(), bytesReference)) - .actionGet(); - } catch (Exception e) { - throw new RuntimeException("Error while initializing config for " + indexName, e); - } - } - - private static BytesReference toByteReference(String string) throws UnsupportedEncodingException { - return BytesReference.fromByteBuffer(ByteBuffer.wrap(string.getBytes("utf-8"))); - } - - private void updateConfigInIndex(Client client, CType configType, Map config) { - try { - String json = configToJson(configType, config); - BytesReference bytesReference = toByteReference(json); - log.info("Update configuration of type '{}' in index '{}', new value '{}'.", configType, indexName, json); - UpdateRequest upsert = new UpdateRequest(indexName, configType.toLCString()).doc(configType.toLCString(), bytesReference) - .setRefreshPolicy(IMMEDIATE); - client.update(upsert).actionGet(); - } catch (Exception e) { - throw new RuntimeException("Error while updating config for " + indexName, e); - } - } - - private static String configToJson(CType configType, Map config) throws IOException { - XContentBuilder builder = XContentFactory.jsonBuilder(); - - builder.startObject(); - builder.startObject("_meta"); - builder.field("type", configType.toLCString()); - builder.field("config_version", 2); - builder.endObject(); - - for (Map.Entry entry : config.entrySet()) { - builder.field(entry.getKey(), entry.getValue()); - } - - builder.endObject(); - - return Strings.toString(builder); - } - - private void writeSingleEntryConfigToIndex(Client client, CType configType, ToXContentObject config) { - writeSingleEntryConfigToIndex(client, configType, configType.toLCString(), config); - } - - private void writeSingleEntryConfigToIndex(Client client, CType configType, String configurationRoot, ToXContentObject config) { - try { - XContentBuilder builder = XContentFactory.jsonBuilder(); - - builder.startObject(); - builder.startObject("_meta"); - builder.field("type", configType.toLCString()); - builder.field("config_version", 2); - builder.endObject(); - - builder.field(configurationRoot, config); - - builder.endObject(); - - String json = Strings.toString(builder); - - log.info("Writing security plugin configuration into index " + configType + ":\n" + json); - - client.index(new IndexRequest(indexName).id(configType.toLCString()) - .setRefreshPolicy(IMMEDIATE).source(configType.toLCString(), toByteReference(json))) - .actionGet(); - } catch (Exception e) { - throw new RuntimeException("Error while initializing config for " + indexName, e); - } - } + private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); + + private Config config = new Config(); + private Map internalUsers = new LinkedHashMap<>(); + private Map roles = new LinkedHashMap<>(); + private AuditConfiguration auditConfiguration; + private Map rolesMapping = new LinkedHashMap<>(); + + private String indexName = ".opendistro_security"; + + public TestSecurityConfig() { + + } + + public TestSecurityConfig configIndexName(String configIndexName) { + this.indexName = configIndexName; + return this; + } + + public TestSecurityConfig authFailureListeners(AuthFailureListeners listener) { + config.authFailureListeners(listener); + return this; + } + + public TestSecurityConfig anonymousAuth(boolean anonymousAuthEnabled) { + config.anonymousAuth(anonymousAuthEnabled); + return this; + } + + public TestSecurityConfig doNotFailOnForbidden(boolean doNotFailOnForbidden) { + config.doNotFailOnForbidden(doNotFailOnForbidden); + return this; + } + + public TestSecurityConfig xff(XffConfig xffConfig) { + config.xffConfig(xffConfig); + return this; + } + + public TestSecurityConfig onBehalfOf(OnBehalfOfConfig onBehalfOfConfig) { + config.onBehalfOfConfig(onBehalfOfConfig); + return this; + } + + public TestSecurityConfig authc(AuthcDomain authcDomain) { + config.authc(authcDomain); + return this; + } + + public TestSecurityConfig authz(AuthzDomain authzDomain) { + config.authz(authzDomain); + return this; + } + + public TestSecurityConfig user(User user) { + this.internalUsers.put(user.name, user); + + for (Role role : user.roles) { + this.roles.put(role.name, role); + } + + return this; + } + + public List getUsers() { + return new ArrayList<>(internalUsers.values()); + } + + public TestSecurityConfig roles(Role... roles) { + for (Role role : roles) { + if (this.roles.containsKey(role.name)) { + throw new IllegalStateException("Role with name " + role.name + " is already defined"); + } + this.roles.put(role.name, role); + } + + return this; + } + + public TestSecurityConfig audit(AuditConfiguration auditConfiguration) { + this.auditConfiguration = auditConfiguration; + return this; + } + + public TestSecurityConfig rolesMapping(RolesMapping... mappings) { + for (RolesMapping mapping : mappings) { + String roleName = mapping.getRoleName(); + if (rolesMapping.containsKey(roleName)) { + throw new IllegalArgumentException("Role mapping " + roleName + " already exists"); + } + this.rolesMapping.put(roleName, mapping); + } + return this; + } + + public static class Config implements ToXContentObject { + private boolean anonymousAuth; + + private Boolean doNotFailOnForbidden; + private XffConfig xffConfig; + private OnBehalfOfConfig onBehalfOfConfig; + private Map authcDomainMap = new LinkedHashMap<>(); + + private AuthFailureListeners authFailureListeners; + private Map authzDomainMap = new LinkedHashMap<>(); + + public Config anonymousAuth(boolean anonymousAuth) { + this.anonymousAuth = anonymousAuth; + return this; + } + + public Config doNotFailOnForbidden(Boolean doNotFailOnForbidden) { + this.doNotFailOnForbidden = doNotFailOnForbidden; + return this; + } + + public Config xffConfig(XffConfig xffConfig) { + this.xffConfig = xffConfig; + return this; + } + + public Config onBehalfOfConfig(OnBehalfOfConfig onBehalfOfConfig) { + this.onBehalfOfConfig = onBehalfOfConfig; + return this; + } + + public Config authc(AuthcDomain authcDomain) { + authcDomainMap.put(authcDomain.id, authcDomain); + return this; + } + + public Config authFailureListeners(AuthFailureListeners authFailureListeners) { + this.authFailureListeners = authFailureListeners; + return this; + } + + public Config authz(AuthzDomain authzDomain) { + authzDomainMap.put(authzDomain.getId(), authzDomain); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + xContentBuilder.startObject("dynamic"); + + if (onBehalfOfConfig != null) { + xContentBuilder.field("on_behalf_of", onBehalfOfConfig); + } + + if (anonymousAuth || (xffConfig != null)) { + xContentBuilder.startObject("http"); + xContentBuilder.field("anonymous_auth_enabled", anonymousAuth); + if (xffConfig != null) { + xContentBuilder.field("xff", xffConfig); + } + xContentBuilder.endObject(); + } + if (doNotFailOnForbidden != null) { + xContentBuilder.field("do_not_fail_on_forbidden", doNotFailOnForbidden); + } + + xContentBuilder.field("authc", authcDomainMap); + if (authzDomainMap.isEmpty() == false) { + xContentBuilder.field("authz", authzDomainMap); + } + + if (authFailureListeners != null) { + xContentBuilder.field("auth_failure_listeners", authFailureListeners); + } + + xContentBuilder.endObject(); + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class User implements UserCredentialsHolder, ToXContentObject { + + public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles( + new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*") + ); + + String name; + private String password; + List roles = new ArrayList<>(); + private Map attributes = new HashMap<>(); + + public User(String name) { + this.name = name; + this.password = "secret"; + } + + public User password(String password) { + this.password = password; + return this; + } + + public User roles(Role... roles) { + // We scope the role names by user to keep tests free of potential side effects + String roleNamePrefix = "user_" + this.getName() + "__"; + this.roles.addAll( + Arrays.asList(roles).stream().map((r) -> r.clone().name(roleNamePrefix + r.getName())).collect(Collectors.toSet()) + ); + return this; + } + + public User attr(String key, Object value) { + this.attributes.put(key, value); + return this; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + + public Set getRoleNames() { + return roles.stream().map(Role::getName).collect(Collectors.toSet()); + } + + public Object getAttribute(String attributeName) { + return attributes.get(attributeName); + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("hash", hash(password.toCharArray())); + + Set roleNames = getRoleNames(); + + if (!roleNames.isEmpty()) { + xContentBuilder.field("opendistro_security_roles", roleNames); + } + + if (attributes != null && attributes.size() != 0) { + xContentBuilder.field("attributes", attributes); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class Role implements ToXContentObject { + public static Role ALL_ACCESS = new Role("all_access").clusterPermissions("*").indexPermissions("*").on("*"); + + private String name; + private List clusterPermissions = new ArrayList<>(); + + private List indexPermissions = new ArrayList<>(); + + public Role(String name) { + this.name = name; + } + + public Role clusterPermissions(String... clusterPermissions) { + this.clusterPermissions.addAll(Arrays.asList(clusterPermissions)); + return this; + } + + public IndexPermission indexPermissions(String... indexPermissions) { + return new IndexPermission(this, indexPermissions); + } + + public Role name(String name) { + this.name = name; + return this; + } + + public String getName() { + return name; + } + + public Role clone() { + Role role = new Role(this.name); + role.clusterPermissions.addAll(this.clusterPermissions); + role.indexPermissions.addAll(this.indexPermissions); + return role; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + if (!clusterPermissions.isEmpty()) { + xContentBuilder.field("cluster_permissions", clusterPermissions); + } + + if (!indexPermissions.isEmpty()) { + xContentBuilder.field("index_permissions", indexPermissions); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class IndexPermission implements ToXContentObject { + private List allowedActions; + private List indexPatterns; + private Role role; + private String dlsQuery; + private List fls; + private List maskedFields; + + IndexPermission(Role role, String... allowedActions) { + this.allowedActions = Arrays.asList(allowedActions); + this.role = role; + } + + public IndexPermission dls(String dlsQuery) { + this.dlsQuery = dlsQuery; + return this; + } + + public IndexPermission fls(String... fls) { + this.fls = Arrays.asList(fls); + return this; + } + + public IndexPermission maskedFields(String... maskedFields) { + this.maskedFields = Arrays.asList(maskedFields); + return this; + } + + public Role on(String... indexPatterns) { + this.indexPatterns = Arrays.asList(indexPatterns); + this.role.indexPermissions.add(this); + return this.role; + } + + public Role on(TestIndex... testindices) { + this.indexPatterns = Arrays.asList(testindices).stream().map(TestIndex::getName).collect(Collectors.toList()); + this.role.indexPermissions.add(this); + return this.role; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("index_patterns", indexPatterns); + xContentBuilder.field("allowed_actions", allowedActions); + + if (dlsQuery != null) { + xContentBuilder.field("dls", dlsQuery); + } + + if (fls != null) { + xContentBuilder.field("fls", fls); + } + + if (maskedFields != null) { + xContentBuilder.field("masked_fields", maskedFields); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class AuthcDomain implements ToXContentObject { + + private static String PUBLIC_KEY = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqZbjLUAWc+DZTkinQAdvy1GFjPHPnxheU89hSiWoDD3NOW76H3u3T7cCDdOah2msdxSlBmCBH6wik8qLYkcV8owWukQg3PQmbEhrdPaKo0QCgomWs4nLgtmEYqcZ+QQldd82MdTlQ1QmoQmI9Uxqs1SuaKZASp3Gy19y8su5CV+FZ6BruUw9HELK055sAwl3X7j5ouabXGbcib2goBF3P52LkvbJLuWr5HDZEOeSkwIeqSeMojASM96K5SdotD+HwEyjaTjzRPL2Aa1BEQFWOQ6CFJLyLH7ZStDuPM1mJU1VxIVfMbZrhsUBjAnIhRynmWxML7YlNqkP9j6jyOIYQIDAQAB"; + + public static final int BASIC_AUTH_DOMAIN_ORDER = 0; + public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain("basic", BASIC_AUTH_DOMAIN_ORDER) + .httpAuthenticatorWithChallenge("basic") + .backend("internal"); + + public final static AuthcDomain AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE = new TestSecurityConfig.AuthcDomain( + "basic", + BASIC_AUTH_DOMAIN_ORDER + ).httpAuthenticator("basic").backend("internal"); + + public final static AuthcDomain DISABLED_AUTHC_HTTPBASIC_INTERNAL = new TestSecurityConfig.AuthcDomain( + "basic", + BASIC_AUTH_DOMAIN_ORDER, + false + ).httpAuthenticator("basic").backend("internal"); + + public final static AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain("jwt", 1).jwtHttpAuthenticator( + new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(PUBLIC_KEY) + ).backend("noop"); + + private final String id; + private boolean enabled = true; + private int order; + private List skipUsers = new ArrayList<>(); + private HttpAuthenticator httpAuthenticator; + private AuthenticationBackend authenticationBackend; + + public AuthcDomain(String id, int order, boolean enabled) { + this.id = id; + this.order = order; + this.enabled = enabled; + } + + public AuthcDomain(String id, int order) { + this(id, order, true); + } + + public AuthcDomain httpAuthenticator(String type) { + this.httpAuthenticator = new HttpAuthenticator(type); + return this; + } + + public AuthcDomain jwtHttpAuthenticator(JwtConfigBuilder builder) { + this.httpAuthenticator = new HttpAuthenticator("jwt").challenge(false).config(builder.build()); + return this; + } + + public AuthcDomain httpAuthenticatorWithChallenge(String type) { + this.httpAuthenticator = new HttpAuthenticator(type).challenge(true); + return this; + } + + public AuthcDomain httpAuthenticator(HttpAuthenticator httpAuthenticator) { + this.httpAuthenticator = httpAuthenticator; + return this; + } + + public AuthcDomain backend(String type) { + this.authenticationBackend = new AuthenticationBackend(type); + return this; + } + + public AuthcDomain backend(AuthenticationBackend authenticationBackend) { + this.authenticationBackend = authenticationBackend; + return this; + } + + public AuthcDomain skipUsers(String... users) { + this.skipUsers.addAll(Arrays.asList(users)); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("http_enabled", enabled); + xContentBuilder.field("order", order); + + if (httpAuthenticator != null) { + xContentBuilder.field("http_authenticator", httpAuthenticator); + } + + if (authenticationBackend != null) { + xContentBuilder.field("authentication_backend", authenticationBackend); + } + + if (skipUsers != null && skipUsers.size() > 0) { + xContentBuilder.field("skip_users", skipUsers); + } + + xContentBuilder.endObject(); + return xContentBuilder; + } + + public static class HttpAuthenticator implements ToXContentObject { + private final String type; + private boolean challenge; + private Map config = new HashMap(); + + public HttpAuthenticator(String type) { + this.type = type; + } + + public HttpAuthenticator challenge(boolean challenge) { + this.challenge = challenge; + return this; + } + + public HttpAuthenticator config(Map config) { + this.config.putAll(config); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("type", type); + xContentBuilder.field("challenge", challenge); + xContentBuilder.field("config", config); + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + + public static class AuthenticationBackend implements ToXContentObject { + private final String type; + private Supplier> config = () -> new HashMap(); + + public AuthenticationBackend(String type) { + this.type = type; + } + + public AuthenticationBackend config(Map config) { + Map configCopy = new HashMap<>(config); + this.config = () -> configCopy; + return this; + } + + public AuthenticationBackend config(Supplier> configSupplier) { + this.config = configSupplier; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.startObject(); + + xContentBuilder.field("type", type); + xContentBuilder.field("config", config.get()); + + xContentBuilder.endObject(); + return xContentBuilder; + } + } + } + + public void initIndex(Client client) { + Map settings = new HashMap<>(); + if (indexName.startsWith(".")) { + settings.put("index.hidden", true); + } + client.admin().indices().create(new CreateIndexRequest(indexName).settings(settings)).actionGet(); + + writeSingleEntryConfigToIndex(client, CType.CONFIG, config); + if (auditConfiguration != null) { + writeSingleEntryConfigToIndex(client, CType.AUDIT, "config", auditConfiguration); + } + writeConfigToIndex(client, CType.ROLES, roles); + writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers); + writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping); + writeEmptyConfigToIndex(client, CType.ACTIONGROUPS); + writeEmptyConfigToIndex(client, CType.TENANTS); + } + + public void updateInternalUsersConfiguration(Client client, List users) { + Map userMap = new HashMap<>(); + for (User user : users) { + userMap.put(user.getName(), user); + } + updateConfigInIndex(client, CType.INTERNALUSERS, userMap); + } + + static String hash(final char[] clearTextPassword) { + final byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); + Arrays.fill(salt, (byte) 0); + Arrays.fill(clearTextPassword, '\0'); + return hash; + } + + private void writeEmptyConfigToIndex(Client client, CType configType) { + writeConfigToIndex(client, configType, Collections.emptyMap()); + } + + private void writeConfigToIndex(Client client, CType configType, Map config) { + try { + String json = configToJson(configType, config); + + log.info("Writing security configuration into index " + configType + ":\n" + json); + + BytesReference bytesReference = toByteReference(json); + client.index( + new IndexRequest(indexName).id(configType.toLCString()) + .setRefreshPolicy(IMMEDIATE) + .source(configType.toLCString(), bytesReference) + ).actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while initializing config for " + indexName, e); + } + } + + private static BytesReference toByteReference(String string) throws UnsupportedEncodingException { + return BytesReference.fromByteBuffer(ByteBuffer.wrap(string.getBytes("utf-8"))); + } + + private void updateConfigInIndex(Client client, CType configType, Map config) { + try { + String json = configToJson(configType, config); + BytesReference bytesReference = toByteReference(json); + log.info("Update configuration of type '{}' in index '{}', new value '{}'.", configType, indexName, json); + UpdateRequest upsert = new UpdateRequest(indexName, configType.toLCString()).doc(configType.toLCString(), bytesReference) + .setRefreshPolicy(IMMEDIATE); + client.update(upsert).actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while updating config for " + indexName, e); + } + } + + private static String configToJson(CType configType, Map config) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + + builder.startObject(); + builder.startObject("_meta"); + builder.field("type", configType.toLCString()); + builder.field("config_version", 2); + builder.endObject(); + + for (Map.Entry entry : config.entrySet()) { + builder.field(entry.getKey(), entry.getValue()); + } + + builder.endObject(); + + return Strings.toString(builder); + } + + private void writeSingleEntryConfigToIndex(Client client, CType configType, ToXContentObject config) { + writeSingleEntryConfigToIndex(client, configType, configType.toLCString(), config); + } + + private void writeSingleEntryConfigToIndex(Client client, CType configType, String configurationRoot, ToXContentObject config) { + try { + XContentBuilder builder = XContentFactory.jsonBuilder(); + + builder.startObject(); + builder.startObject("_meta"); + builder.field("type", configType.toLCString()); + builder.field("config_version", 2); + builder.endObject(); + + builder.field(configurationRoot, config); + + builder.endObject(); + + String json = Strings.toString(builder); + + log.info("Writing security plugin configuration into index " + configType + ":\n" + json); + + client.index( + new IndexRequest(indexName).id(configType.toLCString()) + .setRefreshPolicy(IMMEDIATE) + .source(configType.toLCString(), toByteReference(json)) + ).actionGet(); + } catch (Exception e) { + throw new RuntimeException("Error while initializing config for " + indexName, e); + } + } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 13465294e5..64207ead5b 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -75,419 +75,460 @@ */ public class LocalCluster extends ExternalResource implements AutoCloseable, OpenSearchClientProvider { - private static final Logger log = LogManager.getLogger(LocalCluster.class); - - public static final String INIT_CONFIGURATION_DIR = "security.default_init.dir"; - - protected static final AtomicLong num = new AtomicLong(); - - private boolean sslOnly; - - private final List> plugins; - private final ClusterManager clusterManager; - private final TestSecurityConfig testSecurityConfig; - private Settings nodeOverride; - private final String clusterName; - private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; - private final TestCertificates testCertificates; - private final List clusterDependencies; - private final Map remotes; - private volatile LocalOpenSearchCluster localOpenSearchCluster; - private final List testIndices; - - private boolean loadConfigurationIntoIndex; - - private LocalCluster(String clusterName, TestSecurityConfig testSgConfig, boolean sslOnly, Settings nodeOverride, - ClusterManager clusterManager, List> plugins, TestCertificates testCertificates, - List clusterDependencies, Map remotes, List testIndices, - boolean loadConfigurationIntoIndex, String defaultConfigurationInitDirectory) { - this.plugins = plugins; - this.testCertificates = testCertificates; - this.clusterManager = clusterManager; - this.testSecurityConfig = testSgConfig; - this.sslOnly = sslOnly; - this.nodeOverride = nodeOverride; - this.clusterName = clusterName; - this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); - this.remotes = remotes; - this.clusterDependencies = clusterDependencies; - this.testIndices = testIndices; - this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; - if(StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { - System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); - } - } - - public String getSnapshotDirPath() { - return localOpenSearchCluster.getSnapshotDirPath(); - } - - @Override - public void before() throws Throwable { - if (localOpenSearchCluster == null) { - for (LocalCluster dependency : clusterDependencies) { - if (!dependency.isStarted()) { - dependency.before(); - } - } - - for (Map.Entry entry : remotes.entrySet()) { - @SuppressWarnings("resource") - InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress(); - String key = "cluster.remote." + entry.getKey() + ".seeds"; - String value = transportAddress.getHostString() + ":" + transportAddress.getPort(); - log.info("Remote cluster '{}' added to configuration with the following seed '{}'", key, value); - nodeOverride = Settings.builder().put(nodeOverride) - .putList(key, value) - .build(); - } - start(); - } - } - - @Override - protected void after() { - System.clearProperty(INIT_CONFIGURATION_DIR); - close(); - } - - @Override - public void close() { - if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { - try { - localOpenSearchCluster.destroy(); - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - localOpenSearchCluster = null; - } - } - } - - @Override - public String getClusterName() { - return clusterName; - } - - @Override - public InetSocketAddress getHttpAddress() { - return localOpenSearchCluster.clientNode().getHttpAddress(); - } - - public int getHttpPort() { - return getHttpAddress().getPort(); - } - - @Override - public InetSocketAddress getTransportAddress() { - return localOpenSearchCluster.clientNode().getTransportAddress(); - } - - /** - * Returns a Client object that performs cluster-internal requests. As these requests are regard as cluster-internal, - * no authentication is performed and no user-information is attached to these requests. Thus, this client should - * be only used for preparing test environments, but not as a test subject. - */ - public Client getInternalNodeClient() { - return localOpenSearchCluster.clientNode().getInternalNodeClient(); - } - - /** - * Returns a random node of this cluster. - */ - public PluginAwareNode node() { - return this.localOpenSearchCluster.clusterManagerNode().esNode(); - } - - /** - * Returns all nodes of this cluster. - */ - public List nodes() { - return this.localOpenSearchCluster.getNodes(); - } - - public LocalOpenSearchCluster.Node getNodeByName(String name) { - return this.localOpenSearchCluster.getNodeByName(name); - } - - public boolean isStarted() { - return localOpenSearchCluster != null; - } - - public List getConfiguredUsers() { - return testSecurityConfig.getUsers(); - } - - public Random getRandom() { - return localOpenSearchCluster.getRandom(); - } - - private void start() { - try { - NodeSettingsSupplier nodeSettingsSupplier = minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings(sslOnly, nodeOverride); - localOpenSearchCluster = new LocalOpenSearchCluster(clusterName, clusterManager, nodeSettingsSupplier, plugins, testCertificates); - - localOpenSearchCluster.start(); - - - if (loadConfigurationIntoIndex) { - initSecurityIndex(testSecurityConfig); - } - - try (Client client = getInternalNodeClient()) { - for (TestIndex index : this.testIndices) { - index.create(client); - } - } - - } catch (Exception e) { - log.error("Local ES cluster start failed", e); - throw new RuntimeException(e); - } - } - - private void initSecurityIndex(TestSecurityConfig testSecurityConfig) { - log.info("Initializing OpenSearch Security index"); - try(Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true"))) { - testSecurityConfig.initIndex(client); - triggerConfigurationReload(client); - } - } - - public void updateUserConfiguration(List users) { - try(Client client = new ContextHeaderDecoratorClient(this.getInternalNodeClient(), Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER , "true"))) { - testSecurityConfig.updateInternalUsersConfiguration(client, users); - triggerConfigurationReload(client); - } - } - - private static void triggerConfigurationReload(Client client) { - ConfigUpdateResponse configUpdateResponse = client.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0]))).actionGet(); - if (configUpdateResponse.hasFailures()) { - throw new RuntimeException("ConfigUpdateResponse produced failures: " + configUpdateResponse.failures()); - } - } - - public CertificateData getAdminCertificate() { - return testCertificates.getAdminCertificateData(); - } - - public static class Builder { - - private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); - - private boolean sslOnly = false; - private final List> plugins = new ArrayList<>(); - private Map remoteClusters = new HashMap<>(); - private List clusterDependencies = new ArrayList<>(); - private List testIndices = new ArrayList<>(); - private ClusterManager clusterManager = ClusterManager.DEFAULT; - private TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); - private String clusterName = "local_cluster"; - private TestCertificates testCertificates; - - private boolean loadConfigurationIntoIndex = true; - - private String defaultConfigurationInitDirectory = null; - - public Builder() { - } - - public Builder dependsOn(Object object) { - // We just want to make sure that the object is already done - if (object == null) { - throw new IllegalStateException("Dependency not fulfilled"); - } - return this; - } - - public Builder clusterManager(ClusterManager clusterManager) { - this.clusterManager = clusterManager; - return this; - } - - /** - * Starts a cluster with only one node and thus saves some resources during startup. This shall be only used - * for tests where the node interactions are not relevant to the test. An example for this would be - * authentication tests, as authentication is always done on the directly connected node. - */ - public Builder singleNode() { - this.clusterManager = ClusterManager.SINGLENODE; - return this; - } - - /** - * Specifies the configuration of the security plugin that shall be used by this cluster. - */ - public Builder config(TestSecurityConfig testSecurityConfig) { - this.testSecurityConfig = testSecurityConfig; - return this; - } - - public Builder sslOnly(boolean sslOnly) { - this.sslOnly = sslOnly; - return this; - } - - public Builder nodeSettings(Map settings) { - settings.forEach((key, value) -> { - if (value instanceof List) { - List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); - nodeOverrideSettingsBuilder.putList(key, values); - } else { - nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); - } - }); - - return this; - } - - /** - * Adds additional plugins to the cluster - */ - public Builder plugin(Class plugin) { - this.plugins.add(plugin); - - return this; - } - - public Builder authFailureListeners(AuthFailureListeners listener) { - testSecurityConfig.authFailureListeners(listener); - return this; - } - - /** - * Specifies a remote cluster and its name. The remote cluster can be then used in Cross Cluster Search - * operations with the specified name. - */ - public Builder remote(String name, LocalCluster anotherCluster) { - remoteClusters.put(name, anotherCluster); - - clusterDependencies.add(anotherCluster); - - return this; - } - - /** - * Specifies test indices that shall be created upon startup of the cluster. - */ - public Builder indices(TestIndex... indices) { - this.testIndices.addAll(Arrays.asList(indices)); - return this; - } - - public Builder users(TestSecurityConfig.User... users) { - for (TestSecurityConfig.User user : users) { - testSecurityConfig.user(user); - } - return this; - } - - public Builder audit(AuditConfiguration auditConfiguration) { - if (auditConfiguration != null) { - testSecurityConfig.audit(auditConfiguration); - } - if (auditConfiguration.isEnabled()) { - nodeOverrideSettingsBuilder.put("plugins.security.audit.type", TestRuleAuditLogSink.class.getName()); - } else { - nodeOverrideSettingsBuilder.put("plugins.security.audit.type", "noop"); - } - return this; - } - - public List getUsers() { - return testSecurityConfig.getUsers(); - } - - public Builder roles(Role... roles) { - testSecurityConfig.roles(roles); - return this; - } - - public Builder rolesMapping(RolesMapping...mappings) { - testSecurityConfig.rolesMapping(mappings); - return this; - } - - public Builder authc(TestSecurityConfig.AuthcDomain authc) { - testSecurityConfig.authc(authc); - return this; - } - - public Builder authz(AuthzDomain authzDomain) { - testSecurityConfig.authz(authzDomain); - return this; - } - - public Builder clusterName(String clusterName) { - this.clusterName = clusterName; - return this; - } - - public Builder configIndexName(String configIndexName) { - testSecurityConfig.configIndexName(configIndexName); - return this; - } - - public Builder testCertificates(TestCertificates certificates) { - this.testCertificates = certificates; - return this; - } - - public Builder anonymousAuth(boolean anonAuthEnabled) { - testSecurityConfig.anonymousAuth(anonAuthEnabled); - return this; - } - - public Builder xff(XffConfig xffConfig){ - testSecurityConfig.xff(xffConfig); - return this; - } - - public Builder onBehalfOf(OnBehalfOfConfig onBehalfOfConfig){ - testSecurityConfig.onBehalfOf(onBehalfOfConfig); - return this; - } - - public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { - this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; - return this; - } - public Builder certificates(TestCertificates certificates) { - this.testCertificates = certificates; - return this; - } - - public Builder doNotFailOnForbidden(boolean doNotFailOnForbidden) { - testSecurityConfig.doNotFailOnForbidden(doNotFailOnForbidden); - return this; - } - - public Builder defaultConfigurationInitDirectory(String defaultConfigurationInitDirectory){ - this.defaultConfigurationInitDirectory = defaultConfigurationInitDirectory; - return this; - } - - public LocalCluster build() { - try { - if(testCertificates == null) { - testCertificates = new TestCertificates(); - } - clusterName += "_" + num.incrementAndGet(); - Settings settings = nodeOverrideSettingsBuilder.build(); - return new LocalCluster(clusterName, testSecurityConfig, sslOnly, settings, clusterManager, plugins, testCertificates, - clusterDependencies, remoteClusters, testIndices, loadConfigurationIntoIndex, defaultConfigurationInitDirectory); - } catch (Exception e) { - log.error("Failed to build LocalCluster", e); - throw new RuntimeException(e); - } - } - - } - - @Override - public TestCertificates getTestCertificates() { - return testCertificates; - } + private static final Logger log = LogManager.getLogger(LocalCluster.class); + + public static final String INIT_CONFIGURATION_DIR = "security.default_init.dir"; + + protected static final AtomicLong num = new AtomicLong(); + + private boolean sslOnly; + + private final List> plugins; + private final ClusterManager clusterManager; + private final TestSecurityConfig testSecurityConfig; + private Settings nodeOverride; + private final String clusterName; + private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; + private final TestCertificates testCertificates; + private final List clusterDependencies; + private final Map remotes; + private volatile LocalOpenSearchCluster localOpenSearchCluster; + private final List testIndices; + + private boolean loadConfigurationIntoIndex; + + private LocalCluster( + String clusterName, + TestSecurityConfig testSgConfig, + boolean sslOnly, + Settings nodeOverride, + ClusterManager clusterManager, + List> plugins, + TestCertificates testCertificates, + List clusterDependencies, + Map remotes, + List testIndices, + boolean loadConfigurationIntoIndex, + String defaultConfigurationInitDirectory + ) { + this.plugins = plugins; + this.testCertificates = testCertificates; + this.clusterManager = clusterManager; + this.testSecurityConfig = testSgConfig; + this.sslOnly = sslOnly; + this.nodeOverride = nodeOverride; + this.clusterName = clusterName; + this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); + this.remotes = remotes; + this.clusterDependencies = clusterDependencies; + this.testIndices = testIndices; + this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; + if (StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { + System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); + } + } + + public String getSnapshotDirPath() { + return localOpenSearchCluster.getSnapshotDirPath(); + } + + @Override + public void before() throws Throwable { + if (localOpenSearchCluster == null) { + for (LocalCluster dependency : clusterDependencies) { + if (!dependency.isStarted()) { + dependency.before(); + } + } + + for (Map.Entry entry : remotes.entrySet()) { + @SuppressWarnings("resource") + InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress(); + String key = "cluster.remote." + entry.getKey() + ".seeds"; + String value = transportAddress.getHostString() + ":" + transportAddress.getPort(); + log.info("Remote cluster '{}' added to configuration with the following seed '{}'", key, value); + nodeOverride = Settings.builder().put(nodeOverride).putList(key, value).build(); + } + start(); + } + } + + @Override + protected void after() { + System.clearProperty(INIT_CONFIGURATION_DIR); + close(); + } + + @Override + public void close() { + if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { + try { + localOpenSearchCluster.destroy(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + localOpenSearchCluster = null; + } + } + } + + @Override + public String getClusterName() { + return clusterName; + } + + @Override + public InetSocketAddress getHttpAddress() { + return localOpenSearchCluster.clientNode().getHttpAddress(); + } + + public int getHttpPort() { + return getHttpAddress().getPort(); + } + + @Override + public InetSocketAddress getTransportAddress() { + return localOpenSearchCluster.clientNode().getTransportAddress(); + } + + /** + * Returns a Client object that performs cluster-internal requests. As these requests are regard as cluster-internal, + * no authentication is performed and no user-information is attached to these requests. Thus, this client should + * be only used for preparing test environments, but not as a test subject. + */ + public Client getInternalNodeClient() { + return localOpenSearchCluster.clientNode().getInternalNodeClient(); + } + + /** + * Returns a random node of this cluster. + */ + public PluginAwareNode node() { + return this.localOpenSearchCluster.clusterManagerNode().esNode(); + } + + /** + * Returns all nodes of this cluster. + */ + public List nodes() { + return this.localOpenSearchCluster.getNodes(); + } + + public LocalOpenSearchCluster.Node getNodeByName(String name) { + return this.localOpenSearchCluster.getNodeByName(name); + } + + public boolean isStarted() { + return localOpenSearchCluster != null; + } + + public List getConfiguredUsers() { + return testSecurityConfig.getUsers(); + } + + public Random getRandom() { + return localOpenSearchCluster.getRandom(); + } + + private void start() { + try { + NodeSettingsSupplier nodeSettingsSupplier = minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings( + sslOnly, + nodeOverride + ); + localOpenSearchCluster = new LocalOpenSearchCluster( + clusterName, + clusterManager, + nodeSettingsSupplier, + plugins, + testCertificates + ); + + localOpenSearchCluster.start(); + + if (loadConfigurationIntoIndex) { + initSecurityIndex(testSecurityConfig); + } + + try (Client client = getInternalNodeClient()) { + for (TestIndex index : this.testIndices) { + index.create(client); + } + } + + } catch (Exception e) { + log.error("Local ES cluster start failed", e); + throw new RuntimeException(e); + } + } + + private void initSecurityIndex(TestSecurityConfig testSecurityConfig) { + log.info("Initializing OpenSearch Security index"); + try ( + Client client = new ContextHeaderDecoratorClient( + this.getInternalNodeClient(), + Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true") + ) + ) { + testSecurityConfig.initIndex(client); + triggerConfigurationReload(client); + } + } + + public void updateUserConfiguration(List users) { + try ( + Client client = new ContextHeaderDecoratorClient( + this.getInternalNodeClient(), + Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true") + ) + ) { + testSecurityConfig.updateInternalUsersConfiguration(client, users); + triggerConfigurationReload(client); + } + } + + private static void triggerConfigurationReload(Client client) { + ConfigUpdateResponse configUpdateResponse = client.execute( + ConfigUpdateAction.INSTANCE, + new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])) + ).actionGet(); + if (configUpdateResponse.hasFailures()) { + throw new RuntimeException("ConfigUpdateResponse produced failures: " + configUpdateResponse.failures()); + } + } + + public CertificateData getAdminCertificate() { + return testCertificates.getAdminCertificateData(); + } + + public static class Builder { + + private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); + + private boolean sslOnly = false; + private final List> plugins = new ArrayList<>(); + private Map remoteClusters = new HashMap<>(); + private List clusterDependencies = new ArrayList<>(); + private List testIndices = new ArrayList<>(); + private ClusterManager clusterManager = ClusterManager.DEFAULT; + private TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); + private String clusterName = "local_cluster"; + private TestCertificates testCertificates; + + private boolean loadConfigurationIntoIndex = true; + + private String defaultConfigurationInitDirectory = null; + + public Builder() {} + + public Builder dependsOn(Object object) { + // We just want to make sure that the object is already done + if (object == null) { + throw new IllegalStateException("Dependency not fulfilled"); + } + return this; + } + + public Builder clusterManager(ClusterManager clusterManager) { + this.clusterManager = clusterManager; + return this; + } + + /** + * Starts a cluster with only one node and thus saves some resources during startup. This shall be only used + * for tests where the node interactions are not relevant to the test. An example for this would be + * authentication tests, as authentication is always done on the directly connected node. + */ + public Builder singleNode() { + this.clusterManager = ClusterManager.SINGLENODE; + return this; + } + + /** + * Specifies the configuration of the security plugin that shall be used by this cluster. + */ + public Builder config(TestSecurityConfig testSecurityConfig) { + this.testSecurityConfig = testSecurityConfig; + return this; + } + + public Builder sslOnly(boolean sslOnly) { + this.sslOnly = sslOnly; + return this; + } + + public Builder nodeSettings(Map settings) { + settings.forEach((key, value) -> { + if (value instanceof List) { + List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); + nodeOverrideSettingsBuilder.putList(key, values); + } else { + nodeOverrideSettingsBuilder.put(key, String.valueOf(value)); + } + }); + + return this; + } + + /** + * Adds additional plugins to the cluster + */ + public Builder plugin(Class plugin) { + this.plugins.add(plugin); + + return this; + } + + public Builder authFailureListeners(AuthFailureListeners listener) { + testSecurityConfig.authFailureListeners(listener); + return this; + } + + /** + * Specifies a remote cluster and its name. The remote cluster can be then used in Cross Cluster Search + * operations with the specified name. + */ + public Builder remote(String name, LocalCluster anotherCluster) { + remoteClusters.put(name, anotherCluster); + + clusterDependencies.add(anotherCluster); + + return this; + } + + /** + * Specifies test indices that shall be created upon startup of the cluster. + */ + public Builder indices(TestIndex... indices) { + this.testIndices.addAll(Arrays.asList(indices)); + return this; + } + + public Builder users(TestSecurityConfig.User... users) { + for (TestSecurityConfig.User user : users) { + testSecurityConfig.user(user); + } + return this; + } + + public Builder audit(AuditConfiguration auditConfiguration) { + if (auditConfiguration != null) { + testSecurityConfig.audit(auditConfiguration); + } + if (auditConfiguration.isEnabled()) { + nodeOverrideSettingsBuilder.put("plugins.security.audit.type", TestRuleAuditLogSink.class.getName()); + } else { + nodeOverrideSettingsBuilder.put("plugins.security.audit.type", "noop"); + } + return this; + } + + public List getUsers() { + return testSecurityConfig.getUsers(); + } + + public Builder roles(Role... roles) { + testSecurityConfig.roles(roles); + return this; + } + + public Builder rolesMapping(RolesMapping... mappings) { + testSecurityConfig.rolesMapping(mappings); + return this; + } + + public Builder authc(TestSecurityConfig.AuthcDomain authc) { + testSecurityConfig.authc(authc); + return this; + } + + public Builder authz(AuthzDomain authzDomain) { + testSecurityConfig.authz(authzDomain); + return this; + } + + public Builder clusterName(String clusterName) { + this.clusterName = clusterName; + return this; + } + + public Builder configIndexName(String configIndexName) { + testSecurityConfig.configIndexName(configIndexName); + return this; + } + + public Builder testCertificates(TestCertificates certificates) { + this.testCertificates = certificates; + return this; + } + + public Builder anonymousAuth(boolean anonAuthEnabled) { + testSecurityConfig.anonymousAuth(anonAuthEnabled); + return this; + } + + public Builder xff(XffConfig xffConfig) { + testSecurityConfig.xff(xffConfig); + return this; + } + + public Builder onBehalfOf(OnBehalfOfConfig onBehalfOfConfig) { + testSecurityConfig.onBehalfOf(onBehalfOfConfig); + return this; + } + + public Builder loadConfigurationIntoIndex(boolean loadConfigurationIntoIndex) { + this.loadConfigurationIntoIndex = loadConfigurationIntoIndex; + return this; + } + + public Builder certificates(TestCertificates certificates) { + this.testCertificates = certificates; + return this; + } + + public Builder doNotFailOnForbidden(boolean doNotFailOnForbidden) { + testSecurityConfig.doNotFailOnForbidden(doNotFailOnForbidden); + return this; + } + + public Builder defaultConfigurationInitDirectory(String defaultConfigurationInitDirectory) { + this.defaultConfigurationInitDirectory = defaultConfigurationInitDirectory; + return this; + } + + public LocalCluster build() { + try { + if (testCertificates == null) { + testCertificates = new TestCertificates(); + } + clusterName += "_" + num.incrementAndGet(); + Settings settings = nodeOverrideSettingsBuilder.build(); + return new LocalCluster( + clusterName, + testSecurityConfig, + sslOnly, + settings, + clusterManager, + plugins, + testCertificates, + clusterDependencies, + remoteClusters, + testIndices, + loadConfigurationIntoIndex, + defaultConfigurationInitDirectory + ); + } catch (Exception e) { + log.error("Failed to build LocalCluster", e); + throw new RuntimeException(e); + } + } + + } + + @Override + public TestCertificates getTestCertificates() { + return testCertificates; + } } diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index a7106845aa..920a4ffd4c 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -1,35 +1,5 @@ package org.opensearch.security.action.onbehalf; -import java.io.IOException; -import java.util.List; - -import org.apache.cxf.rs.security.jose.jwt.JwtToken; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.greenrobot.eventbus.Subscribe; -import org.opensearch.action.FailedNodeException; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.nodes.TransportNodesAction; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.inject.Provider; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.transport.TransportAddress; -import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.authtoken.jwt.JwtVendor; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigFactory; -import org.opensearch.security.securityconf.DynamicConfigModel; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.TransportRequest; -import org.opensearch.transport.TransportService; -import org.opensearch.rest.BaseRestHandler; - import java.io.IOException; import java.nio.file.Path; import java.security.cert.X509Certificate; @@ -44,14 +14,26 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.greenrobot.eventbus.Subscribe; +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.nodes.TransportNodesAction; import org.opensearch.client.Client; +import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.inject.Provider; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.TransportAddress; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; @@ -59,19 +41,25 @@ import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestStatus; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.SecurityKeyStore; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.client.node.NodeClient; import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportRequest; +import org.opensearch.transport.TransportService; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index c0e2429948..2aa343014a 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -44,7 +44,7 @@ private static Cipher createCipherFromSecret(final String secret, final CipherMo throw new RuntimeException("Error creating cipher from secret in mode " + mode.name()); } } - + private static byte[] createCipherText(final Cipher cipher, final byte[] data) { try { return cipher.doFinal(data); 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 4b18c6e3ca..5730904eaa 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -12,10 +12,10 @@ package org.opensearch.security.authtoken.jwt; import java.time.Instant; -import java.util.Optional; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.LongSupplier; diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 2bdb78088f..e3f116f241 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -21,8 +21,8 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.Map.Entry; +import java.util.Objects; import java.util.regex.Pattern; import java.util.stream.Collectors; 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 c65b92431a..1a1a0a98e6 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -11,8 +11,6 @@ package org.opensearch.security.authtoken.jwt; -import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.function.LongSupplier; @@ -26,8 +24,6 @@ import org.opensearch.common.settings.Settings; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class JwtVendorTest { @Test @@ -87,7 +83,7 @@ public void testCreateJwtWithBadExpiry() throws Exception { Integer expirySeconds = -300; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); + Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); @@ -101,7 +97,7 @@ public void testCreateJwtWithBadEncryptionKey() throws Exception { List roles = List.of("admin"); Integer expirySeconds = 300; - Settings settings = Settings.builder().put("signing_key", "abc123").build(); + Settings settings = Settings.builder().put("signing_key", "abc123").build(); JwtVendor jwtVendor = new JwtVendor(settings, Optional.empty()); jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, List.of()); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java index ae5bb7280e..a3a267f7f7 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java @@ -75,8 +75,6 @@ public void testGetAccount() throws Exception { assertNotNull(body.getAsSettings("tenants")); assertNotNull(body.getAsList("roles")); - response = rh.executePostRequest(getEndpointPrefix() + "/api/user/onbehalfof", "{\"reason\":\"Test generation\"}", encodeBasicHeader(testUser, testPass)); - System.out.println("This is the response body: " + response.getBody()); } @Test @@ -194,15 +192,15 @@ public void testPutAccountRetainsAccountInformation() throws Exception { final String testPassword = "test-password"; final String newPassword = "new-password"; final String createInternalUserPayload = "{\n" - + " \"password\": \"" - + testPassword - + "\",\n" - + " \"backend_roles\": [\"test-backend-role-1\"],\n" - + " \"opendistro_security_roles\": [\"opendistro_security_all_access\"],\n" - + " \"attributes\": {\n" - + " \"attribute1\": \"value1\"\n" - + " }\n" - + "}"; + + " \"password\": \"" + + testPassword + + "\",\n" + + " \"backend_roles\": [\"test-backend-role-1\"],\n" + + " \"opendistro_security_roles\": [\"opendistro_security_all_access\"],\n" + + " \"attributes\": {\n" + + " \"attribute1\": \"value1\"\n" + + " }\n" + + "}"; final String changePasswordPayload = "{\"password\":\"" + newPassword + "\", \"current_password\":\"" + testPassword + "\"}"; final String internalUserEndpoint = BASE_ENDPOINT + "internalusers/" + testUsername; @@ -221,9 +219,9 @@ public void testPutAccountRetainsAccountInformation() throws Exception { response = rh.executeGetRequest(internalUserEndpoint); assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Settings responseBody = Settings.builder() - .loadFromSource(response.getBody(), XContentType.JSON) - .build() - .getAsSettings(testUsername); + .loadFromSource(response.getBody(), XContentType.JSON) + .build() + .getAsSettings(testUsername); assertTrue(responseBody.getAsList("backend_roles").contains("test-backend-role-1")); assertTrue(responseBody.getAsList("opendistro_security_roles").contains("opendistro_security_all_access")); assertEquals(responseBody.getAsSettings("attributes").get("attribute1"), "value1"); diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index 38a66a4fde..fc93acc1f8 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -26,7 +26,6 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import io.netty.handler.codec.base64.Base64Decoder; import org.apache.commons.lang3.RandomStringUtils; import org.apache.hc.core5.http.HttpHeaders; import org.junit.Assert; @@ -39,7 +38,8 @@ public class OnBehalfOfAuthenticatorTest { final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - final static String signingKey = "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; + final static String signingKey = + "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); final static SecretKey secretKey = Keys.hmacShaKeyFor(signingKeyB64Encoded.getBytes(StandardCharsets.UTF_8)); @@ -48,10 +48,10 @@ public void testNoKey() throws Exception { try { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - null, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false ); Assert.fail("Expected a RuntimeException"); } catch (RuntimeException e) { @@ -64,10 +64,10 @@ public void testEmptyKey() throws Exception { try { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - null, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + null, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false ); Assert.fail("Expected a RuntimeException"); } catch (RuntimeException e) { @@ -80,10 +80,10 @@ public void testBadKey() throws Exception { try { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy"), - false + BaseEncoding.base64().encode(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy"), + false ); Assert.fail("Expected a WeakKeyException"); } catch (RuntimeException e) { @@ -119,10 +119,10 @@ public void testInvalid() throws Exception { public void testBearer() throws Exception { String jwsToken = Jwts.builder() - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) - .compact(); + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .compact(); OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); @@ -140,7 +140,11 @@ public void testBearer() throws Exception { @Test public void testBearerWrongPosition() throws Exception { - String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + String jwsToken = Jwts.builder() + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = new HashMap(); @@ -153,7 +157,11 @@ public void testBearerWrongPosition() throws Exception { @Test public void testBasicAuthHeader() throws Exception { - String jwsToken = Jwts.builder().setSubject("Leonard McCoy").setAudience("ext_0").signWith(secretKey, SignatureAlgorithm.HS512).compact(); + String jwsToken = Jwts.builder() + .setSubject("Leonard McCoy") + .setAudience("ext_0") + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact(); OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings()); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); @@ -167,10 +175,10 @@ public void testRoles() throws Exception { List roles = List.of("IT", "HR"); final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2").setAudience("svc1"), - true + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", "role1,role2").setAudience("svc1"), + true ); Assert.assertNotNull(credentials); @@ -183,10 +191,10 @@ public void testRoles() throws Exception { public void testNullClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", null).setAudience("svc1"), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", null).setAudience("svc1"), + false ); Assert.assertNotNull(credentials); @@ -198,10 +206,10 @@ public void testNullClaim() throws Exception { public void testNonStringClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L).setAudience("svc1"), - true + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").claim("dr", 123L).setAudience("svc1"), + true ); Assert.assertNotNull(credentials); @@ -214,10 +222,10 @@ public void testNonStringClaim() throws Exception { public void testRolesMissing() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setSubject("Leonard McCoy").setAudience("svc1"), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Leonard McCoy").setAudience("svc1"), + false ); Assert.assertNotNull(credentials); @@ -230,10 +238,10 @@ public void testRolesMissing() throws Exception { public void testWrongSubjectKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who").setAudience("svc1"), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().claim("roles", "role1,role2").claim("asub", "Dr. Who").setAudience("svc1"), + false ); Assert.assertNull(credentials); @@ -243,10 +251,10 @@ public void testWrongSubjectKey() throws Exception { public void testExp() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setExpiration(new Date(100)), + false ); Assert.assertNull(credentials); @@ -256,10 +264,10 @@ public void testExp() throws Exception { public void testNbf() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( - signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), - false + signingKeyB64Encoded, + claimsEncryptionKey, + Jwts.builder().setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), + false ); Assert.assertNull(credentials); @@ -269,7 +277,7 @@ public void testNbf() throws Exception { public void testRolesArray() throws Exception { JwtBuilder builder = Jwts.builder() - .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); + .setPayload("{" + "\"sub\": \"Cluster_0\"," + "\"aud\": \"ext_0\"," + "\"dr\": \"a,b,3rd\"" + "}"); final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKeyB64Encoded, claimsEncryptionKey, builder, true); @@ -283,25 +291,24 @@ public void testRolesArray() throws Exception { /** extracts a default user credential from a request header */ private AuthCredentials extractCredentialsFromJwtHeader( - final String signingKeyB64Encoded, - final String encryptionKey, - final JwtBuilder jwtBuilder, - final Boolean bwcPluginCompatibilityMode + final String signingKeyB64Encoded, + final String encryptionKey, + final JwtBuilder jwtBuilder, + final Boolean bwcPluginCompatibilityMode ) { - final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(Settings.builder() - .put("signing_key", signingKeyB64Encoded) - .put("encryption_key", encryptionKey) - .build()); + final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator( + Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", encryptionKey).build() + ); - final String jwsToken = jwtBuilder.signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512).compact(); + final String jwsToken = jwtBuilder.signWith( + Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), + SignatureAlgorithm.HS512 + ).compact(); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null); } private Settings defaultSettings() { - return Settings.builder() - .put("signing_key", signingKeyB64Encoded) - .put("encryption_key", claimsEncryptionKey) - .build(); + return Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); } } From 0ed28b571afbd5c5d0384186c184a4c67961dd9b Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:11:41 -0700 Subject: [PATCH 30/74] Fix Code Hygiene, remove unused imports Signed-off-by: Ryan Liang --- .../security/OpenSearchSecurityPlugin.java | 1 - .../onbehalf/CreateOnBehalfOfToken.java | 34 ------------------- .../security/authtoken/jwt/JwtVendor.java | 8 ----- 3 files changed, 43 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 278112718b..3356f89ee0 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -142,7 +142,6 @@ import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index 920a4ffd4c..62ebbb392f 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -1,65 +1,31 @@ package org.opensearch.security.action.onbehalf; import java.io.IOException; -import java.nio.file.Path; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.LongSupplier; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import org.apache.cxf.rs.security.jose.jwt.JwtToken; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.greenrobot.eventbus.Subscribe; -import org.opensearch.action.FailedNodeException; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.nodes.TransportNodesAction; -import org.opensearch.client.Client; import org.opensearch.client.node.NodeClient; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.bytes.BytesReference; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.inject.Provider; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; import org.opensearch.common.settings.Settings; import org.opensearch.common.transport.TransportAddress; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestStatus; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.auth.BackendRegistry; import org.opensearch.security.authtoken.jwt.JwtVendor; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.DynamicConfigModel; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.transport.PrincipalExtractor; -import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.TransportRequest; -import org.opensearch.transport.TransportService; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; 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 5730904eaa..a494e0db9a 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -12,11 +12,8 @@ package org.opensearch.security.authtoken.jwt; import java.time.Instant; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.function.LongSupplier; import com.google.common.base.Strings; @@ -29,16 +26,11 @@ import org.apache.cxf.rs.security.jose.jwt.JwtClaims; import org.apache.cxf.rs.security.jose.jwt.JwtToken; import org.apache.cxf.rs.security.jose.jwt.JwtUtils; -import org.apache.kafka.common.utils.SystemTime; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.common.settings.Settings; -import org.opensearch.common.transport.TransportAddress; import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; public class JwtVendor { private static final Logger logger = LogManager.getLogger(JwtVendor.class); From 26670ac4d8e1fbd81974d80e01ab6ef01b3ff5d3 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:17:19 -0700 Subject: [PATCH 31/74] Remove some stale TODOs Signed-off-by: Ryan Liang --- .../java/org/opensearch/security/authtoken/jwt/JwtVendor.java | 4 ---- .../org/opensearch/security/http/OnBehalfOfAuthenticator.java | 3 --- 2 files changed, 7 deletions(-) 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 a494e0db9a..80ffd8771c 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -42,8 +42,6 @@ public class JwtVendor { private final JoseJwtProducer jwtProducer; private final LongSupplier timeProvider; - //TODO: Relocate/Remove them at once we make the descisions about the `roles` - private ConfigModel configModel; // This never gets assigned, how does this work at all? public JwtVendor(final Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); @@ -63,7 +61,6 @@ public JwtVendor(final Settings settings, final Optional timeProvi } else { this.timeProvider = () -> System.currentTimeMillis() / 1000; } - this.configModel = null; } /* @@ -131,7 +128,6 @@ public String createJwt(String issuer, String subject, String audience, Integer throw new Exception("The expiration time should be a positive integer"); } - //TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr` if (roles != null) { String listOfRoles = String.join(",", roles); jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index e3f116f241..b647d81a64 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -113,7 +113,6 @@ private List extractSecurityRolesFromClaims(Claims claims) { // Extracting roles based on the compatbility mode String decryptedRoles = rolesClaim; if (rolesObject == claims.get("er")) { - //TODO: WHERE TO GET THE ENCRYTION KEY decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); } roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).collect(Collectors.toList()); @@ -123,7 +122,6 @@ private List extractSecurityRolesFromClaims(Claims claims) { } private String[] extractBackendRolesFromClaims(Claims claims) { - //TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr Object backendRolesObject = ObjectUtils.firstNonNull(claims.get("ebr"), claims.get("dbr")); String[] backendRoles; @@ -137,7 +135,6 @@ private String[] extractBackendRolesFromClaims(Claims claims) { // Extracting roles based on the compatibility mode String decryptedBackendRoles = backendRolesClaim; if (backendRolesObject == claims.get("ebr")) { - //TODO: WHERE TO GET THE ENCRYTION KEY decryptedBackendRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, backendRolesClaim); } backendRoles = Arrays.stream(decryptedBackendRoles.split(",")).map(String::trim).toArray(String[]::new); From 12bd3e8fc805ec40d4a2f646ee6bc8284dc8ccf3 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:35:30 -0700 Subject: [PATCH 32/74] Change the * imports into seprate ones in OBOAuthenticationTest Signed-off-by: Ryan Liang --- .../security/http/OnBehalfOfJwtAuthenticationTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 7cd130873f..9d687f9c01 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -19,7 +19,10 @@ import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +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.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; From 0e590bac86ead6ff04da1285826be1d1bb561356 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:41:08 -0700 Subject: [PATCH 33/74] Remove another unused import in jwtVendor Signed-off-by: Ryan Liang --- .../java/org/opensearch/security/authtoken/jwt/JwtVendor.java | 1 - 1 file changed, 1 deletion(-) 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 80ffd8771c..9a60f36c76 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -30,7 +30,6 @@ import org.apache.logging.log4j.Logger; import org.opensearch.common.settings.Settings; -import org.opensearch.security.securityconf.ConfigModel; public class JwtVendor { private static final Logger logger = LogManager.getLogger(JwtVendor.class); From d1341e25a7bc9d0cdb87dfcd4bf6c23df089feb5 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:49:43 -0700 Subject: [PATCH 34/74] Add license header for CreateOnBehalfOfToken + OnBehalfOfJwtAuthenticationTest Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 11 +++++++++++ .../action/onbehalf/CreateOnBehalfOfToken.java | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 9d687f9c01..24226d20e9 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -1,3 +1,14 @@ +/* + * 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 java.nio.charset.StandardCharsets; diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index 62ebbb392f..ccf801d8cb 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -1,3 +1,14 @@ +/* + * 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.action.onbehalf; import java.io.IOException; From 9e832cf0a8ab8097e8298152f7d55b9c02e9d282 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 19 Jun 2023 15:59:47 -0700 Subject: [PATCH 35/74] Remove wording "extension" in OnBehalfOfJwtAuthenticationTest's testing signingKey Signed-off-by: Ryan Liang --- .../security/http/OnBehalfOfJwtAuthenticationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 24226d20e9..c56b1b5d2b 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -54,7 +54,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final List roles = List.of("admin", "HR"); public static final List backendRoles = List.of("IT"); - private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of extensions".getBytes(StandardCharsets.UTF_8)); + private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of OBO authentication".getBytes(StandardCharsets.UTF_8)); private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); private static final OnBehalfOfJwtAuthorizationHeaderFactory tokenFactory = new OnBehalfOfJwtAuthorizationHeaderFactory( From 626c8530840a49025a03381dcf226a30a66f1f07 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 16:08:37 -0700 Subject: [PATCH 36/74] Trim the obo integ test body Signed-off-by: Ryan Liang --- .../security/http/OnBehalfOfJwtAuthenticationTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index c56b1b5d2b..cd1926a194 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -72,6 +72,7 @@ public class OnBehalfOfJwtAuthenticationTest { public static final String ADMIN_USER_NAME = "admin"; public static final String DEFAULT_PASSWORD = "secret"; public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; + public static final String OBO_ENDPOINT_PREFIX = "_plugins/_security/api/user/onbehalfof"; @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder() @@ -87,7 +88,6 @@ public class OnBehalfOfJwtAuthenticationTest { @Test public void shouldAuthenticateWithOBOToken() { - // TODO: This integration test should use an endpoint to get an OnBehalfOf token, not generate it try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ TestRestClient.HttpResponse response = client.getAuthInfo(); @@ -102,13 +102,13 @@ public void shouldAuthenticateWithOBOToken() { @Test public void shouldAuthenticateWithOBOTokenEndPoint() { - //Header contentTypeHeader = new BasicHeader(headerNameContentType, "json"); Header adminOboAuthHeader; + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { client.assertCorrectCredentials(ADMIN_USER_NAME); - TestRestClient.HttpResponse response = client.postJson("_plugins/_security/api/user/onbehalfof", OBO_TOKEN_REASON); + TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); response.assertStatusCode(200); Map oboEndPointResponse = response.getBodyAs(Map.class); @@ -119,6 +119,7 @@ public void shouldAuthenticateWithOBOTokenEndPoint() { hasKey("duration"))); String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); + System.out.println("This is the OBO Token of admin user: " + encodedOboTokenStr); adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + encodedOboTokenStr); } From e755753187ea9b669e3fe7e8525734b664e25bc1 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 16:18:18 -0700 Subject: [PATCH 37/74] Switch to java base64 decoder in OBOAuthenticator L75 Signed-off-by: Ryan Liang --- .../org/opensearch/security/http/OnBehalfOfAuthenticator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index b647d81a64..e8cf0a82ac 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -20,6 +20,7 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Map.Entry; import java.util.Objects; @@ -29,7 +30,6 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.WeakKeyException; import org.apache.commons.lang3.ObjectUtils; import org.apache.hc.core5.http.HttpHeaders; @@ -72,7 +72,7 @@ private JwtParser initParser(final String signingKey) { .replace("-----BEGIN PUBLIC KEY-----\n", "") .replace("-----END PUBLIC KEY-----", ""); - final byte[] decoded = Decoders.BASE64.decode(minmalKeyFormat); + final byte[] decoded = Base64.getDecoder().decode(minmalKeyFormat); Key key = null; try { From 68af23a611c3c4be86fa16d046fe58a626088e0f Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 16:29:24 -0700 Subject: [PATCH 38/74] Add stack trace to see the error of base64 decoder Signed-off-by: Ryan Liang --- .../org/opensearch/security/http/OnBehalfOfAuthenticator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index e8cf0a82ac..2e1b6236ae 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -95,6 +95,7 @@ private JwtParser initParser(final String signingKey) { return Jwts.parser().setSigningKey(signingKey); } catch (Throwable e) { log.error("Error while creating JWT authenticator", e); + e.printStackTrace(); throw new RuntimeException(e); } } From 66b52e98087765c2f7d7b7a7955cdb030bba23a9 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 16:32:21 -0700 Subject: [PATCH 39/74] Fix the formatting in DynamicConfigModelV7 Signed-off-by: Ryan Liang --- .../opensearch/security/securityconf/DynamicConfigModelV7.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index b84ddf9a40..8700667b8e 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -316,7 +316,8 @@ private void buildAAA() { Settings oboSettings = getDynamicOnBehalfOfSettings(); if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { - final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings()), false, -1); restAuthDomains0.add(_ad); + final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings()), false, -1); + restAuthDomains0.add(_ad); } List originalDestroyableComponents = destroyableComponents; From 983267b31a1f7097cca8b55d5b9c77affc0a4f2e Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 16:45:09 -0700 Subject: [PATCH 40/74] Fix the typo of minimalKeyFormat + adding a debugging statement to check the signing_key. Signed-off-by: Ryan Liang --- .../opensearch/security/http/OnBehalfOfAuthenticator.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 2e1b6236ae..d5ea297f6d 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -68,11 +68,13 @@ private JwtParser initParser(final String signingKey) { } try { - final String minmalKeyFormat = signingKey + final String minimalKeyFormat = signingKey .replace("-----BEGIN PUBLIC KEY-----\n", "") .replace("-----END PUBLIC KEY-----", ""); - final byte[] decoded = Base64.getDecoder().decode(minmalKeyFormat); + System.out.println("THIS IS THE minimalKeyFormat of signingKey: " + minimalKeyFormat); + + final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); Key key = null; try { From 4999a58bf3ec4ffa13972cd14278baf1e98e18ba Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 17:09:43 -0700 Subject: [PATCH 41/74] Fix the signingKeys + encryptionKeys in src/integrationTest/resources/config.yml Signed-off-by: Ryan Liang --- src/integrationTest/resources/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml index 1fbea10e28..737cfece61 100644 --- a/src/integrationTest/resources/config.yml +++ b/src/integrationTest/resources/config.yml @@ -16,5 +16,5 @@ config: type: "internal" config: {} on_behalf_of: - signing_key: "signing key" - encryption_key: "encryption key" + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" From d95ad8bce2a38fdca6de7e6e406328dd5701e0fc Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 17:19:13 -0700 Subject: [PATCH 42/74] Fix the signingKeys + encryptionKeys in src/test/resources/config.yml + src/test/resources/restapi/securityconfig_nondefault.json Signed-off-by: Ryan Liang --- src/test/resources/config.yml | 4 ++-- src/test/resources/restapi/securityconfig_nondefault.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml index c4bb432125..794c7527d7 100644 --- a/src/test/resources/config.yml +++ b/src/test/resources/config.yml @@ -97,5 +97,5 @@ config: hosts_resolver_mode: "ip-only" transport_userrname_attribute: null on_behalf_of: - signing_key: "signing key" - encryption_key: "encryption key" + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index e30ca9148b..77974080dc 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -172,8 +172,8 @@ "hosts_resolver_mode" : "ip-only", "do_not_fail_on_forbidden_empty" : false, "on_behalf_of": { - "signing_key": "signing key", - "encryption_key": "encryption key" + "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + "encryption_key": "ZW5jcnlwdGlvbktleQ==" } } } From dcec4e97858419e52c2b7504f68409d4c9253669 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Tue, 20 Jun 2023 18:24:30 -0700 Subject: [PATCH 43/74] Remove debugging statements and add obo setting in src/test/resources/restapi/config.yml Signed-off-by: Ryan Liang --- .../org/opensearch/security/http/OnBehalfOfAuthenticator.java | 3 --- src/test/resources/restapi/config.yml | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index d5ea297f6d..ccb6c917a9 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -72,8 +72,6 @@ private JwtParser initParser(final String signingKey) { .replace("-----BEGIN PUBLIC KEY-----\n", "") .replace("-----END PUBLIC KEY-----", ""); - System.out.println("THIS IS THE minimalKeyFormat of signingKey: " + minimalKeyFormat); - final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); Key key = null; @@ -97,7 +95,6 @@ private JwtParser initParser(final String signingKey) { return Jwts.parser().setSigningKey(signingKey); } catch (Throwable e) { log.error("Error while creating JWT authenticator", e); - e.printStackTrace(); throw new RuntimeException(e); } } diff --git a/src/test/resources/restapi/config.yml b/src/test/resources/restapi/config.yml index 2ed865657a..763176dbf5 100644 --- a/src/test/resources/restapi/config.yml +++ b/src/test/resources/restapi/config.yml @@ -94,3 +94,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" From cd2eac62606cdc36f434db1075ca4e5682e84f55 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 21 Jun 2023 09:34:05 -0700 Subject: [PATCH 44/74] Add a debug statement to print dcm.getobosettings Signed-off-by: Ryan Liang --- .../security/action/onbehalf/CreateOnBehalfOfToken.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index ccf801d8cb..d7775b9d9d 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -57,6 +57,7 @@ public void onConfigModelChanged(ConfigModel configModel) { @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; + System.out.println("Hello this is the obo setting: " + dcm.getDynamicOnBehalfOfSettings()); this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); } From be8362534bd74a47fd1776b43581b3930bfd3774 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 21 Jun 2023 11:11:28 -0700 Subject: [PATCH 45/74] Fix the missing comma in src/test/resources/restapi/securityconfig_nondefault.json Signed-off-by: Ryan Liang --- src/test/resources/restapi/securityconfig_nondefault.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index 77974080dc..a3f2a307d6 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -172,7 +172,7 @@ "hosts_resolver_mode" : "ip-only", "do_not_fail_on_forbidden_empty" : false, "on_behalf_of": { - "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", "encryption_key": "ZW5jcnlwdGlvbktleQ==" } } From 8836e8c2c573e3b5785c09761a46b079dd799d66 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 21 Jun 2023 11:45:35 -0700 Subject: [PATCH 46/74] Add obo setting to test config of src/test/resources/config_anon.yml Signed-off-by: Ryan Liang --- src/test/resources/config_anon.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/resources/config_anon.yml b/src/test/resources/config_anon.yml index 0c2c5ccbcc..35a52ba3b6 100644 --- a/src/test/resources/config_anon.yml +++ b/src/test/resources/config_anon.yml @@ -41,3 +41,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" From 817436d89ad1c0309d2ecdc12aa75d90fb7fd52b Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 21 Jun 2023 13:14:49 -0700 Subject: [PATCH 47/74] Add obo setting into multiple testing config files Signed-off-by: Ryan Liang --- src/test/resources/config_clientcert.yml | 3 +++ src/test/resources/config_multirolespan.yml | 3 +++ src/test/resources/config_proxy.yml | 3 +++ src/test/resources/config_proxy_custom.yml | 3 +++ src/test/resources/config_rest_impersonation.yml | 3 +++ src/test/resources/multitenancy/config_nodnfof.yml | 3 +++ src/test/resources/restapi/security_config.json | 6 +++++- src/test/resources/restapi/securityconfig.json | 2 ++ 8 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/test/resources/config_clientcert.yml b/src/test/resources/config_clientcert.yml index c50d770c26..ce55218ecd 100644 --- a/src/test/resources/config_clientcert.yml +++ b/src/test/resources/config_clientcert.yml @@ -41,3 +41,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_multirolespan.yml b/src/test/resources/config_multirolespan.yml index 388cba2903..e0d343d0dd 100644 --- a/src/test/resources/config_multirolespan.yml +++ b/src/test/resources/config_multirolespan.yml @@ -95,3 +95,6 @@ config: do_not_fail_on_forbidden: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_proxy.yml b/src/test/resources/config_proxy.yml index b3151d9748..ed485bbfaa 100644 --- a/src/test/resources/config_proxy.yml +++ b/src/test/resources/config_proxy.yml @@ -53,3 +53,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_proxy_custom.yml b/src/test/resources/config_proxy_custom.yml index cf03610e4b..ffdc133884 100644 --- a/src/test/resources/config_proxy_custom.yml +++ b/src/test/resources/config_proxy_custom.yml @@ -54,3 +54,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_rest_impersonation.yml b/src/test/resources/config_rest_impersonation.yml index a34232ff77..dd8e89e3bd 100644 --- a/src/test/resources/config_rest_impersonation.yml +++ b/src/test/resources/config_rest_impersonation.yml @@ -54,3 +54,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/multitenancy/config_nodnfof.yml b/src/test/resources/multitenancy/config_nodnfof.yml index f4afce87d4..674d5d2ed2 100644 --- a/src/test/resources/multitenancy/config_nodnfof.yml +++ b/src/test/resources/multitenancy/config_nodnfof.yml @@ -159,3 +159,6 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null + on_behalf_of: + signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" + encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/restapi/security_config.json b/src/test/resources/restapi/security_config.json index e5c09050cc..590c3b1f92 100644 --- a/src/test/resources/restapi/security_config.json +++ b/src/test/resources/restapi/security_config.json @@ -132,7 +132,11 @@ "do_not_fail_on_forbidden":false, "multi_rolespan_enabled":false, "hosts_resolver_mode":"ip-only", - "do_not_fail_on_forbidden_empty":false + "do_not_fail_on_forbidden_empty":false, + "on_behalf_of": { + "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", + "encryption_key": "ZW5jcnlwdGlvbktleQ==" + } } } diff --git a/src/test/resources/restapi/securityconfig.json b/src/test/resources/restapi/securityconfig.json index 13bc7c23a6..d0327b1b5a 100644 --- a/src/test/resources/restapi/securityconfig.json +++ b/src/test/resources/restapi/securityconfig.json @@ -155,6 +155,8 @@ "hosts_resolver_mode":"ip-only", "do_not_fail_on_forbidden_empty":false, "on_behalf_of": { + "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", + "encryption_key": "ZW5jcnlwdGlvbktleQ==" } } From 7cb5cb5b0f73d789ef5126208ada1aee4a67d5da Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 21 Jun 2023 13:50:13 -0700 Subject: [PATCH 48/74] Add checking for obo setting's presence Signed-off-by: Ryan Liang --- .../action/onbehalf/CreateOnBehalfOfToken.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index d7775b9d9d..10ed06bb1f 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -58,7 +58,11 @@ public void onConfigModelChanged(ConfigModel configModel) { public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; System.out.println("Hello this is the obo setting: " + dcm.getDynamicOnBehalfOfSettings()); - this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); + if (dcm.getDynamicOnBehalfOfSettings().get("signing_key") != null && dcm.getDynamicOnBehalfOfSettings().get("encryption_key") != null) { + this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); + } else { + this.vendor = null; + } } public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { @@ -96,6 +100,12 @@ public void accept(RestChannel channel) throws Exception { final XContentBuilder builder = channel.newBuilder(); BytesRestResponse response; try { + if (vendor == null) { + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "OBO Setting is not being configured"); + channel.sendResponse(response); + return; + } + final Map requestBody = request.contentOrSourceParamParser().map(); final String reason = (String)requestBody.getOrDefault("reason", null); From eb9f5a49bfa0241b0f5957485a6878a1e8eb5f3c Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 21 Jun 2023 14:48:25 -0700 Subject: [PATCH 49/74] Add checking for obo setting's presence with correct status type of service_unavailable Signed-off-by: Ryan Liang --- .../security/action/onbehalf/CreateOnBehalfOfToken.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index 10ed06bb1f..f245f5cdd3 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -101,8 +101,7 @@ public void accept(RestChannel channel) throws Exception { BytesRestResponse response; try { if (vendor == null) { - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "OBO Setting is not being configured"); - channel.sendResponse(response); + channel.sendResponse(new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "on_behalf_of configuration is not being configured")); return; } From d448af6f4382fa1a22b26cd65df7648f48fcd042 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 21 Jun 2023 15:19:17 -0700 Subject: [PATCH 50/74] Revert back those testing configs Signed-off-by: Ryan Liang --- .../security/action/onbehalf/CreateOnBehalfOfToken.java | 1 - src/test/resources/config_anon.yml | 3 --- src/test/resources/config_clientcert.yml | 3 --- src/test/resources/config_multirolespan.yml | 3 --- src/test/resources/config_proxy.yml | 3 --- src/test/resources/config_proxy_custom.yml | 3 --- src/test/resources/config_rest_impersonation.yml | 3 --- src/test/resources/multitenancy/config_nodnfof.yml | 3 --- src/test/resources/restapi/config.yml | 3 --- 9 files changed, 25 deletions(-) diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java index f245f5cdd3..3534589292 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java @@ -57,7 +57,6 @@ public void onConfigModelChanged(ConfigModel configModel) { @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; - System.out.println("Hello this is the obo setting: " + dcm.getDynamicOnBehalfOfSettings()); if (dcm.getDynamicOnBehalfOfSettings().get("signing_key") != null && dcm.getDynamicOnBehalfOfSettings().get("encryption_key") != null) { this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); } else { diff --git a/src/test/resources/config_anon.yml b/src/test/resources/config_anon.yml index 35a52ba3b6..0c2c5ccbcc 100644 --- a/src/test/resources/config_anon.yml +++ b/src/test/resources/config_anon.yml @@ -41,6 +41,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_clientcert.yml b/src/test/resources/config_clientcert.yml index ce55218ecd..c50d770c26 100644 --- a/src/test/resources/config_clientcert.yml +++ b/src/test/resources/config_clientcert.yml @@ -41,6 +41,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_multirolespan.yml b/src/test/resources/config_multirolespan.yml index e0d343d0dd..388cba2903 100644 --- a/src/test/resources/config_multirolespan.yml +++ b/src/test/resources/config_multirolespan.yml @@ -95,6 +95,3 @@ config: do_not_fail_on_forbidden: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_proxy.yml b/src/test/resources/config_proxy.yml index ed485bbfaa..b3151d9748 100644 --- a/src/test/resources/config_proxy.yml +++ b/src/test/resources/config_proxy.yml @@ -53,6 +53,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_proxy_custom.yml b/src/test/resources/config_proxy_custom.yml index ffdc133884..cf03610e4b 100644 --- a/src/test/resources/config_proxy_custom.yml +++ b/src/test/resources/config_proxy_custom.yml @@ -54,6 +54,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/config_rest_impersonation.yml b/src/test/resources/config_rest_impersonation.yml index dd8e89e3bd..a34232ff77 100644 --- a/src/test/resources/config_rest_impersonation.yml +++ b/src/test/resources/config_rest_impersonation.yml @@ -54,6 +54,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/multitenancy/config_nodnfof.yml b/src/test/resources/multitenancy/config_nodnfof.yml index 674d5d2ed2..f4afce87d4 100644 --- a/src/test/resources/multitenancy/config_nodnfof.yml +++ b/src/test/resources/multitenancy/config_nodnfof.yml @@ -159,6 +159,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" diff --git a/src/test/resources/restapi/config.yml b/src/test/resources/restapi/config.yml index 763176dbf5..2ed865657a 100644 --- a/src/test/resources/restapi/config.yml +++ b/src/test/resources/restapi/config.yml @@ -94,6 +94,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" From 275294a777224b0c5cd076b3c2fbf0d707fb5525 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 22 Jun 2023 01:16:18 -0700 Subject: [PATCH 51/74] Remove the security role adding in noop auth backend class Signed-off-by: Ryan Liang --- .../security/auth/internal/NoOpAuthenticationBackend.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java index 299a1a4577..ceedbaea78 100644 --- a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java @@ -47,7 +47,6 @@ public String getType() { @Override public User authenticate(final AuthCredentials credentials) { User user = new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); - user.addSecurityRoles(credentials.getSecurityRoles()); return user; } From 2b64b8bcb4f9c79e3d09883ca9178b7ae6d42fbf Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 22 Jun 2023 12:27:04 -0700 Subject: [PATCH 52/74] Add the check of NoOpAuthbackend in the backend registry Signed-off-by: Ryan Liang --- .../java/org/opensearch/security/auth/BackendRegistry.java | 3 +++ .../security/auth/internal/NoOpAuthenticationBackend.java | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 51e93978bd..001055d48a 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -533,6 +533,9 @@ private User impersonate(final RestRequest request, final User originalUser) thr //loop over all http/rest auth domains for (final AuthDomain authDomain: restAuthDomains) { final AuthenticationBackend authenticationBackend = authDomain.getBackend(); + if (authDomain.getBackend() instanceof NoOpAuthenticationBackend ) { + continue; + } final User impersonatedUser = checkExistsAndAuthz(restImpersonationCache, new User(impersonatedUserHeader), authenticationBackend, restAuthorizers); diff --git a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java index ceedbaea78..299a1a4577 100644 --- a/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/NoOpAuthenticationBackend.java @@ -47,6 +47,7 @@ public String getType() { @Override public User authenticate(final AuthCredentials credentials) { User user = new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + user.addSecurityRoles(credentials.getSecurityRoles()); return user; } From 896e8265ad5c5539002f77d5b440087a0a3e65c5 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 22 Jun 2023 15:25:20 -0700 Subject: [PATCH 53/74] Fix both impersonation tests Signed-off-by: Ryan Liang --- .../java/org/opensearch/security/auth/BackendRegistry.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 001055d48a..e9267d78d1 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -60,6 +60,7 @@ import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.ssl.util.Utils; @@ -533,7 +534,7 @@ private User impersonate(final RestRequest request, final User originalUser) thr //loop over all http/rest auth domains for (final AuthDomain authDomain: restAuthDomains) { final AuthenticationBackend authenticationBackend = authDomain.getBackend(); - if (authDomain.getBackend() instanceof NoOpAuthenticationBackend ) { + if (authDomain.getHttpAuthenticator() instanceof OnBehalfOfAuthenticator) { continue; } final User impersonatedUser = checkExistsAndAuthz(restImpersonationCache, new User(impersonatedUserHeader), authenticationBackend, From 729329b500fe20f8ed79396881a62f684fcf6212 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Fri, 23 Jun 2023 10:32:13 -0700 Subject: [PATCH 54/74] Trim the obo integration test and add a new test case for tempered token Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 66 +++++++++---------- ...BehalfOfJwtAuthorizationHeaderFactory.java | 61 ----------------- 2 files changed, 32 insertions(+), 95 deletions(-) delete mode 100644 src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index cd1926a194..8e2e2a0e40 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -46,29 +46,8 @@ public class OnBehalfOfJwtAuthenticationTest { static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - public static final String issuer = "cluster_0"; - public static final String subject = "testUser"; - public static final String audience = "audience_0"; - public static final Integer expirySeconds = 100000; - public static final String headerName = "Authorization"; - public static final List roles = List.of("admin", "HR"); - public static final List backendRoles = List.of("IT"); - private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of OBO authentication".getBytes(StandardCharsets.UTF_8)); private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); - - private static final OnBehalfOfJwtAuthorizationHeaderFactory tokenFactory = new OnBehalfOfJwtAuthorizationHeaderFactory( - signingKey, - issuer, - subject, - audience, - roles, - backendRoles, - expirySeconds, - headerName, - encryptionKey - ); - public static final String ADMIN_USER_NAME = "admin"; public static final String DEFAULT_PASSWORD = "secret"; public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; @@ -87,21 +66,40 @@ public class OnBehalfOfJwtAuthenticationTest { .build(); @Test - public void shouldAuthenticateWithOBOToken() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken())){ + public void shouldAuthenticateWithOBOTokenEndPoint() { + Header adminOboAuthHeader; - TestRestClient.HttpResponse response = client.getAuthInfo(); + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + + client.assertCorrectCredentials(ADMIN_USER_NAME); + TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); response.assertStatusCode(200); + + Map oboEndPointResponse = response.getBodyAs(Map.class); + assertThat(oboEndPointResponse, allOf( + aMapWithSize(3), + hasKey("user"), + hasKey("onBehalfOfToken"), + hasKey("duration"))); + + String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); + + adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + encodedOboTokenStr); + } + + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat("testUser", equalTo(username)); - } catch (Exception e) { - throw new RuntimeException(e); + assertThat(username, equalTo(ADMIN_USER_NAME)); } } @Test - public void shouldAuthenticateWithOBOTokenEndPoint() { + public void shouldNotAuthenticateWithATemperedOBOToken() { Header adminOboAuthHeader; try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { @@ -119,18 +117,18 @@ public void shouldAuthenticateWithOBOTokenEndPoint() { hasKey("duration"))); String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); - System.out.println("This is the OBO Token of admin user: " + encodedOboTokenStr); + StringBuilder stringBuilder = new StringBuilder(encodedOboTokenStr); + stringBuilder.deleteCharAt(encodedOboTokenStr.length() - 1); + String temperedOboTokenStr = stringBuilder.toString(); - adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + encodedOboTokenStr); + adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + temperedOboTokenStr); } try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { TestRestClient.HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(200); - - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(ADMIN_USER_NAME)); + response.assertStatusCode(401); + response.getBody().contains("Unauthorized"); } } } diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java deleted file mode 100644 index 966594ec4d..0000000000 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthorizationHeaderFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 java.util.List; -import java.util.Optional; -import java.util.function.LongSupplier; - -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.message.BasicHeader; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.authtoken.jwt.JwtVendor; - -import static java.util.Objects.requireNonNull; - -class OnBehalfOfJwtAuthorizationHeaderFactory { - - private final String issuer; - private final String subject; - private final String audience; - private final List roles; - private final List backendRoles; - private final String encryption_key; - private final String signing_key; - private final String headerName; - private final Integer expirySeconds; - - - public OnBehalfOfJwtAuthorizationHeaderFactory(String signing_key, String issuer, String subject, String audience, List roles, List backendRoles, Integer expirySeconds, String headerName, String encryption_key) { - this.signing_key = requireNonNull(signing_key, "Signing key is required"); - this.issuer = requireNonNull(issuer, "Issuer is required"); - this.subject = requireNonNull(subject, "Subject is required"); - this.audience = requireNonNull(audience, "Audience is required."); - this.roles = requireNonNull(roles, "Roles claim is required"); - this.backendRoles = requireNonNull(backendRoles, "Backend roles claim is required"); - this.expirySeconds = requireNonNull(expirySeconds, "Expiry is required"); - this.headerName = requireNonNull(headerName, "Header name is required"); - this.encryption_key = encryption_key; - } - - Header generateValidToken() throws Exception { - Optional currentTime = Optional.of(() -> System.currentTimeMillis() / 1000); - Settings settings = Settings.builder().put("signing_key", signing_key).put("encryption_key", encryption_key).build(); - JwtVendor jwtVendor = new JwtVendor(settings, currentTime); - String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles); - - return toHeader(encodedJwt); - } - - private BasicHeader toHeader(String token) { - return new BasicHeader(headerName, "Bearer " + token); - } -} From cfed7639aad12261dbf1c80b4251049107d22be5 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 26 Jun 2023 09:38:44 -0700 Subject: [PATCH 55/74] Change into matcher for OBO authenticator. Signed-off-by: Ryan Liang --- .../opensearch/security/http/OnBehalfOfAuthenticator.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index ccb6c917a9..45b4a7e5fd 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -181,9 +181,8 @@ private AuthCredentials extractCredentials0(final RestRequest request) { jwtToken = null; } - final int index; - if(jwtToken != null && (index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer - jwtToken = jwtToken.substring(index+BEARER_PREFIX.length()); + if(jwtToken != null && Pattern.compile(BEARER_PREFIX).matcher(jwtToken.toLowerCase()).find()) { + jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); } else { if(log.isDebugEnabled()) { log.debug("No Bearer scheme found in header"); From df18f61cf75ba6686644795b328aa9c602cb3063 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 26 Jun 2023 09:47:00 -0700 Subject: [PATCH 56/74] Rename the create obo token action Signed-off-by: Ryan Liang --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 4 ++-- ...eOnBehalfOfToken.java => CreateOnBehalfOfTokenAction.java} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/org/opensearch/security/action/onbehalf/{CreateOnBehalfOfToken.java => CreateOnBehalfOfTokenAction.java} (97%) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 3356f89ee0..1bda1b2c52 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -117,7 +117,7 @@ import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.TransportConfigUpdateAction; -import org.opensearch.security.action.onbehalf.CreateOnBehalfOfToken; +import org.opensearch.security.action.onbehalf.CreateOnBehalfOfTokenAction; import org.opensearch.security.action.whoami.TransportWhoAmIAction; import org.opensearch.security.action.whoami.WhoAmIAction; import org.opensearch.security.auditlog.AuditLog; @@ -478,7 +478,7 @@ public List getRestHandlers(Settings settings, RestController restC Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - CreateOnBehalfOfToken cobot = new CreateOnBehalfOfToken(settings, threadPool); + CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool); dcf.registerDCFListener(cobot); handlers.add(cobot); handlers.addAll( diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java similarity index 97% rename from src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java rename to src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java index 3534589292..244e550ca3 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfToken.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -40,7 +40,7 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -public class CreateOnBehalfOfToken extends BaseRestHandler { +public class CreateOnBehalfOfTokenAction extends BaseRestHandler { private JwtVendor vendor; private final ThreadPool threadPool; @@ -64,7 +64,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { } } - public CreateOnBehalfOfToken(final Settings settings, final ThreadPool threadPool) { + public CreateOnBehalfOfTokenAction(final Settings settings, final ThreadPool threadPool) { this.threadPool = threadPool; } From ed14cead8609dd3d0124c3cfd946d640c8f4ff84 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 26 Jun 2023 09:56:11 -0700 Subject: [PATCH 57/74] Add description in the testing config of signingKey + encryptionKey Signed-off-by: Ryan Liang --- src/integrationTest/resources/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml index 737cfece61..a08771c9ac 100644 --- a/src/integrationTest/resources/config.yml +++ b/src/integrationTest/resources/config.yml @@ -16,5 +16,7 @@ config: type: "internal" config: {} on_behalf_of: + # The decrypted signing key is: This is the jwt signing key for an on behalf of token authentication backend for testing of extensions + # The decrypted encryption key is: encryptionKey signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" encryption_key: "ZW5jcnlwdGlvbktleQ==" From a29992f76fddac227096f97f9976c723427d80fa Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 26 Jun 2023 10:12:47 -0700 Subject: [PATCH 58/74] Fix the token duration Signed-off-by: Ryan Liang --- .../action/onbehalf/CreateOnBehalfOfTokenAction.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 244e550ca3..0a39bc07f5 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -110,8 +110,8 @@ public void accept(RestChannel channel) throws Exception { final Integer tokenDuration = Optional.ofNullable(requestBody.get("duration")) .map(value -> (String)value) .map(Integer::parseInt) - .map(value -> Math.min(value, 72 * 3600)) // Max duration is 72 hours - .orElse(24 * 3600); // Fallback to default; + .map(value -> Math.min(value, 10 * 60)) // Max duration is 10 minutes + .orElse(5 * 60); // Fallback to default of 5 minutes; final String source = "self-issued"; final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); @@ -120,7 +120,10 @@ public void accept(RestChannel channel) throws Exception { builder.startObject(); builder.field("user", user.getName()); - final String token = vendor.createJwt(/* TODO: Update the issuer to represent the cluster */"OpenSearch", + + /* TODO: Update the issuer to represent the cluster */ + final String token = vendor.createJwt( + "OpenSearch", user.getName(), source, tokenDuration, From 7b6221f4a626823caeef04d94aefc46d042a0573 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 26 Jun 2023 16:42:09 -0700 Subject: [PATCH 59/74] No backendRoles by default Signed-off-by: Ryan Liang --- .../opensearch/security/authtoken/jwt/JwtVendor.java | 5 +---- .../security/http/OnBehalfOfAuthenticator.java | 10 ++++++---- .../security/authtoken/jwt/JwtVendorTest.java | 2 -- 3 files changed, 7 insertions(+), 10 deletions(-) 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 9a60f36c76..0d9e77cab2 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -134,10 +134,7 @@ public String createJwt(String issuer, String subject, String audience, Integer throw new Exception("Roles cannot be null"); } - if (backendRoles != null) { - String listOfBackendRoles = String.join(",", backendRoles); - jwtClaims.setProperty("ebr", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfBackendRoles)); - } + /* TODO: If the backendRoles is not null and the BWC Mode is on, put them into the "dbr" claim */ String encodedJwt = jwtProducer.processJwt(jwt); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 45b4a7e5fd..57270485d3 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -122,7 +122,12 @@ private List extractSecurityRolesFromClaims(Claims claims) { } private String[] extractBackendRolesFromClaims(Claims claims) { - Object backendRolesObject = ObjectUtils.firstNonNull(claims.get("ebr"), claims.get("dbr")); + //Object backendRolesObject = ObjectUtils.firstNonNull(claims.get("ebr"), claims.get("dbr")); + if (!claims.containsKey("dbr")) { + return null; + } + + Object backendRolesObject = claims.get("dbr"); String[] backendRoles; if (backendRolesObject == null) { @@ -134,9 +139,6 @@ private String[] extractBackendRolesFromClaims(Claims claims) { // Extracting roles based on the compatibility mode String decryptedBackendRoles = backendRolesClaim; - if (backendRolesObject == claims.get("ebr")) { - decryptedBackendRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, backendRolesClaim); - } backendRoles = Arrays.stream(decryptedBackendRoles.split(",")).map(String::trim).toArray(String[]::new); } 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 1a1a0a98e6..1abd80c220 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -50,7 +50,6 @@ public void testCreateJwtWithRoles() throws Exception { List roles = List.of("IT", "HR"); List backendRoles = List.of("Sales"); String expectedRoles = "IT,HR"; - String expectedBackendRoles = "Sales"; Integer expirySeconds = 300; LongSupplier currentTime = () -> (int) 100; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); @@ -71,7 +70,6 @@ public void testCreateJwtWithRoles() throws Exception { Assert.assertEquals(expectedExp, jwt.getClaim("exp")); Assert.assertNotEquals(expectedRoles, jwt.getClaim("er")); Assert.assertEquals(expectedRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("er").toString())); - Assert.assertEquals(expectedBackendRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("ebr").toString())); } @Test(expected = Exception.class) From d243fe5e29a8490e46176c3e73326ef46134b9a9 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 26 Jun 2023 19:52:13 -0700 Subject: [PATCH 60/74] Change the roles claim extraction error log msg Signed-off-by: Ryan Liang --- .../opensearch/security/http/OnBehalfOfAuthenticator.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 57270485d3..7ac9041c5a 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -104,8 +104,7 @@ private List extractSecurityRolesFromClaims(Claims claims) { List roles; if (rolesObject == null) { - log.warn( - "Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload."); + log.warn("This is a malformed On-behalf-of Token"); roles = List.of(); } else { final String rolesClaim = rolesObject.toString(); @@ -131,8 +130,7 @@ private String[] extractBackendRolesFromClaims(Claims claims) { String[] backendRoles; if (backendRolesObject == null) { - log.warn( - "Failed to get backend roles from JWT claims. Check if this key is correct and available in the JWT payload."); + log.warn("This is a malformed On-behalf-of Token"); backendRoles = new String[0]; } else { final String backendRolesClaim = backendRolesObject.toString(); From 85408628701dae7a81675a92bd64fc5a3c23b598 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 28 Jun 2023 11:47:27 -0700 Subject: [PATCH 61/74] Resolve some comments of formatting Signed-off-by: Ryan Liang --- src/integrationTest/resources/config.yml | 4 ++-- .../security/action/onbehalf/CreateOnBehalfOfTokenAction.java | 1 - src/test/resources/config.yml | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/integrationTest/resources/config.yml b/src/integrationTest/resources/config.yml index a08771c9ac..17aeb1881d 100644 --- a/src/integrationTest/resources/config.yml +++ b/src/integrationTest/resources/config.yml @@ -16,7 +16,7 @@ config: type: "internal" config: {} on_behalf_of: - # The decrypted signing key is: This is the jwt signing key for an on behalf of token authentication backend for testing of extensions - # The decrypted encryption key is: encryptionKey + # The decoded signing key is: This is the jwt signing key for an on behalf of token authentication backend for testing of extensions + # The decoded encryption key is: encryptionKey signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" encryption_key: "ZW5jcnlwdGlvbktleQ==" 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 0a39bc07f5..c273c073f9 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -135,7 +135,6 @@ public void accept(RestChannel channel) throws Exception { response = new BytesRestResponse(RestStatus.OK, builder); } catch (final Exception exception) { - System.out.println(exception.toString()); builder.startObject() .field("error", exception.toString()) .endObject(); diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml index 794c7527d7..4b85b883ea 100644 --- a/src/test/resources/config.yml +++ b/src/test/resources/config.yml @@ -97,5 +97,7 @@ config: hosts_resolver_mode: "ip-only" transport_userrname_attribute: null on_behalf_of: + # The decoded signing key is: This is the jwt signing key for an on behalf of token authentication backend for testing of extensions + # The decoded encryption key is: encryptionKey signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" encryption_key: "ZW5jcnlwdGlvbktleQ==" From b4be6627f9a8bc7cbebf40cdfbdc30bdd9bcf54d Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 28 Jun 2023 14:20:37 -0700 Subject: [PATCH 62/74] Update the issuer to represent the cluster Signed-off-by: Ryan Liang --- .../opensearch/security/OpenSearchSecurityPlugin.java | 2 +- .../action/onbehalf/CreateOnBehalfOfTokenAction.java | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 1bda1b2c52..b882a3524e 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -478,7 +478,7 @@ public List getRestHandlers(Settings settings, RestController restC Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool); + CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool, Objects.requireNonNull(cs)); dcf.registerDCFListener(cobot); handlers.add(cobot); handlers.addAll( 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 c273c073f9..7a621f0209 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -22,6 +22,7 @@ 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.common.transport.TransportAddress; import org.opensearch.core.xcontent.XContentBuilder; @@ -31,6 +32,7 @@ import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestStatus; +import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; @@ -44,6 +46,7 @@ public class CreateOnBehalfOfTokenAction extends BaseRestHandler { private JwtVendor vendor; private final ThreadPool threadPool; + private final ClusterService clusterService; private ConfigModel configModel; @@ -64,8 +67,9 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { } } - public CreateOnBehalfOfTokenAction(final Settings settings, final ThreadPool threadPool) { + public CreateOnBehalfOfTokenAction(final Settings settings, final ThreadPool threadPool, final ClusterService clusterService) { this.threadPool = threadPool; + this.clusterService = clusterService; } @Override @@ -104,6 +108,8 @@ public void accept(RestChannel channel) throws Exception { return; } + final String clusterIdentifier = clusterService.getClusterName().value(); + final Map requestBody = request.contentOrSourceParamParser().map(); final String reason = (String)requestBody.getOrDefault("reason", null); @@ -121,9 +127,8 @@ public void accept(RestChannel channel) throws Exception { builder.startObject(); builder.field("user", user.getName()); - /* TODO: Update the issuer to represent the cluster */ final String token = vendor.createJwt( - "OpenSearch", + clusterIdentifier, user.getName(), source, tokenDuration, From 8e0631f3c7e08a36fce0058e406b1b06796d8244 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 28 Jun 2023 14:53:54 -0700 Subject: [PATCH 63/74] Fix the obo token's duration issue Signed-off-by: Ryan Liang --- .../security/action/onbehalf/CreateOnBehalfOfTokenAction.java | 2 +- .../java/org/opensearch/security/authtoken/jwt/JwtVendor.java | 4 ++-- .../org/opensearch/security/authtoken/jwt/JwtVendorTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 7a621f0209..86875e5aa9 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -135,7 +135,7 @@ public void accept(RestChannel channel) throws Exception { mappedRoles.stream().collect(Collectors.toList()), user.getRoles().stream().collect(Collectors.toList())); builder.field("onBehalfOfToken", token); - builder.field("duration", tokenDuration); + builder.field("duration", tokenDuration + " seconds"); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); 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 0d9e77cab2..41c4e3186c 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -118,10 +118,10 @@ public String createJwt(String issuer, String subject, String audience, Integer jwtClaims.setNotBefore(timeMillis); if (expirySeconds == null) { - long expiryTime = timeProvider.getAsLong() + (300 * 1000); + long expiryTime = timeProvider.getAsLong() + 300; jwtClaims.setExpiryTime(expiryTime); } else if (expirySeconds > 0) { - long expiryTime = timeProvider.getAsLong() + (expirySeconds * 1000); + long expiryTime = timeProvider.getAsLong() + expirySeconds; jwtClaims.setExpiryTime(expiryTime); } else { throw new Exception("The expiration time should be a positive integer"); 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 1abd80c220..a278a526b3 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -54,7 +54,7 @@ public void testCreateJwtWithRoles() throws Exception { LongSupplier currentTime = () -> (int) 100; String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); Settings settings = Settings.builder().put("signing_key", "abc123").put("encryption_key", claimsEncryptionKey).build(); - Long expectedExp = currentTime.getAsLong() + (expirySeconds * 1000); + Long expectedExp = currentTime.getAsLong() + expirySeconds; JwtVendor jwtVendor = new JwtVendor(settings, Optional.of(currentTime)); String encodedJwt = jwtVendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles); From 144efdfec717e5c5c3428774ffcc16bb091602fe Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 28 Jun 2023 15:30:56 -0700 Subject: [PATCH 64/74] Fix unused imports Signed-off-by: Ryan Liang --- .../security/action/onbehalf/CreateOnBehalfOfTokenAction.java | 1 - 1 file changed, 1 deletion(-) 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 86875e5aa9..8b40f4f28c 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -32,7 +32,6 @@ import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestStatus; -import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; From ce9baa2f8c3f819bf689a2c456087aafacb2a9cc Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 3 Jul 2023 11:01:43 -0700 Subject: [PATCH 65/74] Fix formatting again after branch sync Signed-off-by: Ryan Liang --- .../http/OnBehalfOfJwtAuthenticationTest.java | 136 +++++++++--------- .../security/OpenSearchSecurityPlugin.java | 55 ++++++- .../onbehalf/CreateOnBehalfOfTokenAction.java | 43 +++--- .../security/auth/BackendRegistry.java | 1 - .../jwt/EncryptionDecryptionUtil.java | 2 + .../security/authtoken/jwt/JwtVendor.java | 10 +- .../http/OnBehalfOfAuthenticator.java | 23 ++- .../securityconf/DynamicConfigModelV7.java | 7 +- .../security/user/AuthCredentials.java | 8 +- 9 files changed, 171 insertions(+), 114 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 8e2e2a0e40..c4292386c6 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -37,98 +37,98 @@ 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 OnBehalfOfJwtAuthenticationTest { - public static final String POINTER_USERNAME = "/user_name"; + public static final String POINTER_USERNAME = "/user_name"; - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - private static final String signingKey = Base64.getEncoder().encodeToString("jwt signing key for an on behalf of token authentication backend for testing of OBO authentication".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 DEFAULT_PASSWORD = "secret"; - public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; - public static final String OBO_ENDPOINT_PREFIX = "_plugins/_security/api/user/onbehalfof"; + private static final String signingKey = Base64.getEncoder() + .encodeToString( + "jwt signing key for an on behalf of token authentication backend for testing of OBO authentication".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 DEFAULT_PASSWORD = "secret"; + public static final String OBO_TOKEN_REASON = "{\"reason\":\"Test generation\"}"; + public static final String OBO_ENDPOINT_PREFIX = "_plugins/_security/api/user/onbehalfof"; - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .users(ADMIN_USER) - .nodeSettings(Map.of( - "plugins.security.allow_default_init_securityindex", true, - "plugins.security.restapi.roles_enabled", List.of("user_admin__all_access") - )) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) - .build(); + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .users(ADMIN_USER) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .onBehalfOf(new OnBehalfOfConfig().signing_key(signingKey).encryption_key(encryptionKey)) + .build(); - @Test - public void shouldAuthenticateWithOBOTokenEndPoint() { - Header adminOboAuthHeader; + @Test + public void shouldAuthenticateWithOBOTokenEndPoint() { + Header adminOboAuthHeader; - try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.assertCorrectCredentials(ADMIN_USER_NAME); + client.assertCorrectCredentials(ADMIN_USER_NAME); - TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); - response.assertStatusCode(200); + TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); + response.assertStatusCode(200); - Map oboEndPointResponse = response.getBodyAs(Map.class); - assertThat(oboEndPointResponse, allOf( - aMapWithSize(3), - hasKey("user"), - hasKey("onBehalfOfToken"), - hasKey("duration"))); + Map oboEndPointResponse = response.getBodyAs(Map.class); + assertThat(oboEndPointResponse, allOf(aMapWithSize(3), hasKey("user"), hasKey("onBehalfOfToken"), hasKey("duration"))); - String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); + String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); - adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + encodedOboTokenStr); - } + adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + encodedOboTokenStr); + } - try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { - TestRestClient.HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(200); + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(ADMIN_USER_NAME)); - } - } + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(ADMIN_USER_NAME)); + } + } - @Test - public void shouldNotAuthenticateWithATemperedOBOToken() { - Header adminOboAuthHeader; + @Test + public void shouldNotAuthenticateWithATemperedOBOToken() { + Header adminOboAuthHeader; - try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.assertCorrectCredentials(ADMIN_USER_NAME); + client.assertCorrectCredentials(ADMIN_USER_NAME); - TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); - response.assertStatusCode(200); + TestRestClient.HttpResponse response = client.postJson(OBO_ENDPOINT_PREFIX, OBO_TOKEN_REASON); + response.assertStatusCode(200); - Map oboEndPointResponse = response.getBodyAs(Map.class); - assertThat(oboEndPointResponse, allOf( - aMapWithSize(3), - hasKey("user"), - hasKey("onBehalfOfToken"), - hasKey("duration"))); + Map oboEndPointResponse = response.getBodyAs(Map.class); + assertThat(oboEndPointResponse, allOf(aMapWithSize(3), hasKey("user"), hasKey("onBehalfOfToken"), hasKey("duration"))); - String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); - StringBuilder stringBuilder = new StringBuilder(encodedOboTokenStr); - stringBuilder.deleteCharAt(encodedOboTokenStr.length() - 1); - String temperedOboTokenStr = stringBuilder.toString(); + String encodedOboTokenStr = oboEndPointResponse.get("onBehalfOfToken").toString(); + StringBuilder stringBuilder = new StringBuilder(encodedOboTokenStr); + stringBuilder.deleteCharAt(encodedOboTokenStr.length() - 1); + String temperedOboTokenStr = stringBuilder.toString(); - adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + temperedOboTokenStr); - } + adminOboAuthHeader = new BasicHeader("Authorization", "Bearer " + temperedOboTokenStr); + } - try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { + try (TestRestClient client = cluster.getRestClient(adminOboAuthHeader)) { - TestRestClient.HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(401); - response.getBody().contains("Unauthorized"); - } - } + TestRestClient.HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(401); + response.getBody().contains("Unauthorized"); + } + } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index d081e60fb7..c32f4c7078 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -493,11 +493,45 @@ public List getRestHandlers( new SecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool)) ); handlers.add(new SecurityHealthAction(settings, restController, Objects.requireNonNull(backendRegistry))); - handlers.add(new DashboardsInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); - handlers.add(new TenantInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool), - Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); - handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.add( + new DashboardsInfoAction( + settings, + restController, + Objects.requireNonNull(evaluator), + Objects.requireNonNull(threadPool) + ) + ); + handlers.add( + new TenantInfoAction( + settings, + restController, + Objects.requireNonNull(evaluator), + Objects.requireNonNull(threadPool), + Objects.requireNonNull(cs), + Objects.requireNonNull(adminDns), + Objects.requireNonNull(cr) + ) + ); + handlers.add( + new SecurityConfigUpdateAction( + settings, + restController, + Objects.requireNonNull(threadPool), + adminDns, + configPath, + principalExtractor + ) + ); + handlers.add( + new SecurityWhoAmIAction( + settings, + restController, + Objects.requireNonNull(threadPool), + adminDns, + configPath, + principalExtractor + ) + ); CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool, Objects.requireNonNull(cs)); dcf.registerDCFListener(cobot); handlers.add(cobot); @@ -990,8 +1024,15 @@ public Collection createComponents( principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, - principalExtractor, settings, configPath, compatConfig); + securityRestHandler = new SecurityRestFilter( + backendRegistry, + auditLog, + threadPool, + principalExtractor, + settings, + configPath, + compatConfig + ); dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih); dcf.registerDCFListener(backendRegistry); dcf.registerDCFListener(compatConfig); 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 8b40f4f28c..134a2eca87 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -59,7 +59,8 @@ public void onConfigModelChanged(ConfigModel configModel) { @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; - if (dcm.getDynamicOnBehalfOfSettings().get("signing_key") != null && dcm.getDynamicOnBehalfOfSettings().get("encryption_key") != null) { + if (dcm.getDynamicOnBehalfOfSettings().get("signing_key") != null + && dcm.getDynamicOnBehalfOfSettings().get("encryption_key") != null) { this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty()); } else { this.vendor = null; @@ -78,11 +79,7 @@ public String getName() { @Override public List routes() { - return addRoutesPrefix( - ImmutableList.of( - new Route(Method.POST, "/user/onbehalfof") - ) - ); + return addRoutesPrefix(ImmutableList.of(new Route(Method.POST, "/user/onbehalfof"))); } @Override @@ -103,45 +100,47 @@ public void accept(RestChannel channel) throws Exception { BytesRestResponse response; try { if (vendor == null) { - channel.sendResponse(new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "on_behalf_of configuration is not being configured")); + channel.sendResponse( + new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "on_behalf_of configuration is not being configured") + ); return; } final String clusterIdentifier = clusterService.getClusterName().value(); final Map requestBody = request.contentOrSourceParamParser().map(); - final String reason = (String)requestBody.getOrDefault("reason", null); + final String reason = (String) requestBody.getOrDefault("reason", null); final Integer tokenDuration = Optional.ofNullable(requestBody.get("duration")) - .map(value -> (String)value) - .map(Integer::parseInt) - .map(value -> Math.min(value, 10 * 60)) // Max duration is 10 minutes - .orElse(5 * 60); // Fallback to default of 5 minutes; + .map(value -> (String) value) + .map(Integer::parseInt) + .map(value -> Math.min(value, 10 * 60)) // Max duration is 10 minutes + .orElse(5 * 60); // Fallback to default of 5 minutes; final String source = "self-issued"; final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final TransportAddress caller = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + final TransportAddress caller = threadPool.getThreadContext() + .getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); Set mappedRoles = mapRoles(user, caller); builder.startObject(); builder.field("user", user.getName()); final String token = vendor.createJwt( - clusterIdentifier, - user.getName(), - source, - tokenDuration, - mappedRoles.stream().collect(Collectors.toList()), - user.getRoles().stream().collect(Collectors.toList())); + clusterIdentifier, + user.getName(), + source, + tokenDuration, + mappedRoles.stream().collect(Collectors.toList()), + user.getRoles().stream().collect(Collectors.toList()) + ); builder.field("onBehalfOfToken", token); builder.field("duration", tokenDuration + " seconds"); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); } catch (final Exception exception) { - builder.startObject() - .field("error", exception.toString()) - .endObject(); + builder.startObject().field("error", exception.toString()).endObject(); response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); } diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index c6652971b8..0a287d19f5 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -59,7 +59,6 @@ import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.ssl.util.Utils; diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 2aa343014a..e38a48cde3 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -56,7 +56,9 @@ private static byte[] createCipherText(final Cipher cipher, final byte[] data) { private enum CipherMode { ENCRYPT(Cipher.ENCRYPT_MODE), DECRYPT(Cipher.DECRYPT_MODE); + private final int opmode; + private CipherMode(final int opmode) { this.opmode = opmode; } 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 b2c4c6ee0d..3cec339647 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -41,7 +41,6 @@ public class JwtVendor { private final JoseJwtProducer jwtProducer; private final LongSupplier timeProvider; - public JwtVendor(final Settings settings, final Optional timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { @@ -98,7 +97,14 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { } } - public String createJwt(String issuer, String subject, String audience, Integer expirySeconds, List roles, List backendRoles) throws Exception { + public String createJwt( + String issuer, + String subject, + String audience, + Integer expirySeconds, + List roles, + List backendRoles + ) throws Exception { long timeMillis = timeProvider.getAsLong(); Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 7ac9041c5a..921e06e5b8 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -68,9 +68,7 @@ private JwtParser initParser(final String signingKey) { } try { - final String minimalKeyFormat = signingKey - .replace("-----BEGIN PUBLIC KEY-----\n", "") - .replace("-----END PUBLIC KEY-----", ""); + final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace("-----END PUBLIC KEY-----", ""); final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); Key key = null; @@ -121,7 +119,7 @@ private List extractSecurityRolesFromClaims(Claims claims) { } private String[] extractBackendRolesFromClaims(Claims claims) { - //Object backendRolesObject = ObjectUtils.firstNonNull(claims.get("ebr"), claims.get("dbr")); + // Object backendRolesObject = ObjectUtils.firstNonNull(claims.get("ebr"), claims.get("dbr")); if (!claims.containsKey("dbr")) { return null; } @@ -171,7 +169,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { String jwtToken = request.header(HttpHeaders.AUTHORIZATION); if (jwtToken == null || jwtToken.length() == 0) { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); } return null; @@ -181,10 +179,10 @@ private AuthCredentials extractCredentials0(final RestRequest request) { jwtToken = null; } - if(jwtToken != null && Pattern.compile(BEARER_PREFIX).matcher(jwtToken.toLowerCase()).find()) { - jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); + if (jwtToken != null && Pattern.compile(BEARER_PREFIX).matcher(jwtToken.toLowerCase()).find()) { + jwtToken = jwtToken.substring(jwtToken.toLowerCase().indexOf(BEARER_PREFIX) + BEARER_PREFIX.length()); } else { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("No Bearer scheme found in header"); } } @@ -213,8 +211,8 @@ private AuthCredentials extractCredentials0(final RestRequest request) { final AuthCredentials ac = new AuthCredentials(subject, roles, backendRoles).markComplete(); - for(Entry claim: claims.entrySet()) { - ac.addAttribute("attr.jwt."+claim.getKey(), String.valueOf(claim.getValue())); + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); } return ac; @@ -224,7 +222,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { return null; } catch (Exception e) { e.printStackTrace(); - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); } return null; @@ -241,7 +239,8 @@ public String getType() { return "onbehalfof_jwt"; } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, InvalidKeySpecException { + private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, + InvalidKeySpecException { X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance(algo); return kf.generatePublic(spec); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index f9a584ad30..3f51524e2c 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -366,7 +366,12 @@ private void buildAAA() { Settings oboSettings = getDynamicOnBehalfOfSettings(); if (oboSettings.get("signing_key") != null && oboSettings.get("encryption_key") != null) { - final AuthDomain _ad = new AuthDomain(new NoOpAuthenticationBackend(Settings.EMPTY, null), new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings()), false, -1); + final AuthDomain _ad = new AuthDomain( + new NoOpAuthenticationBackend(Settings.EMPTY, null), + new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings()), + false, + -1 + ); restAuthDomains0.add(_ad); } diff --git a/src/main/java/org/opensearch/security/user/AuthCredentials.java b/src/main/java/org/opensearch/security/user/AuthCredentials.java index 58e7383ff1..f939fa28b8 100644 --- a/src/main/java/org/opensearch/security/user/AuthCredentials.java +++ b/src/main/java/org/opensearch/security/user/AuthCredentials.java @@ -108,7 +108,13 @@ public AuthCredentials(final String username, List securityRoles, String this.securityRoles.addAll(securityRoles); } - private AuthCredentials(final String username, byte[] password, Object nativeCredentials, List securityRoles, String... backendRoles) { + private AuthCredentials( + final String username, + byte[] password, + Object nativeCredentials, + List securityRoles, + String... backendRoles + ) { this(username, null, null, backendRoles); } From 0495b50f5d92e0dd6ea708730ba6fccd4bde920f Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 3 Jul 2023 11:52:14 -0700 Subject: [PATCH 66/74] Fix Impersonation tests again Signed-off-by: Ryan Liang --- .../java/org/opensearch/security/auth/BackendRegistry.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 0a287d19f5..37a53cf4c0 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -59,6 +59,7 @@ import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.http.OnBehalfOfAuthenticator; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.ssl.util.Utils; @@ -606,6 +607,11 @@ private User impersonate(final RestRequest request, final User originalUser) thr // loop over all http/rest auth domains for (final AuthDomain authDomain : restAuthDomains) { final AuthenticationBackend authenticationBackend = authDomain.getBackend(); + + if (authDomain.getHttpAuthenticator() instanceof OnBehalfOfAuthenticator) { + continue; + } + final User impersonatedUser = checkExistsAndAuthz( restImpersonationCache, new User(impersonatedUserHeader), From b8c9ba1feb0fbc5a479ecd16d9ee6107514ee63e Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Mon, 3 Jul 2023 12:55:24 -0700 Subject: [PATCH 67/74] Add api parameter for services (aud) Signed-off-by: Ryan Liang --- .../security/action/onbehalf/CreateOnBehalfOfTokenAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 134a2eca87..1f8f13c000 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -117,7 +117,7 @@ public void accept(RestChannel channel) throws Exception { .map(value -> Math.min(value, 10 * 60)) // Max duration is 10 minutes .orElse(5 * 60); // Fallback to default of 5 minutes; - final String source = "self-issued"; + final String service = (String) requestBody.getOrDefault("service", "self-issued"); final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final TransportAddress caller = threadPool.getThreadContext() .getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); @@ -129,7 +129,7 @@ public void accept(RestChannel channel) throws Exception { final String token = vendor.createJwt( clusterIdentifier, user.getName(), - source, + service, tokenDuration, mappedRoles.stream().collect(Collectors.toList()), user.getRoles().stream().collect(Collectors.toList()) From 49a705be43c27457bb140627745e1ae31a1d90f9 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 5 Jul 2023 13:15:51 -0700 Subject: [PATCH 68/74] Remove the obo config json config files Signed-off-by: Ryan Liang --- src/test/resources/restapi/security_config.json | 6 +----- src/test/resources/restapi/securityconfig.json | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/test/resources/restapi/security_config.json b/src/test/resources/restapi/security_config.json index 590c3b1f92..e5c09050cc 100644 --- a/src/test/resources/restapi/security_config.json +++ b/src/test/resources/restapi/security_config.json @@ -132,11 +132,7 @@ "do_not_fail_on_forbidden":false, "multi_rolespan_enabled":false, "hosts_resolver_mode":"ip-only", - "do_not_fail_on_forbidden_empty":false, - "on_behalf_of": { - "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", - "encryption_key": "ZW5jcnlwdGlvbktleQ==" - } + "do_not_fail_on_forbidden_empty":false } } diff --git a/src/test/resources/restapi/securityconfig.json b/src/test/resources/restapi/securityconfig.json index d0327b1b5a..4e4b1bba63 100644 --- a/src/test/resources/restapi/securityconfig.json +++ b/src/test/resources/restapi/securityconfig.json @@ -153,11 +153,7 @@ "do_not_fail_on_forbidden":false, "multi_rolespan_enabled":false, "hosts_resolver_mode":"ip-only", - "do_not_fail_on_forbidden_empty":false, - "on_behalf_of": { - "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", - "encryption_key": "ZW5jcnlwdGlvbktleQ==" - } + "do_not_fail_on_forbidden_empty":false } } From 4b7e42a70c04e8ec4ca974f23349432242b93dad Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 5 Jul 2023 13:47:56 -0700 Subject: [PATCH 69/74] Remove the obo config in src/test/resources/config.yml Signed-off-by: Ryan Liang --- src/test/resources/config.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml index 4b85b883ea..3663b3c706 100644 --- a/src/test/resources/config.yml +++ b/src/test/resources/config.yml @@ -96,8 +96,3 @@ config: multi_rolespan_enabled: false hosts_resolver_mode: "ip-only" transport_userrname_attribute: null - on_behalf_of: - # The decoded signing key is: This is the jwt signing key for an on behalf of token authentication backend for testing of extensions - # The decoded encryption key is: encryptionKey - signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z" - encryption_key: "ZW5jcnlwdGlvbktleQ==" From dfa6df9842448e5f2857998daedd28e98d56f847 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 5 Jul 2023 15:45:50 -0700 Subject: [PATCH 70/74] Remove the obo config in src/test/resources/restapi/securityconfig_nondefault.json Signed-off-by: Ryan Liang --- src/test/resources/restapi/securityconfig_nondefault.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index a3f2a307d6..6fb297be37 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -170,10 +170,6 @@ "do_not_fail_on_forbidden" : false, "multi_rolespan_enabled" : true, "hosts_resolver_mode" : "ip-only", - "do_not_fail_on_forbidden_empty" : false, - "on_behalf_of": { - "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", - "encryption_key": "ZW5jcnlwdGlvbktleQ==" - } + "do_not_fail_on_forbidden_empty" : false } } From cb477924d5352a758a73648ceeba5ab93b490901 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 5 Jul 2023 16:11:41 -0700 Subject: [PATCH 71/74] Add back the obo config of securityconfig_nondefault.json Signed-off-by: Ryan Liang --- src/test/resources/restapi/securityconfig_nondefault.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index 6fb297be37..a3f2a307d6 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -170,6 +170,10 @@ "do_not_fail_on_forbidden" : false, "multi_rolespan_enabled" : true, "hosts_resolver_mode" : "ip-only", - "do_not_fail_on_forbidden_empty" : false + "do_not_fail_on_forbidden_empty" : false, + "on_behalf_of": { + "signing_key": "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z", + "encryption_key": "ZW5jcnlwdGlvbktleQ==" + } } } From bc574c481b8a97fe7dec1e43af47b0f1139c2151 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 6 Jul 2023 13:50:51 -0700 Subject: [PATCH 72/74] Make a keyUtil for checking algo Signed-off-by: Ryan Liang --- .../http/OnBehalfOfAuthenticator.java | 31 +---------- .../org/opensearch/security/util/keyUtil.java | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/opensearch/security/util/keyUtil.java diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 921e06e5b8..d8137925b5 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -13,14 +13,8 @@ import java.security.AccessController; import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; import java.security.PrivilegedAction; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; -import java.util.Base64; import java.util.List; import java.util.Map.Entry; import java.util.Objects; @@ -45,6 +39,7 @@ import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.keyUtil; public class OnBehalfOfAuthenticator implements HTTPAuthenticator { @@ -68,22 +63,7 @@ private JwtParser initParser(final String signingKey) { } try { - final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace("-----END PUBLIC KEY-----", ""); - - final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); - Key key = null; - - try { - key = getPublicKey(decoded, "RSA"); - } catch (Exception e) { - log.debug("No public RSA key, try other algos ({})", e.toString()); - } - - try { - key = getPublicKey(decoded, "EC"); - } catch (Exception e) { - log.debug("No public ECDSA key, try other algos ({})", e.toString()); - } + Key key = keyUtil.keyAlgorithmCheck(signingKey, log); if (Objects.nonNull(key)) { return Jwts.parser().setSigningKey(key); @@ -239,11 +219,4 @@ public String getType() { return "onbehalfof_jwt"; } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, - InvalidKeySpecException { - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance(algo); - return kf.generatePublic(spec); - } - } diff --git a/src/main/java/org/opensearch/security/util/keyUtil.java b/src/main/java/org/opensearch/security/util/keyUtil.java new file mode 100644 index 0000000000..fd71f72e40 --- /dev/null +++ b/src/main/java/org/opensearch/security/util/keyUtil.java @@ -0,0 +1,54 @@ +/* + * 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.util; + +import org.apache.logging.log4j.Logger; + +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +public class keyUtil { + + public static Key keyAlgorithmCheck(final String signingKey, final Logger log) { + Key key = null; + + final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace("-----END PUBLIC KEY-----", ""); + + final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); + try { + key = getPublicKey(decoded, "RSA"); + } catch (Exception e) { + log.debug("No public RSA key, try other algos ({})", e.toString()); + } + + try { + key = getPublicKey(decoded, "EC"); + } catch (final Exception e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } + + return key; + } + + private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, + InvalidKeySpecException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(algo); + return kf.generatePublic(spec); + } + +} From 9340e6c058a0955dd8109a9cc1352ffbca3c0d43 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 6 Jul 2023 17:22:18 -0700 Subject: [PATCH 73/74] Extend the keyUtil and add to OBO authbackend Signed-off-by: Ryan Liang --- .../http/OnBehalfOfAuthenticator.java | 19 +------- .../org/opensearch/security/util/keyUtil.java | 47 +++++++++++++------ 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index d8137925b5..57df1bc8b4 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -58,23 +58,8 @@ public OnBehalfOfAuthenticator(Settings settings) { } private JwtParser initParser(final String signingKey) { - if (signingKey == null || signingKey.length() == 0) { - throw new RuntimeException("Unable to find on behalf of authenticator signing key"); - } - - try { - Key key = keyUtil.keyAlgorithmCheck(signingKey, log); - - if (Objects.nonNull(key)) { - return Jwts.parser().setSigningKey(key); - } - // Fallback to the decoded signing key - // TODO: Should we ever do this, I think no?? - return Jwts.parser().setSigningKey(signingKey); - } catch (Throwable e) { - log.error("Error while creating JWT authenticator", e); - throw new RuntimeException(e); - } + JwtParser _jwtParser = keyUtil.keyAlgorithmCheck(signingKey, log); + return _jwtParser; } private List extractSecurityRolesFromClaims(Claims claims) { diff --git a/src/main/java/org/opensearch/security/util/keyUtil.java b/src/main/java/org/opensearch/security/util/keyUtil.java index fd71f72e40..b5bdf9d514 100644 --- a/src/main/java/org/opensearch/security/util/keyUtil.java +++ b/src/main/java/org/opensearch/security/util/keyUtil.java @@ -11,6 +11,8 @@ package org.opensearch.security.util; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; import org.apache.logging.log4j.Logger; import java.security.Key; @@ -20,28 +22,43 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; +import java.util.Objects; public class keyUtil { - public static Key keyAlgorithmCheck(final String signingKey, final Logger log) { - Key key = null; - - final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace("-----END PUBLIC KEY-----", ""); - - final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); - try { - key = getPublicKey(decoded, "RSA"); - } catch (Exception e) { - log.debug("No public RSA key, try other algos ({})", e.toString()); + public static JwtParser keyAlgorithmCheck(final String signingKey, final Logger log) { + if (signingKey == null || signingKey.length() == 0) { + throw new RuntimeException("Unable to find on behalf of authenticator signing key"); } try { - key = getPublicKey(decoded, "EC"); - } catch (final Exception e) { - log.debug("No public ECDSA key, try other algos ({})", e.toString()); - } + Key key = null; + + final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace("-----END PUBLIC KEY-----", ""); - return key; + final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); + + try { + key = getPublicKey(decoded, "RSA"); + } catch (Exception e) { + log.debug("No public RSA key, try other algos ({})", e.toString()); + } + + try { + key = getPublicKey(decoded, "EC"); + } catch (final Exception e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } + + if (Objects.nonNull(key)) { + return Jwts.parser().setSigningKey(key); + } + + return Jwts.parser().setSigningKey(decoded); + } catch (Throwable e) { + log.error("Error while creating JWT authenticator", e); + throw new RuntimeException(e); + } } private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, From cd203b838e58e354c4372ebdfdc1a11b756cc796 Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Thu, 6 Jul 2023 18:47:09 -0700 Subject: [PATCH 74/74] Apply KeyUtil to both OBO and HTTPJWT authbackend Signed-off-by: Ryan Liang --- .../auth/http/jwt/HTTPJwtAuthenticator.java | 56 +------------------ .../http/OnBehalfOfAuthenticator.java | 8 ++- .../org/opensearch/security/util/keyUtil.java | 48 ++++++++-------- 3 files changed, 33 insertions(+), 79 deletions(-) diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java index 3468bb89af..ee152a0d95 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java @@ -13,21 +13,13 @@ import java.nio.file.Path; import java.security.AccessController; -import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; import java.security.PrivilegedAction; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; import java.util.Collection; import java.util.Map.Entry; import java.util.regex.Pattern; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.WeakKeyException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; @@ -43,6 +35,7 @@ import org.opensearch.rest.RestStatus; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.keyUtil; public class HTTPJwtAuthenticator implements HTTPAuthenticator { @@ -63,44 +56,8 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { super(); - JwtParser _jwtParser = null; - - try { - String signingKey = settings.get("signing_key"); - - if (signingKey == null || signingKey.length() == 0) { - log.error("signingKey must not be null or empty. JWT authentication will not work"); - } else { - - signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", ""); - signingKey = signingKey.replace("-----END PUBLIC KEY-----", ""); - - byte[] decoded = Decoders.BASE64.decode(signingKey); - Key key = null; - - try { - key = getPublicKey(decoded, "RSA"); - } catch (Exception e) { - log.debug("No public RSA key, try other algos ({})", e.toString()); - } - - try { - key = getPublicKey(decoded, "EC"); - } catch (Exception e) { - log.debug("No public ECDSA key, try other algos ({})", e.toString()); - } - - if (key != null) { - _jwtParser = Jwts.parser().setSigningKey(key); - } else { - _jwtParser = Jwts.parser().setSigningKey(decoded); - } - - } - } catch (Throwable e) { - log.error("Error creating JWT authenticator. JWT authentication will not work", e); - throw new RuntimeException(e); - } + String signingKey = settings.get("signing_key"); + JwtParser _jwtParser = keyUtil.keyAlgorithmCheck(signingKey, log); jwtUrlParameter = settings.get("jwt_url_parameter"); jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION); @@ -282,11 +239,4 @@ protected String[] extractRoles(final Claims claims, final RestRequest request) return roles; } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, - InvalidKeySpecException { - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance(algo); - return kf.generatePublic(spec); - } - } diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index 57df1bc8b4..863ec179cb 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -12,7 +12,6 @@ package org.opensearch.security.http; import java.security.AccessController; -import java.security.Key; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.List; @@ -23,7 +22,6 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.WeakKeyException; import org.apache.commons.lang3.ObjectUtils; import org.apache.hc.core5.http.HttpHeaders; @@ -59,7 +57,11 @@ public OnBehalfOfAuthenticator(Settings settings) { private JwtParser initParser(final String signingKey) { JwtParser _jwtParser = keyUtil.keyAlgorithmCheck(signingKey, log); - return _jwtParser; + if (_jwtParser != null) { + return _jwtParser; + } else { + throw new RuntimeException("Unable to find on behalf of authenticator signing key"); + } } private List extractSecurityRolesFromClaims(Claims claims) { diff --git a/src/main/java/org/opensearch/security/util/keyUtil.java b/src/main/java/org/opensearch/security/util/keyUtil.java index b5bdf9d514..214af6da31 100644 --- a/src/main/java/org/opensearch/security/util/keyUtil.java +++ b/src/main/java/org/opensearch/security/util/keyUtil.java @@ -28,36 +28,38 @@ public class keyUtil { public static JwtParser keyAlgorithmCheck(final String signingKey, final Logger log) { if (signingKey == null || signingKey.length() == 0) { - throw new RuntimeException("Unable to find on behalf of authenticator signing key"); - } + log.error("Unable to find signing key"); + return null; + } else { + try { + Key key = null; - try { - Key key = null; + final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") + .replace("-----END PUBLIC KEY-----", ""); - final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace("-----END PUBLIC KEY-----", ""); + final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); - final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); + try { + key = getPublicKey(decoded, "RSA"); + } catch (Exception e) { + log.debug("No public RSA key, try other algos ({})", e.toString()); + } - try { - key = getPublicKey(decoded, "RSA"); - } catch (Exception e) { - log.debug("No public RSA key, try other algos ({})", e.toString()); - } + try { + key = getPublicKey(decoded, "EC"); + } catch (final Exception e) { + log.debug("No public ECDSA key, try other algos ({})", e.toString()); + } - try { - key = getPublicKey(decoded, "EC"); - } catch (final Exception e) { - log.debug("No public ECDSA key, try other algos ({})", e.toString()); - } + if (Objects.nonNull(key)) { + return Jwts.parser().setSigningKey(key); + } - if (Objects.nonNull(key)) { - return Jwts.parser().setSigningKey(key); + return Jwts.parser().setSigningKey(decoded); + } catch (Throwable e) { + log.error("Error while creating JWT authenticator", e); + throw new RuntimeException(e); } - - return Jwts.parser().setSigningKey(decoded); - } catch (Throwable e) { - log.error("Error while creating JWT authenticator", e); - throw new RuntimeException(e); } }