diff --git a/.github/actions/create-bwc-build/action.yaml b/.github/actions/create-bwc-build/action.yaml index fcfa612a7d..a2bf324cdc 100644 --- a/.github/actions/create-bwc-build/action.yaml +++ b/.github/actions/create-bwc-build/action.yaml @@ -35,7 +35,7 @@ runs: - name: Build uses: gradle/gradle-build-action@v2 with: - arguments: assemble -Dbuild.snapshot=false + arguments: assemble build-root-directory: ${{ inputs.plugin-branch }} - id: get-opensearch-version @@ -46,5 +46,5 @@ runs: - name: Copy current distro into the expected folder run: | mkdir -p ./bwc-test/src/test/resources/${{ steps.get-opensearch-version.outputs.version }} - cp ${{ inputs.plugin-branch }}/build/distributions/opensearch-security-${{ steps.get-opensearch-version.outputs.version }}.zip ./bwc-test/src/test/resources/${{ steps.get-opensearch-version.outputs.version }} + cp ${{ inputs.plugin-branch }}/build/distributions/opensearch-security-${{ steps.get-opensearch-version.outputs.version }}-SNAPSHOT.zip ./bwc-test/src/test/resources/${{ steps.get-opensearch-version.outputs.version }} shell: bash diff --git a/bwc-test/build.gradle b/bwc-test/build.gradle index 9f5c752287..68b9f27e04 100644 --- a/bwc-test/build.gradle +++ b/bwc-test/build.gradle @@ -84,8 +84,8 @@ String baseName = "securityBwcCluster" String bwcFilePath = "src/test/resources/" String projectVersion = nextVersion -String previousOpenSearch = extractVersion(previousVersion); -String nextOpenSearch = extractVersion(nextVersion); +String previousOpenSearch = extractVersion(previousVersion) + "-SNAPSHOT"; +String nextOpenSearch = extractVersion(nextVersion) + "-SNAPSHOT"; println previousOpenSearch + nextOpenSearch; diff --git a/release-notes/opensearch-security.release-notes-2.9.0.0.md b/release-notes/opensearch-security.release-notes-2.9.0.0.md new file mode 100644 index 0000000000..281fa1b488 --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.9.0.0.md @@ -0,0 +1,32 @@ +## 2023-07-18 Version 2.9.0.0 + +Compatible with OpenSearch 2.9.0 + +### Enhancements + +* Use boucycastle PEM reader instead of reg expression ([#2877](https://github.com/opensearch-project/security/pull/2877)) +* Adding field level security test cases for FlatFields ([#2876](https://github.com/opensearch-project/security/pull/2876)) ([#2893](https://github.com/opensearch-project/security/pull/2893)) +* Add password message to /dashboardsinfo endpoint ([#2949](https://github.com/opensearch-project/security/pull/2949)) ([#2955](https://github.com/opensearch-project/security/pull/2955)) +* Add .plugins-ml-connector to system index ([#2947](https://github.com/opensearch-project/security/pull/2947)) ([#2954](https://github.com/opensearch-project/security/pull/2954)) +* Parallel test jobs for CI ([#2861](https://github.com/opensearch-project/security/pull/2861)) ([#2936](https://github.com/opensearch-project/security/pull/2936)) +* Adds a check to skip serialization-deserialization if request is for same node ([#2765](https://github.com/opensearch-project/security/pull/2765)) ([#2973](https://github.com/opensearch-project/security/pull/2973)) +* Add workflow cluster permissions to alerting roles and add .plugins-ml-config in the system index ([#2996](https://github.com/opensearch-project/security/pull/2996)) + +### Maintenance + +* Match version of zstd-jni from core ([#2835](https://github.com/opensearch-project/security/pull/2835)) +* Add Andrey Pleskach (Willyborankin) to Maintainers ([#2843](https://github.com/opensearch-project/security/pull/2843)) +* Updates bwc versions to latest release ([#2849](https://github.com/opensearch-project/security/pull/2849)) +* Add search model group permission to ml_read_access role ([#2855](https://github.com/opensearch-project/security/pull/2855)) ([#2858](https://github.com/opensearch-project/security/pull/2858)) +* Format 2.x ([#2878](https://github.com/opensearch-project/security/pull/2878)) +* Update snappy to 1.1.10.1 and guava to 32.0.1-jre ([#2886](https://github.com/opensearch-project/security/pull/2886)) ([#2889](https://github.com/opensearch-project/security/pull/2889)) +* Resolve ImmutableOpenMap issue from core refactor ([#2908](https://github.com/opensearch-project/security/pull/2908)) +* Misc changes ([#2902](https://github.com/opensearch-project/security/pull/2902)) ([#2904](https://github.com/opensearch-project/security/pull/2904)) +* Bump BouncyCastle from jdk15on to jdk15to18 ([#2901](https://github.com/opensearch-project/security/pull/2901)) ([#2917](https://github.com/opensearch-project/security/pull/2917)) +* Fix the import org.opensearch.core.common.Strings; and import org.opensearch.core.common.logging.LoggerMessageFormat; ([#2953](https://github.com/opensearch-project/security/pull/2953)) +* Remove commons-collections 3.2.2 ([#2924](https://github.com/opensearch-project/security/pull/2924)) ([#2957](https://github.com/opensearch-project/security/pull/2957)) +* Resolve CVE-2023-2976 by forcing use of Guava 32.0.1 ([#2937](https://github.com/opensearch-project/security/pull/2937)) ([#2974](https://github.com/opensearch-project/security/pull/2974)) +* Bump jaxb to 2.3.8 ([#2977](https://github.com/opensearch-project/security/pull/2977)) ([#2979](https://github.com/opensearch-project/security/pull/2979)) +* Update Gradle to 8.2.1 ([#2978](https://github.com/opensearch-project/security/pull/2978)) ([#2981](https://github.com/opensearch-project/security/pull/2981)) +* Changed maven repo location for compatibility check ([#2988](https://github.com/opensearch-project/security/pull/2988)) +* Bump guava to 32.1.1-jre ([#2976](https://github.com/opensearch-project/security/pull/2976)) ([#2990](https://github.com/opensearch-project/security/pull/2990)) diff --git a/scripts/integtest.sh b/scripts/integtest.sh index 961b91299c..0401d00fa0 100755 --- a/scripts/integtest.sh +++ b/scripts/integtest.sh @@ -20,7 +20,7 @@ function usage() { echo -e "-v OPENSEARCH_VERSION\t, no defaults" echo -e "-n SNAPSHOT\t, defaults to false" echo -e "-m CLUSTER_NAME\t, defaults to docker-cluster" - echo -e "-u COMMON_UTILS_VERSION\t, defaults to 2.2.0.0" + echo -e "-u COMMON_UTILS_VERSION\t, defaults to 3.0.0.0-SNAPSHOT" echo "--------------------------------------------------------------------------" } @@ -101,7 +101,7 @@ then fi if [ -z "$COMMON_UTILS_VERSION" ] then - COMMON_UTILS_VERSION="2.2.0.0" + COMMON_UTILS_VERSION="3.0.0.0-SNAPSHOT" fi USERNAME=`echo $CREDENTIAL | awk -F ':' '{print $1}'` diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 1dbc787b74..6539aca68c 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -100,12 +100,16 @@ import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; import org.opensearch.core.index.Index; +import org.opensearch.identity.Subject; +import org.opensearch.identity.tokens.TokenManager; import org.opensearch.index.IndexModule; import org.opensearch.index.cache.query.QueryCache; import org.opensearch.indices.IndicesService; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.indices.breaker.CircuitBreakerService; import org.opensearch.plugins.ClusterPlugin; +import org.opensearch.plugins.ExtensionAwarePlugin; +import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; @@ -145,6 +149,8 @@ import org.opensearch.security.http.SecurityHttpServerTransport; import org.opensearch.security.http.SecurityNonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.identity.SecuritySubject; +import org.opensearch.security.identity.SecurityTokenManager; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; @@ -193,7 +199,12 @@ import org.opensearch.watcher.ResourceWatcherService; // CS-ENFORCE-SINGLE -public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin { +public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin + implements + ClusterPlugin, + MapperPlugin, + ExtensionAwarePlugin, + IdentityPlugin { private static final String KEYWORD = ".keyword"; private static final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); @@ -212,6 +223,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; private volatile ClusterService cs; + private volatile SecuritySubject subject = new SecuritySubject(); + private volatile SecurityTokenManager tokenManager; private static volatile DiscoveryNode localNode; private volatile AuditLog auditLog; private volatile BackendRegistry backendRegistry; @@ -226,6 +239,13 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile Salt salt; private volatile OpensearchDynamicSetting transportPassiveAuthSetting; + public static Setting RESERVED_INDICES_SETTING = Setting.listSetting( + "reserved_indices", + List.of(), + Function.identity(), + Property.ExtensionScope + ); + public static boolean isActionTraceEnabled() { return actionTrace.isTraceEnabled(); } @@ -990,6 +1010,9 @@ public Collection createComponents( cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); + subject.setThreadContext(threadPool.getThreadContext()); + tokenManager = new SecurityTokenManager(cs); + userService = new UserService(cs, cr, settings, localClient); final XFFResolver xffResolver = new XFFResolver(threadPool); @@ -1107,6 +1130,13 @@ public Settings additionalSettings() { return builder.build(); } + @Override + public List> getExtensionSettings() { + List> settings = new ArrayList>(); + settings.add(RESERVED_INDICES_SETTING); + return settings; + } + @Override public List> getSettings() { List> settings = new ArrayList>(); @@ -1894,6 +1924,16 @@ public static void setLocalNode(DiscoveryNode node) { localNode = node; } + @Override + public Subject getSubject() { + return subject; + } + + @Override + public TokenManager getTokenManager() { + return tokenManager; + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java new file mode 100644 index 0000000000..e38a48cde3 --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.authtoken.jwt; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class EncryptionDecryptionUtil { + + public static String encrypt(final String secret, final String data) { + 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, 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, 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(mode.opmode, originalKey); + return cipher; + } catch (final Exception e) { + 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); + } catch (final Exception e) { + 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; + } + } +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java new file mode 100644 index 0000000000..c3b7fa4215 --- /dev/null +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.authtoken.jwt; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.function.LongSupplier; + +import com.google.common.base.Strings; +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; +import org.apache.cxf.rs.security.jose.jwk.PublicKeyUse; +import org.apache.cxf.rs.security.jose.jws.JwsUtils; +import org.apache.cxf.rs.security.jose.jwt.JoseJwtProducer; +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.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; + +public class JwtVendor { + private static final Logger logger = LogManager.getLogger(JwtVendor.class); + + private static JsonMapObjectReaderWriter jsonMapReaderWriter = new JsonMapObjectReaderWriter(); + + private final String claimsEncryptionKey; + private final JsonWebKey signingKey; + private final JoseJwtProducer jwtProducer; + private final LongSupplier timeProvider; + + public JwtVendor(final Settings settings, final Optional timeProvider) { + 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"); + } + if (timeProvider.isPresent()) { + this.timeProvider = timeProvider.get(); + } else { + this.timeProvider = () -> System.currentTimeMillis() / 1000; + } + } + + /* + * 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"); + + if (!Strings.isNullOrEmpty(signingKey)) { + + JsonWebKey jwk = new JsonWebKey(); + + jwk.setKeyType(KeyType.OCTET); + jwk.setAlgorithm("HS512"); + jwk.setPublicKeyUse(PublicKeyUse.SIGN); + jwk.setProperty("k", signingKey); + + return jwk; + } else { + Settings jwkSettings = settings.getAsSettings("jwt").getAsSettings("key"); + + if (jwkSettings.isEmpty()) { + throw new Exception("Settings for key is missing. Please specify at least the option signing_key with a shared secret."); + } + + JsonWebKey jwk = new JsonWebKey(); + + for (String key : jwkSettings.keySet()) { + jwk.setProperty(key, jwkSettings.get(key)); + } + + return jwk; + } + } + + 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()); + + jwtProducer.setSignatureProvider(JwsUtils.getSignatureProvider(signingKey)); + JwtClaims jwtClaims = new JwtClaims(); + JwtToken jwt = new JwtToken(jwtClaims); + + jwtClaims.setIssuer(issuer); + + jwtClaims.setIssuedAt(timeMillis); + + jwtClaims.setSubject(subject); + + jwtClaims.setAudience(audience); + + jwtClaims.setNotBefore(timeMillis); + + if (expirySeconds == null) { + long expiryTime = timeProvider.getAsLong() + 300; + jwtClaims.setExpiryTime(expiryTime); + } else if (expirySeconds > 0) { + long expiryTime = timeProvider.getAsLong() + expirySeconds; + jwtClaims.setExpiryTime(expiryTime); + } else { + throw new Exception("The expiration time should be a positive integer"); + } + + if (roles != null) { + String listOfRoles = String.join(",", roles); + jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); + } else { + throw new Exception("Roles cannot be null"); + } + + /* TODO: If the backendRoles is not null and the BWC Mode is on, put them into the "dbr" claim */ + + String encodedJwt = jwtProducer.processJwt(jwt); + + if (logger.isDebugEnabled()) { + logger.debug( + "Created JWT: " + + encodedJwt + + "\n" + + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) + + "\n" + + JwtUtils.claimsToJson(jwt.getClaims()) + ); + } + + return encodedJwt; + } + + public String issueServiceAccountToken(String issuer, String subject, Integer expirySeconds) throws Exception { + long timeMillis = timeProvider.getAsLong(); + Instant now = Instant.ofEpochMilli(timeProvider.getAsLong()); + + jwtProducer.setSignatureProvider(JwsUtils.getSignatureProvider(signingKey)); + JwtClaims jwtClaims = new JwtClaims(); + JwtToken jwt = new JwtToken(jwtClaims); + + jwtClaims.setIssuer(issuer); + + jwtClaims.setIssuedAt(timeMillis); + + jwtClaims.setSubject(subject); + + jwtClaims.setProperty("typ", "sat"); + + jwtClaims.setNotBefore(timeMillis); + + if (expirySeconds == null) { + long expiryTime = timeProvider.getAsLong() + 300; + jwtClaims.setExpiryTime(expiryTime); + } else if (expirySeconds > 0) { + long expiryTime = timeProvider.getAsLong() + expirySeconds; + jwtClaims.setExpiryTime(expiryTime); + } else { + throw new Exception("The expiration time should be a positive integer"); + } + + /* TODO: If the backendRoles is not null and the BWC Mode is on, put them into the "dbr" claim */ + + String encodedJwt = jwtProducer.processJwt(jwt); + + if (logger.isDebugEnabled()) { + logger.debug( + "Created JWT: " + + encodedJwt + + "\n" + + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) + + "\n" + + JwtUtils.claimsToJson(jwt.getClaims()) + ); + } + + return encodedJwt; + } +} diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java new file mode 100644 index 0000000000..cf7412d498 --- /dev/null +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -0,0 +1,217 @@ +/* + * 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.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.security.WeakKeyException; +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; + +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; +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 { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + 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"; + + private final JwtParser jwtParser; + private final String encryptionKey; + + public OnBehalfOfAuthenticator(Settings settings) { + encryptionKey = settings.get("encryption_key"); + jwtParser = initParser(settings.get("signing_key")); + } + + private JwtParser initParser(final String signingKey) { + JwtParser _jwtParser = KeyUtil.keyAlgorithmCheck(signingKey, log); + if (_jwtParser != null) { + return _jwtParser; + } else { + throw new RuntimeException("Unable to find on behalf of authenticator signing key"); + } + } + + private List extractSecurityRolesFromClaims(Claims claims) { + Object rolesObject = ObjectUtils.firstNonNull(claims.get("er"), claims.get("dr")); + List roles; + + if (rolesObject == null) { + log.warn("This is a malformed On-behalf-of Token"); + roles = List.of(); + } 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); + } + roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).collect(Collectors.toList()); + } + + return roles; + } + + private String[] extractBackendRolesFromClaims(Claims claims) { + // 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) { + log.warn("This is a malformed On-behalf-of Token"); + backendRoles = new String[0]; + } else { + final String backendRolesClaim = backendRolesObject.toString(); + + // Extracting roles based on the compatibility mode + String decryptedBackendRoles = 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(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public AuthCredentials run() { + return extractCredentials0(request); + } + }); + + return creds; + } + + private AuthCredentials extractCredentials0(final RestRequest request) { + if (jwtParser == null) { + log.error("Missing Signing Key. JWT authentication will not work"); + return null; + } + + String jwtToken = request.header(HttpHeaders.AUTHORIZATION); + + if (jwtToken == null || jwtToken.length() == 0) { + if (log.isDebugEnabled()) { + log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); + } + return null; + } + + if (!BEARER.matcher(jwtToken).matches()) { + jwtToken = null; + } + + 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"); + } + } + + if (jwtToken == null) { + return null; + } + + try { + final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + + final String subject = claims.getSubject(); + if (Objects.isNull(subject)) { + log.error("Valid jwt on behalf of token with no subject"); + return null; + } + + if (claims.get("typ") != null && "sat".equals(claims.get("typ"))) { + System.out.println("Received Service Account Token for " + claims.getSubject()); + + final AuthCredentials ac = new AuthCredentials(subject, List.of()).markComplete(); + + return ac; + } + + final String audience = claims.getAudience(); + if (Objects.isNull(audience)) { + log.error("Valid jwt on behalf of token with no audience"); + return null; + } + + List roles = extractSecurityRolesFromClaims(claims); + String[] backendRoles = extractBackendRolesFromClaims(claims); + + final AuthCredentials ac = new AuthCredentials(subject, List.of()).markComplete(); + + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); + } + + return ac; + + } catch (WeakKeyException e) { + log.error("Cannot authenticate user with JWT because of ", e); + return null; + } catch (Exception e) { + e.printStackTrace(); + if (log.isDebugEnabled()) { + log.debug("Invalid or expired JWT token.", e); + } + return null; + } + } + + @Override + public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + return false; + } + + @Override + public String getType() { + return "onbehalfof_jwt"; + } + +} diff --git a/src/main/java/org/opensearch/security/identity/SecuritySubject.java b/src/main/java/org/opensearch/security/identity/SecuritySubject.java new file mode 100644 index 0000000000..c7b0a755db --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/SecuritySubject.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.identity; + +import java.security.Principal; + +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.identity.NamedPrincipal; +import org.opensearch.identity.Subject; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; + +public class SecuritySubject implements Subject { + + private ThreadContext threadContext; + + public SecuritySubject() {} + + public void setThreadContext(ThreadContext threadContext) { + this.threadContext = threadContext; + } + + @Override + public Principal getPrincipal() { + if (threadContext == null) { + return NamedPrincipal.UNAUTHENTICATED; + } + final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + if (user == null) { + return NamedPrincipal.UNAUTHENTICATED; + } + return new NamedPrincipal(user.getName()); + } + + @Override + public void authenticate(AuthToken authToken) { + // TODO implement this - replace with logic from SecurityRestFilter + } +} diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java new file mode 100644 index 0000000000..20e0b82417 --- /dev/null +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -0,0 +1,51 @@ +package org.opensearch.security.identity; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.identity.tokens.BearerAuthToken; +import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.authtoken.jwt.JwtVendor; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; + +public class SecurityTokenManager implements TokenManager { + + public static Settings DEMO_SETTINGS = Settings.builder() + .put( + "signing_key", + Base64.getEncoder() + .encodeToString( + "This is my super secret that no one in the universe will ever be able to guess in a bajillion years".getBytes( + StandardCharsets.UTF_8 + ) + ) + ) + .put("encryption_key", "encryptionKey") + .build(); + + private ClusterService cs; + + public SecurityTokenManager(ClusterService cs) { + this.cs = cs; + } + + private JwtVendor jwtVendor = new JwtVendor(DEMO_SETTINGS, Optional.empty()); + + @Override + public AuthToken issueToken(String s) { + return null; + } + + @Override + public AuthToken issueServiceAccountToken(String extensionUniqueId) throws OpenSearchSecurityException { + try { + return new BearerAuthToken(jwtVendor.issueServiceAccountToken(cs.getClusterName().value(), extensionUniqueId, null)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index a3738dadac..d9dec4378a 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -165,7 +165,7 @@ public PrivilegesEvaluator( this.clusterInfoHolder = clusterInfoHolder; this.irr = irr; snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); - securityIndexAccessEvaluator = new SecurityIndexAccessEvaluator(settings, auditLog, irr); + securityIndexAccessEvaluator = new SecurityIndexAccessEvaluator(settings, auditLog, irr, threadPool.getThreadContext()); protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); termsAggregationEvaluator = new TermsAggregationEvaluator(); pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); diff --git a/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java index 94b0478759..7e22a54163 100644 --- a/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -39,13 +40,19 @@ import org.opensearch.action.RealtimeRequest; import org.opensearch.action.search.SearchRequest; import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.extensions.ExtensionsSettings; +import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; import org.opensearch.tasks.Task; +import static org.opensearch.security.OpenSearchSecurityPlugin.RESERVED_INDICES_SETTING; + public class SecurityIndexAccessEvaluator { Logger log = LogManager.getLogger(this.getClass()); @@ -58,15 +65,22 @@ public class SecurityIndexAccessEvaluator { // for system-indices configuration private final WildcardMatcher systemIndexMatcher; + private final ThreadContext threadContext; private final boolean systemIndexEnabled; - public SecurityIndexAccessEvaluator(final Settings settings, AuditLog auditLog, IndexResolverReplacer irr) { + public SecurityIndexAccessEvaluator( + final Settings settings, + AuditLog auditLog, + IndexResolverReplacer irr, + ThreadContext threadContext + ) { this.securityIndex = settings.get( ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); this.auditLog = auditLog; this.irr = irr; + this.threadContext = threadContext; this.filterSecurityIndex = settings.getAsBoolean(ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, false); this.systemIndexMatcher = WildcardMatcher.from( settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT) @@ -145,8 +159,28 @@ public PrivilegesEvaluatorResponse evaluate( } return presponse; } else { + User authenticatedUser = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + System.out.println("Authenticated User is " + authenticatedUser.getName()); + Optional matchingExtension = OpenSearchSecurityPlugin.GuiceHolder.getExtensionsManager() + .lookupExtensionSettingsById(authenticatedUser.getName()); + System.out.println("Matching Extension " + matchingExtension); auditLog.logSecurityIndexAttempt(request, action, task); final String foundSystemIndexes = getProtectedIndexes(requestedResolved).stream().collect(Collectors.joining(", ")); + if (matchingExtension.isPresent()) { + List reservedIndices = (List) matchingExtension.get() + .getAdditionalSettings() + .get(RESERVED_INDICES_SETTING); + System.out.println("Reserved indices for " + authenticatedUser.getName() + ": " + reservedIndices); + if (matchAllReservedIndices(requestedResolved, reservedIndices)) { + log.info( + "{} for '{}' index is allowed for service account of extension reserving the indices", + action, + foundSystemIndexes + ); + presponse.allowed = true; + return presponse.markComplete(); + } + } log.warn("{} for '{}' index is not allowed for a regular user", action, foundSystemIndexes); presponse.allowed = false; return presponse.markComplete(); @@ -189,4 +223,15 @@ private List getProtectedIndexes(final Resolved requestedResolved) { } return protectedIndexes; } + + private boolean matchAllReservedIndices(final Resolved requestedResolved, final List reservedIndices) { + final List requestedIndexes = requestedResolved.getAllIndices() + .stream() + .filter(securityIndex::equals) + .collect(Collectors.toList()); + if (systemIndexEnabled) { + return reservedIndices.containsAll(requestedIndexes); + } + return true; + } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 60637e4b8c..5f234dd251 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -57,6 +57,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.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; @@ -64,6 +66,8 @@ import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthzDomain; import org.opensearch.security.support.ReflectionHelper; +import static org.opensearch.security.identity.SecurityTokenManager.DEMO_SETTINGS; + public class DynamicConfigModelV7 extends DynamicConfigModel { private final ConfigV7 config; @@ -358,6 +362,14 @@ private void buildAAA() { } } + final AuthDomain _ad = new AuthDomain( + new NoOpAuthenticationBackend(Settings.EMPTY, null), + new OnBehalfOfAuthenticator(DEMO_SETTINGS), + false, + -1 + ); + restAuthDomains0.add(_ad); + List originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); 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..f6f8601222 --- /dev/null +++ b/src/main/java/org/opensearch/security/util/KeyUtil.java @@ -0,0 +1,82 @@ +/* + * 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 io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import org.apache.logging.log4j.Logger; + +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.Base64; +import java.util.Objects; + +public class KeyUtil { + + public static JwtParser keyAlgorithmCheck(final String signingKey, final Logger log) { + if (signingKey == null || signingKey.length() == 0) { + log.error("Unable to find signing key"); + return null; + } else { + try { + JwtParser parser = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public JwtParser run() { + 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()); + } + + if (Objects.nonNull(key)) { + return Jwts.parser().setSigningKey(key); + } + + return Jwts.parser().setSigningKey(decoded); + } + }); + + return parser; + } 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, + InvalidKeySpecException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance(algo); + return kf.generatePublic(spec); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java index a86c49d21d..c2d9454946 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java @@ -164,7 +164,7 @@ public void testCcs() throws Exception { Assert.assertFalse(ccs.getBody().contains("secret1")); Assert.assertFalse(ccs.getBody().contains("AnotherSecredField")); Assert.assertFalse(ccs.getBody().contains("xxx1")); - Assert.assertEquals(ccs.getHeaders().toString(), 2, ccs.getHeaders().size()); + Assert.assertEquals(ccs.getHeaders().toString(), 3, ccs.getHeaders().size()); } @Test @@ -251,7 +251,7 @@ public void testCcsDifferentConfig() throws Exception { Assert.assertTrue(ccs.getBody().contains("__fn__crl2")); Assert.assertFalse(ccs.getBody().contains("secret1")); Assert.assertFalse(ccs.getBody().contains("AnotherSecredField")); - Assert.assertEquals(ccs.getHeaders().toString(), 2, ccs.getHeaders().size()); + Assert.assertEquals(ccs.getHeaders().toString(), 3, ccs.getHeaders().size()); } @Test @@ -382,6 +382,6 @@ public void testCcsDifferentConfigBoth() throws Exception { Assert.assertFalse(ccs.getBody().contains("secret1")); Assert.assertFalse(ccs.getBody().contains("AnotherSecredField")); Assert.assertTrue(ccs.getBody().contains("someoneelse")); - Assert.assertEquals(ccs.getHeaders().toString(), 2, ccs.getHeaders().size()); + Assert.assertEquals(ccs.getHeaders().toString(), 3, ccs.getHeaders().size()); } } diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java index a9361d275a..48f63ec555 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java @@ -111,7 +111,8 @@ public void testDls() throws Exception { ); Assert.assertTrue(res.getBody().contains("\"value\" : 1,\n \"relation")); Assert.assertTrue(res.getBody().contains("\"failed\" : 0")); - Assert.assertEquals(res.getHeaders().toString(), 2, res.getHeaders().size()); + + Assert.assertEquals(res.getHeaders().toString(), 3, res.getHeaders().size()); Assert.assertEquals( HttpStatus.SC_OK, diff --git a/src/test/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluatorTest.java index 14c5eabb73..1d96730fde 100644 --- a/src/test/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluatorTest.java @@ -25,6 +25,7 @@ import org.opensearch.action.search.SearchRequest; import org.opensearch.action.support.IndicesOptions; import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; @@ -54,6 +55,8 @@ public class SecurityIndexAccessEvaluatorTest { private PrivilegesEvaluatorResponse presponse; @Mock private Logger log; + @Mock + private ThreadContext threadContext; private SecurityIndexAccessEvaluator evaluator; @@ -68,7 +71,8 @@ public void before() { .put("plugins.security.system_indices.enabled", true) .build(), auditLog, - irr + irr, + threadContext ); evaluator.log = log;