diff --git a/CHANGELOG.md b/CHANGELOG.md index 64274e86a4f74..b9c6db986001a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,7 +86,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Security ## [Unreleased 2.x] -### Added +### Add TokenManager Interface ([#7452](https://github.com/opensearch-project/OpenSearch/pull/7452)) ### Dependencies - Bump `jackson` from 2.15.1 to 2.15.2 ([#7897](https://github.com/opensearch-project/OpenSearch/pull/7897)) diff --git a/plugins/identity-shiro/build.gradle b/plugins/identity-shiro/build.gradle index c02f3727f935e..22dc21864b620 100644 --- a/plugins/identity-shiro/build.gradle +++ b/plugins/identity-shiro/build.gradle @@ -18,6 +18,7 @@ opensearchplugin { dependencies { implementation 'org.apache.shiro:shiro-core:1.11.0' + // Needed for shiro implementation "org.slf4j:slf4j-api:${versions.slf4j}" @@ -25,12 +26,15 @@ dependencies { implementation 'commons-logging:commons-logging:1.2' implementation 'commons-lang:commons-lang:2.6' + implementation 'org.passay:passay:1.6.3' + implementation "org.bouncycastle:bcprov-jdk15on:${versions.bouncycastle}" testImplementation project(path: ':modules:transport-netty4') // for http testImplementation project(path: ':plugins:transport-nio') // for http testImplementation "org.mockito:mockito-core:${versions.mockito}" - testImplementation project(path: ':client:rest-high-level') + testImplementation project(path: ':client:rest-high-level') + testImplementation 'junit:junit:4.13.2' } /* @@ -46,6 +50,7 @@ internalClusterTest { } thirdPartyAudit.ignoreMissingClasses( + 'com.google.common.hash.BloomFilter', 'javax.servlet.ServletContextEvent', 'javax.servlet.ServletContextListener', 'org.apache.avalon.framework.logger.Logger', @@ -61,7 +66,10 @@ thirdPartyAudit.ignoreMissingClasses( 'org.apache.log4j.Level', 'org.apache.log4j.Logger', 'org.apache.log4j.Priority', + 'org.cryptacular.bean.HashBean', 'org.slf4j.impl.StaticLoggerBinder', 'org.slf4j.impl.StaticMDCBinder', - 'org.slf4j.impl.StaticMarkerBinder' + 'org.slf4j.impl.StaticMarkerBinder', + 'org.springframework.context.MessageSource', + 'org.springframework.context.support.MessageSourceAccessor' ) diff --git a/plugins/identity-shiro/licenses/passay-1.6.3.jar.sha1 b/plugins/identity-shiro/licenses/passay-1.6.3.jar.sha1 new file mode 100644 index 0000000000000..52ac0b1a81469 --- /dev/null +++ b/plugins/identity-shiro/licenses/passay-1.6.3.jar.sha1 @@ -0,0 +1 @@ +bae4754c87297f5600e4071b2596c0b6625cf92b \ No newline at end of file diff --git a/plugins/identity-shiro/licenses/passay-LICENSE.txt b/plugins/identity-shiro/licenses/passay-LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/plugins/identity-shiro/licenses/passay-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/identity-shiro/licenses/passay-NOTICE.txt b/plugins/identity-shiro/licenses/passay-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/AuthTokenHandler.java b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/AuthTokenHandler.java deleted file mode 100644 index 14801b665f14f..0000000000000 --- a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/AuthTokenHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.identity.shiro; - -import java.util.Optional; - -import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.UsernamePasswordToken; -import org.opensearch.identity.tokens.BasicAuthToken; - -/** - * Extracts Shiro's {@link AuthenticationToken} from different types of auth headers - * - * @opensearch.experimental - */ -class AuthTokenHandler { - - /** - * Translates into shiro auth token from the given header token - * @param authenticationToken the token from which to translate - * @return An optional of the shiro auth token for login - */ - public Optional translateAuthToken(org.opensearch.identity.tokens.AuthToken authenticationToken) { - if (authenticationToken instanceof BasicAuthToken) { - final BasicAuthToken basicAuthToken = (BasicAuthToken) authenticationToken; - return Optional.of(new UsernamePasswordToken(basicAuthToken.getUser(), basicAuthToken.getPassword())); - } - - return Optional.empty(); - } -} diff --git a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java index eee5dd8ce0bd4..c3c08b1359aaa 100644 --- a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java +++ b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroIdentityPlugin.java @@ -11,6 +11,7 @@ import org.opensearch.identity.Subject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.identity.tokens.TokenManager; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.common.settings.Settings; import org.opensearch.plugins.Plugin; @@ -26,7 +27,7 @@ public final class ShiroIdentityPlugin extends Plugin implements IdentityPlugin private Logger log = LogManager.getLogger(this.getClass()); private final Settings settings; - private final AuthTokenHandler authTokenHandler; + private final ShiroTokenManager authTokenHandler; /** * Create a new instance of the Shiro Identity Plugin @@ -35,7 +36,7 @@ public final class ShiroIdentityPlugin extends Plugin implements IdentityPlugin */ public ShiroIdentityPlugin(final Settings settings) { this.settings = settings; - authTokenHandler = new AuthTokenHandler(); + authTokenHandler = new ShiroTokenManager(); SecurityManager securityManager = new ShiroSecurityManager(); SecurityUtils.setSecurityManager(securityManager); @@ -50,4 +51,14 @@ public ShiroIdentityPlugin(final Settings settings) { public Subject getSubject() { return new ShiroSubject(authTokenHandler, SecurityUtils.getSubject()); } + + /** + * Return the Shiro Token Handler + * + * @return the Shiro Token Handler + */ + @Override + public TokenManager getTokenManager() { + return this.authTokenHandler; + } } diff --git a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroSubject.java b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroSubject.java index 3208d2bb5d8ca..89a801d1aab76 100644 --- a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroSubject.java +++ b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroSubject.java @@ -20,7 +20,7 @@ * @opensearch.experimental */ public class ShiroSubject implements Subject { - private final AuthTokenHandler authTokenHandler; + private final ShiroTokenManager authTokenHandler; private final org.apache.shiro.subject.Subject shiroSubject; /** @@ -29,7 +29,7 @@ public class ShiroSubject implements Subject { * @param authTokenHandler Used to extract auth header info * @param subject The specific subject being authc/z'd */ - public ShiroSubject(final AuthTokenHandler authTokenHandler, final org.apache.shiro.subject.Subject subject) { + public ShiroSubject(final ShiroTokenManager authTokenHandler, final org.apache.shiro.subject.Subject subject) { this.authTokenHandler = Objects.requireNonNull(authTokenHandler); this.shiroSubject = Objects.requireNonNull(subject); } diff --git a/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroTokenManager.java b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroTokenManager.java new file mode 100644 index 0000000000000..110095a5cd4ef --- /dev/null +++ b/plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroTokenManager.java @@ -0,0 +1,135 @@ +/* + * 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.identity.shiro; + +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.opensearch.common.Randomness; +import org.opensearch.identity.IdentityService; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.identity.tokens.TokenManager; +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.PasswordGenerator; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Extracts Shiro's {@link AuthenticationToken} from different types of auth headers + * + * @opensearch.experimental + */ +class ShiroTokenManager implements TokenManager { + + private static final Logger log = LogManager.getLogger(IdentityService.class); + + private static Map shiroTokenPasswordMap = new HashMap<>(); + + /** + * Translates into shiro auth token from the given header token + * @param authenticationToken the token from which to translate + * @return An optional of the shiro auth token for login + */ + public Optional translateAuthToken(org.opensearch.identity.tokens.AuthToken authenticationToken) { + if (authenticationToken instanceof BasicAuthToken) { + final BasicAuthToken basicAuthToken = (BasicAuthToken) authenticationToken; + return Optional.of(new UsernamePasswordToken(basicAuthToken.getUser(), basicAuthToken.getPassword())); + } + + return Optional.empty(); + } + + @Override + public AuthToken issueToken(String audience) { + + String password = generatePassword(); + final byte[] rawEncoded = Base64.getEncoder().encode((audience + ":" + password).getBytes(UTF_8)); + final String usernamePassword = new String(rawEncoded, UTF_8); + final String header = "Basic " + usernamePassword; + BasicAuthToken token = new BasicAuthToken(header); + shiroTokenPasswordMap.put(token, password); + + return token; + } + + public boolean validateToken(AuthToken token) { + if (token instanceof BasicAuthToken) { + final BasicAuthToken basicAuthToken = (BasicAuthToken) token; + return basicAuthToken.getUser().equals(SecurityUtils.getSubject().toString()) + && basicAuthToken.getPassword().equals(shiroTokenPasswordMap.get(basicAuthToken)); + } + return false; + } + + public String getTokenInfo(AuthToken token) { + if (token instanceof BasicAuthToken) { + final BasicAuthToken basicAuthToken = (BasicAuthToken) token; + return basicAuthToken.toString(); + } + throw new UnsupportedAuthenticationToken(); + } + + public void revokeToken(AuthToken token) { + if (token instanceof BasicAuthToken) { + final BasicAuthToken basicAuthToken = (BasicAuthToken) token; + basicAuthToken.revoke(); + return; + } + throw new UnsupportedAuthenticationToken(); + } + + public void resetToken(AuthToken token) { + if (token instanceof BasicAuthToken) { + final BasicAuthToken basicAuthToken = (BasicAuthToken) token; + basicAuthToken.revoke(); + } + } + + /** + * When the ShiroTokenManager is in use, a random password is generated for each token and is then output to the logs. + * The password is used for development only. + * @return A randomly generated password for development + */ + public String generatePassword() { + + CharacterRule lowercaseCharacterRule = new CharacterRule(EnglishCharacterData.LowerCase, 1); + CharacterRule uppercaseCharacterRule = new CharacterRule(EnglishCharacterData.UpperCase, 1); + CharacterRule numericCharacterRule = new CharacterRule(EnglishCharacterData.Digit, 1); + CharacterRule specialCharacterRule = new CharacterRule(EnglishCharacterData.Special, 1); + + List rules = Arrays.asList( + lowercaseCharacterRule, + uppercaseCharacterRule, + numericCharacterRule, + specialCharacterRule + ); + PasswordGenerator passwordGenerator = new PasswordGenerator(); + + Random random = Randomness.get(); + + String password = passwordGenerator.generatePassword(random.nextInt(8) + 8, rules); // Generate a 8 to 16 char password + log.info("Generated password: " + password); + return password; + } + + Map getShiroTokenPasswordMap() { + return shiroTokenPasswordMap; + } + +} diff --git a/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/AuthTokenHandlerTests.java b/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/AuthTokenHandlerTests.java index 942d777df2086..540fed368aeda 100644 --- a/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/AuthTokenHandlerTests.java +++ b/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/AuthTokenHandlerTests.java @@ -8,32 +8,41 @@ package org.opensearch.identity.shiro; +import java.util.Optional; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; +import org.junit.Before; +import org.opensearch.identity.noop.NoopTokenManager; +import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.BasicAuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; import org.opensearch.test.OpenSearchTestCase; -import org.junit.Before; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.instanceOf; +import org.passay.CharacterCharacteristicsRule; +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.LengthRule; +import org.passay.PasswordData; +import org.passay.PasswordValidator; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import java.util.Optional; - public class AuthTokenHandlerTests extends OpenSearchTestCase { - private AuthTokenHandler authTokenHandler; + private ShiroTokenManager shiroAuthTokenHandler; + private NoopTokenManager noopTokenManager; @Before public void testSetup() { - authTokenHandler = new AuthTokenHandler(); + shiroAuthTokenHandler = new ShiroTokenManager(); + noopTokenManager = new NoopTokenManager(); } public void testShouldExtractBasicAuthTokenSuccessfully() { final BasicAuthToken authToken = new BasicAuthToken("Basic YWRtaW46YWRtaW4="); // admin:admin - final AuthenticationToken translatedToken = authTokenHandler.translateAuthToken(authToken).get(); + final AuthenticationToken translatedToken = shiroAuthTokenHandler.translateAuthToken(authToken).get(); assertThat(translatedToken, is(instanceOf(UsernamePasswordToken.class))); final UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) translatedToken; @@ -45,7 +54,7 @@ public void testShouldExtractBasicAuthTokenSuccessfully() { public void testShouldExtractBasicAuthTokenSuccessfully_twoSemiColonPassword() { final BasicAuthToken authToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA=="); // test:te:st - final AuthenticationToken translatedToken = authTokenHandler.translateAuthToken(authToken).get(); + final AuthenticationToken translatedToken = shiroAuthTokenHandler.translateAuthToken(authToken).get(); assertThat(translatedToken, is(instanceOf(UsernamePasswordToken.class))); final UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) translatedToken; @@ -55,8 +64,84 @@ public void testShouldExtractBasicAuthTokenSuccessfully_twoSemiColonPassword() { } public void testShouldReturnNullWhenExtractingNullToken() { - final Optional translatedToken = authTokenHandler.translateAuthToken(null); + final Optional translatedToken = shiroAuthTokenHandler.translateAuthToken(null); assertThat(translatedToken.isEmpty(), is(true)); } + + public void testShouldRevokeTokenSuccessfully() { + final BasicAuthToken authToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA=="); + assertTrue(authToken.toString().equals("Basic auth token with user=test, password=te:st")); + shiroAuthTokenHandler.revokeToken(authToken); + assert (authToken.toString().equals("Basic auth token with user=, password=")); + } + + public void testShouldResetTokenSuccessfully() { + final BasicAuthToken authToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA=="); + assertTrue(authToken.toString().equals("Basic auth token with user=test, password=te:st")); + shiroAuthTokenHandler.resetToken(authToken); + assert (authToken.toString().equals("Basic auth token with user=, password=")); + } + + public void testShouldFailWhenRevokeToken() { + final BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); + assert (bearerAuthToken.getTokenIdentifier().equals("Bearer")); + assertThrows(UnsupportedAuthenticationToken.class, () -> shiroAuthTokenHandler.revokeToken(bearerAuthToken)); + } + + public void testShouldFailGetTokenInfo() { + final BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); + assert (bearerAuthToken.getTokenIdentifier().equals("Bearer")); + assertThrows(UnsupportedAuthenticationToken.class, () -> shiroAuthTokenHandler.getTokenInfo(bearerAuthToken)); + } + + public void testShouldFailValidateToken() { + final BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); + assertFalse(shiroAuthTokenHandler.validateToken(bearerAuthToken)); + } + + public void testShoudPassMapLookupWithToken() { + final BasicAuthToken authToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA=="); + shiroAuthTokenHandler.getShiroTokenPasswordMap().put(authToken, "te:st"); + assertTrue(authToken.getPassword().equals(shiroAuthTokenHandler.getShiroTokenPasswordMap().get(authToken))); + } + + public void testShouldPassThrougbResetToken(AuthToken token) { + final BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature"); + shiroAuthTokenHandler.resetToken(bearerAuthToken); + } + + public void testVerifyBearerTokenObject() { + BearerAuthToken testGoodToken = new BearerAuthToken("header.payload.signature"); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new BearerAuthToken("asddfhadfasdfad")); + assert (exception.getMessage().contains("Illegally formed bearer authorization token ")); + assertEquals(testGoodToken.getCompleteToken(), "header.payload.signature"); + assertEquals(testGoodToken.getTokenIdentifier(), "Bearer"); + assertEquals(testGoodToken.getHeader(), "header"); + assertEquals(testGoodToken.getPayload(), "payload"); + assertEquals(testGoodToken.getSignature(), "signature"); + assertEquals(testGoodToken.toString(), "Bearer auth token with header=header, payload=payload, signature=signature"); + } + + public void testGeneratedPasswordContents() { + String password = shiroAuthTokenHandler.generatePassword(); + PasswordData data = new PasswordData(password); + + LengthRule lengthRule = new LengthRule(8, 16); + + CharacterCharacteristicsRule characteristicsRule = new CharacterCharacteristicsRule(); + + // Define M (3 in this case) + characteristicsRule.setNumberOfCharacteristics(3); + + // Define elements of N (upper, lower, digit, symbol) + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.UpperCase, 1)); + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.LowerCase, 1)); + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.Digit, 1)); + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.Special, 1)); + + PasswordValidator validator = new PasswordValidator(lengthRule, characteristicsRule); + validator.validate(data); + } + } diff --git a/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/ShiroIdentityPluginTests.java b/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/ShiroIdentityPluginTests.java new file mode 100644 index 0000000000000..f06dff7eea382 --- /dev/null +++ b/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/ShiroIdentityPluginTests.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.identity.shiro; + +import java.util.List; +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.IdentityService; +import org.opensearch.plugins.IdentityPlugin; +import org.opensearch.test.OpenSearchTestCase; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThrows; + +public class ShiroIdentityPluginTests extends OpenSearchTestCase { + + public void testSingleIdentityPluginSucceeds() { + IdentityPlugin identityPlugin1 = new ShiroIdentityPlugin(Settings.EMPTY); + List pluginList1 = List.of(identityPlugin1); + IdentityService identityService1 = new IdentityService(Settings.EMPTY, pluginList1); + assertThat(identityService1.getTokenManager(), is(instanceOf(ShiroTokenManager.class))); + } + + public void testMultipleIdentityPluginsFail() { + IdentityPlugin identityPlugin1 = new ShiroIdentityPlugin(Settings.EMPTY); + IdentityPlugin identityPlugin2 = new ShiroIdentityPlugin(Settings.EMPTY); + IdentityPlugin identityPlugin3 = new ShiroIdentityPlugin(Settings.EMPTY); + List pluginList = List.of(identityPlugin1, identityPlugin2, identityPlugin3); + Exception ex = assertThrows(OpenSearchException.class, () -> new IdentityService(Settings.EMPTY, pluginList)); + assert (ex.getMessage().contains("Multiple identity plugins are not supported,")); + } + +} diff --git a/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/ShiroSubjectTests.java b/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/ShiroSubjectTests.java index baf6090f79ff3..930945e9a2d8d 100644 --- a/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/ShiroSubjectTests.java +++ b/plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/ShiroSubjectTests.java @@ -25,13 +25,13 @@ public class ShiroSubjectTests extends OpenSearchTestCase { private org.apache.shiro.subject.Subject shiroSubject; - private AuthTokenHandler authTokenHandler; + private ShiroTokenManager authTokenHandler; private ShiroSubject subject; @Before public void setup() { shiroSubject = mock(org.apache.shiro.subject.Subject.class); - authTokenHandler = mock(AuthTokenHandler.class); + authTokenHandler = mock(ShiroTokenManager.class); subject = new ShiroSubject(authTokenHandler, shiroSubject); } diff --git a/server/src/main/java/org/opensearch/identity/IdentityService.java b/server/src/main/java/org/opensearch/identity/IdentityService.java index e7e785ba75231..ab1456cd860ac 100644 --- a/server/src/main/java/org/opensearch/identity/IdentityService.java +++ b/server/src/main/java/org/opensearch/identity/IdentityService.java @@ -11,6 +11,7 @@ import org.opensearch.identity.noop.NoopIdentityPlugin; import java.util.List; import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.TokenManager; import org.opensearch.plugins.IdentityPlugin; import java.util.stream.Collectors; @@ -48,4 +49,11 @@ public IdentityService(final Settings settings, final List ident public Subject getSubject() { return identityPlugin.getSubject(); } + + /** + * Gets the token manager + */ + public TokenManager getTokenManager() { + return identityPlugin.getTokenManager(); + } } diff --git a/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java b/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java index 437c059684565..c6ed8d57da435 100644 --- a/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java +++ b/server/src/main/java/org/opensearch/identity/noop/NoopIdentityPlugin.java @@ -8,6 +8,7 @@ package org.opensearch.identity.noop; +import org.opensearch.identity.tokens.TokenManager; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.identity.Subject; @@ -29,4 +30,12 @@ public Subject getSubject() { return new NoopSubject(); } + /** + * Get a new NoopTokenManager + * @return Must never return null + */ + @Override + public TokenManager getTokenManager() { + return new NoopTokenManager(); + } } diff --git a/server/src/main/java/org/opensearch/identity/noop/NoopTokenManager.java b/server/src/main/java/org/opensearch/identity/noop/NoopTokenManager.java new file mode 100644 index 0000000000000..a55f28e02a8aa --- /dev/null +++ b/server/src/main/java/org/opensearch/identity/noop/NoopTokenManager.java @@ -0,0 +1,33 @@ +/* + * 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.identity.noop; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.identity.IdentityService; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.TokenManager; + +/** + * This class represents a Noop Token Manager + */ +public class NoopTokenManager implements TokenManager { + + private static final Logger log = LogManager.getLogger(IdentityService.class); + + /** + * Issue a new Noop Token + * @return a new Noop Token + */ + @Override + public AuthToken issueToken(String audience) { + return new AuthToken() { + }; + } +} diff --git a/server/src/main/java/org/opensearch/identity/tokens/BasicAuthToken.java b/server/src/main/java/org/opensearch/identity/tokens/BasicAuthToken.java index e5000cb3d6965..9cd6cb6b6208a 100644 --- a/server/src/main/java/org/opensearch/identity/tokens/BasicAuthToken.java +++ b/server/src/main/java/org/opensearch/identity/tokens/BasicAuthToken.java @@ -16,13 +16,13 @@ */ public final class BasicAuthToken implements AuthToken { - public final static String TOKEN_IDENIFIER = "Basic"; + public final static String TOKEN_IDENTIFIER = "Basic"; private String user; private String password; public BasicAuthToken(final String headerValue) { - final String base64Encoded = headerValue.substring(TOKEN_IDENIFIER.length()).trim(); + final String base64Encoded = headerValue.substring(TOKEN_IDENTIFIER.length()).trim(); final byte[] rawDecoded = Base64.getDecoder().decode(base64Encoded); final String usernamepassword = new String(rawDecoded, StandardCharsets.UTF_8); @@ -41,4 +41,14 @@ public String getUser() { public String getPassword() { return password; } + + @Override + public String toString() { + return "Basic auth token with user=" + user + ", password=" + password; + } + + public void revoke() { + this.password = ""; + this.user = ""; + } } diff --git a/server/src/main/java/org/opensearch/identity/tokens/BearerAuthToken.java b/server/src/main/java/org/opensearch/identity/tokens/BearerAuthToken.java new file mode 100644 index 0000000000000..eac164af1c5d3 --- /dev/null +++ b/server/src/main/java/org/opensearch/identity/tokens/BearerAuthToken.java @@ -0,0 +1,61 @@ +/* + * 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.identity.tokens; + +/** + * Bearer (JWT) Authentication Token in a http request header + */ +public class BearerAuthToken implements AuthToken { + + public final String DELIMITER = "\\."; + public final static String TOKEN_IDENTIFIER = "Bearer"; + + private String header; + private String payload; + private String signature; + + private String completeToken; + + public BearerAuthToken(final String token) { + + String[] tokenComponents = token.split(DELIMITER); + if (tokenComponents.length != 3) { + throw new IllegalArgumentException("Illegally formed bearer authorization token " + token); + } + completeToken = token; + header = tokenComponents[0]; + payload = tokenComponents[1]; + signature = tokenComponents[2]; + } + + public String getHeader() { + return header; + } + + public String getPayload() { + return payload; + } + + public String getSignature() { + return signature; + } + + public String getCompleteToken() { + return completeToken; + } + + public String getTokenIdentifier() { + return TOKEN_IDENTIFIER; + } + + @Override + public String toString() { + return "Bearer auth token with header=" + header + ", payload=" + payload + ", signature=" + signature; + } +} diff --git a/server/src/main/java/org/opensearch/identity/tokens/RestTokenExtractor.java b/server/src/main/java/org/opensearch/identity/tokens/RestTokenExtractor.java index eaeacdb240fd9..ae200c7461a60 100644 --- a/server/src/main/java/org/opensearch/identity/tokens/RestTokenExtractor.java +++ b/server/src/main/java/org/opensearch/identity/tokens/RestTokenExtractor.java @@ -40,7 +40,7 @@ public static AuthToken extractToken(final RestRequest request) { if (authHeaderValue.isPresent()) { final String authHeaderValueStr = authHeaderValue.get(); - if (authHeaderValueStr.startsWith(BasicAuthToken.TOKEN_IDENIFIER)) { + if (authHeaderValueStr.startsWith(BasicAuthToken.TOKEN_IDENTIFIER)) { return new BasicAuthToken(authHeaderValueStr); } else { if (logger.isDebugEnabled()) { diff --git a/server/src/main/java/org/opensearch/identity/tokens/TokenManager.java b/server/src/main/java/org/opensearch/identity/tokens/TokenManager.java new file mode 100644 index 0000000000000..029ce430e7532 --- /dev/null +++ b/server/src/main/java/org/opensearch/identity/tokens/TokenManager.java @@ -0,0 +1,22 @@ +/* + * 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.identity.tokens; + +/** + * This interface defines the expected methods of a token manager + */ +public interface TokenManager { + + /** + * Create a new auth token + * @param audience: The audience for the token + * @return A new auth token + */ + public AuthToken issueToken(String audience); +} diff --git a/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java b/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java index 511b5595c5328..00f3f8aff585c 100644 --- a/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java +++ b/server/src/main/java/org/opensearch/plugins/IdentityPlugin.java @@ -9,6 +9,7 @@ package org.opensearch.plugins; import org.opensearch.identity.Subject; +import org.opensearch.identity.tokens.TokenManager; /** * Plugin that provides identity and access control for OpenSearch @@ -22,5 +23,12 @@ public interface IdentityPlugin { * * Should never return null * */ - Subject getSubject(); + public Subject getSubject(); + + /** + * Get the Identity Plugin's token manager implementation + * + * Should never return null + */ + public TokenManager getTokenManager(); } diff --git a/server/src/test/java/org/opensearch/bootstrap/IdentityPluginTests.java b/server/src/test/java/org/opensearch/bootstrap/IdentityPluginTests.java index 5595fdd564d01..b84a9a87ec77e 100644 --- a/server/src/test/java/org/opensearch/bootstrap/IdentityPluginTests.java +++ b/server/src/test/java/org/opensearch/bootstrap/IdentityPluginTests.java @@ -13,16 +13,20 @@ import org.opensearch.common.settings.Settings; import org.opensearch.identity.IdentityService; import org.opensearch.identity.noop.NoopIdentityPlugin; +import org.opensearch.identity.noop.NoopTokenManager; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.test.OpenSearchTestCase; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; public class IdentityPluginTests extends OpenSearchTestCase { public void testSingleIdentityPluginSucceeds() { IdentityPlugin identityPlugin1 = new NoopIdentityPlugin(); - List pluginList = List.of(identityPlugin1); - IdentityService identityService = new IdentityService(Settings.EMPTY, pluginList); - assertTrue(identityService.getSubject().getPrincipal().getName().equalsIgnoreCase("Unauthenticated")); + List pluginList1 = List.of(identityPlugin1); + IdentityService identityService1 = new IdentityService(Settings.EMPTY, pluginList1); + assertTrue(identityService1.getSubject().getPrincipal().getName().equalsIgnoreCase("Unauthenticated")); + assertThat(identityService1.getTokenManager(), is(instanceOf(NoopTokenManager.class))); } public void testMultipleIdentityPluginsFail() {