Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Feature/Extension] Extension token handler #3034

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
import org.opensearch.security.http.SecurityHttpServerTransport;
import org.opensearch.security.http.SecurityNonSslHttpServerTransport;
import org.opensearch.security.http.XFFResolver;
import org.opensearch.security.identity.SecurityTokenManager;
import org.opensearch.security.privileges.PrivilegesEvaluator;
import org.opensearch.security.privileges.PrivilegesInterceptor;
import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator;
Expand Down Expand Up @@ -192,9 +193,13 @@
import org.opensearch.transport.TransportResponseHandler;
import org.opensearch.transport.TransportService;
import org.opensearch.watcher.ResourceWatcherService;

import org.opensearch.identity.Subject;
import org.opensearch.identity.tokens.TokenManager;
import org.opensearch.plugins.IdentityPlugin;
// CS-ENFORCE-SINGLE

public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin {
public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin, IdentityPlugin {

private static final String KEYWORD = ".keyword";
private static final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace");
Expand All @@ -208,6 +213,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
private volatile SecurityInterceptor si;
private volatile PrivilegesEvaluator evaluator;
private volatile UserService userService;
private volatile SecurityTokenManager securityTokenManager;
private volatile RestLayerPrivilegesEvaluator restLayerEvaluator;
private volatile ThreadPool threadPool;
private volatile ConfigurationRepository cr;
Expand Down Expand Up @@ -997,6 +1003,8 @@ public Collection<Object> createComponents(

userService = new UserService(cs, cr, settings, localClient);

securityTokenManager = new SecurityTokenManager(threadPool, clusterService, cr, localClient, settings, userService);

final XFFResolver xffResolver = new XFFResolver(threadPool);
backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool);

Expand Down Expand Up @@ -1890,6 +1898,24 @@ private static String handleKeyword(final String field) {
return field;
}

public static DiscoveryNode getLocalNode() {
return localNode;
}

public static void setLocalNode(DiscoveryNode node) {
localNode = node;
}
Comment on lines +1901 to +1907
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be removed. Check this PR for more details: https://github.com/opensearch-project/security/pull/3066/files


@Override
public Subject getSubject() {
return null;
}

@Override
public TokenManager getTokenManager() {
return securityTokenManager;
}

public static class GuiceHolder implements LifecycleComponent {

private static RepositoriesService repositoriesService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import java.io.IOException;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -159,6 +160,8 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C
return;
} catch (IOException ex) {
throw new IOException(ex);
} catch (NoSuchAlgorithmException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add this catch block to then throw RuntimeException?

throw new RuntimeException(e);
}

// for existing users, hash is optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import java.io.IOException;
import java.security.AccessController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.SecureRandom;
Expand All @@ -32,6 +34,7 @@
import org.apache.commons.lang3.tuple.Pair;
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;

import org.bouncycastle.util.encoders.Hex;
import org.opensearch.ExceptionsHelper;
import org.opensearch.OpenSearchParseException;
import org.opensearch.SpecialPermission;
Expand All @@ -50,6 +53,7 @@
import org.opensearch.security.DefaultObjectMapper;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.user.User;
import java.nio.charset.StandardCharsets;

import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION;

Expand Down Expand Up @@ -213,6 +217,19 @@ public static String hash(final char[] clearTextPassword) {
return hash;
}

/**
* This generates a SHA-256 hash for a given password.
* It is used for validating internal user tokens since we don't want to store the salt in the plugin.
* @param password The password to be hashed
* @return hash of the password
*/
public static String universalHash(String password) throws NoSuchAlgorithmException {

MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
return new String(Hex.encode(hash));
}

/**
* Generate field resource paths
* @param fields fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ public class OnBehalfOfAuthenticator implements HTTPAuthenticator {
private static final String BEARER_PREFIX = "bearer ";
private static final String TOKEN_TYPE_CLAIM = "typ";
private static final String TOKEN_TYPE = "obo";

private final JwtParser jwtParser;
private final String encryptionKey;
private final Boolean oboEnabled;
Expand Down Expand Up @@ -191,6 +190,12 @@ private AuthCredentials extractCredentials0(final RestRequest request) {

final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody();

final String tokenType = claims.get(TOKEN_TYPE_CLAIM).toString();
if (!tokenType.equals(TOKEN_TYPE)) {
log.error("This token is not verifying as an on-behalf-of token");
return null;
}

final String subject = claims.getSubject();
if (Objects.isNull(subject)) {
log.error("Valid jwt on behalf of token with no subject");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.identity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
import org.apache.cxf.rs.security.jose.jwt.JwtConstants;
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
import org.greenrobot.eventbus.Subscribe;
import org.opensearch.OpenSearchSecurityException;
import org.opensearch.client.Client;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.inject.Inject;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.common.transport.TransportAddress;
import org.opensearch.identity.tokens.AuthToken;
import org.opensearch.identity.tokens.BasicAuthToken;
import org.opensearch.identity.tokens.BearerAuthToken;
import org.opensearch.identity.tokens.TokenManager;
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.security.securityconf.impl.SecurityDynamicConfiguration;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.user.InternalUserTokenHandler;
import org.opensearch.security.user.User;
import org.opensearch.security.user.UserService;
import org.opensearch.security.user.UserServiceException;
import org.opensearch.security.user.UserTokenHandler;
import org.opensearch.threadpool.ThreadPool;
import org.apache.cxf.rs.security.jose.jwt.JwtConstants;

/**
* This class serves as a funneling implementation of the TokenManager interface.
* The class allows the Security Plugin to implement two separate types of token managers without requiring specific interfaces
* in the IdentityPlugin.
*/
public class SecurityTokenManager implements TokenManager {

Settings settings;

ThreadPool threadPool;

ClusterService clusterService;
Client client;
ConfigurationRepository configurationRepository;
UserService userService;
UserTokenHandler userTokenHandler;
InternalUserTokenHandler internalUserTokenHandler;

public final String TOKEN_NOT_SUPPORTED_MESSAGE = "The provided token type is not supported by the Security Plugin.";
private ConfigModel configModel;
private DynamicConfigModel dcm;
private JwtVendor vendor;

@Inject
public SecurityTokenManager(
ThreadPool threadPool,
ClusterService clusterService,
ConfigurationRepository configurationRepository,
Client client,
Settings settings,
UserService userService
) {
this.threadPool = threadPool;
this.clusterService = clusterService;
this.client = client;
this.configurationRepository = configurationRepository;
this.settings = settings;
this.userService = userService;
userTokenHandler = new UserTokenHandler(threadPool, clusterService, configurationRepository, client);
internalUserTokenHandler = new InternalUserTokenHandler(settings, userService);

}

@Override
public AuthToken issueOnBehalfOfToken(Map<String, Object> claims) {
String oboToken;

User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
if (user == null && !claims.containsKey(JwtConstants.CLAIM_AUDIENCE)) {
throw new OpenSearchSecurityException("On-behalf-of Token cannot be issued due to the missing of subject/audience.");
}

final TransportAddress caller = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS);

String issuer = clusterService.getClusterName().value();
String subject = user.getName();
String audience = (String) claims.get(JwtConstants.CLAIM_AUDIENCE);
Integer expirySeconds = null;
List<String> roles = new ArrayList<>(mapRoles(user, caller));
List<String> backendRoles = new ArrayList<>(user.getRoles());

try {
oboToken = vendor.createJwt(issuer, subject, audience, expirySeconds, roles, backendRoles);
} catch (Exception e) {
throw new RuntimeException(e);
}

return new BearerAuthToken(oboToken);
}

// TODO: IMPLEMENT ISSUE SERVICE ACCOUNT INTERFACE FIRST
@Override
public AuthToken issueServiceAccountToken(String pluginOrExtensionPrincipal) {
return null;
}

public boolean validateToken(AuthToken authToken) {

if (authToken instanceof BearerAuthToken) {
String encodedTokenString = authToken.getTokenValue();
JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(encodedTokenString);
JwtToken jwt = jwtConsumer.getJwtToken();
if ("obo".equals(jwt.getClaim("typ"))) {
return userTokenHandler.validateJustInTimeToken(authToken);
}
return userTokenHandler.validateToken(authToken);
}

if (authToken instanceof BasicAuthToken) {
return internalUserTokenHandler.validateToken(authToken);
}
throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE);
}

public String getTokenInfo(AuthToken authToken) {

if (authToken instanceof BearerAuthToken) {
return userTokenHandler.getTokenInfo(authToken);
}
if (authToken instanceof BasicAuthToken) {
return internalUserTokenHandler.getTokenInfo(authToken);
}
throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE);
}

public void revokeToken(AuthToken authToken) {
if (authToken instanceof BearerAuthToken) {
userTokenHandler.revokeToken(authToken);
return;
}
if (authToken instanceof BasicAuthToken) {
internalUserTokenHandler.revokeToken(authToken);
return;
}
throw new UserServiceException(TOKEN_NOT_SUPPORTED_MESSAGE);
}

/**
* Only for testing
*/
public void setJwtVendor(JwtVendor vendor) {
this.vendor = vendor;
}

/**
* Only for testing
*/
public void setInternalUserTokenHandler(InternalUserTokenHandler handler) {
this.internalUserTokenHandler = handler;
}

/**
* Only for testing
*/
public void setUserTokenHandler(UserTokenHandler handler) {
this.userTokenHandler = handler;
}

/**
* Load data for a given CType
* @param config CType whose data is to be loaded in-memory
* @return configuration loaded with given CType data
*/
protected final SecurityDynamicConfiguration<?> load(final CType config, boolean logComplianceEvent) {
SecurityDynamicConfiguration<?> loaded = configurationRepository.getConfigurationsFromIndex(
Collections.singleton(config),
logComplianceEvent
).get(config).deepClone();
return DynamicConfigFactory.addStatics(loaded);
}

public Set<String> mapRoles(final User user, final TransportAddress caller) {
return this.configModel.mapSecurityRoles(user, caller);
}

@Subscribe
public void onConfigModelChanged(ConfigModel configModel) {
this.configModel = configModel;
}

@Subscribe
public void onDynamicConfigModelChanged(DynamicConfigModel dcm) {
this.dcm = dcm;
if (dcm.getDynamicOnBehalfOfSettings().get("signing_key") != null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should extract: dcm.getDynamicOnBehalfOfSettings() to a variable

&& dcm.getDynamicOnBehalfOfSettings().get("encryption_key") != null) {
this.vendor = new JwtVendor(dcm.getDynamicOnBehalfOfSettings(), Optional.empty());
} else {
this.vendor = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7;
import org.opensearch.security.securityconf.impl.v7.RoleV7;
import org.opensearch.security.securityconf.impl.v7.TenantV7;
import org.opensearch.identity.tokens.BearerAuthToken;

public enum CType {

Expand All @@ -59,7 +60,8 @@ public enum CType {
NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class)),
WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)),
ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)),
AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class));
AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)),
REVOKEDTOKENS(toMap(1, BearerAuthToken.class));

private Map<Integer, Class<?>> implementations;

Expand Down
Loading
Loading