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

[2.x] Adds On-Behalf-Of authentication mechanism and service account capability #3416

Closed
Show file tree
Hide file tree
Changes from 11 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 @@ -13,22 +13,14 @@

import java.nio.file.Path;
import java.security.AccessController;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.WeakKeyException;
import org.apache.http.HttpHeaders;
import org.apache.logging.log4j.LogManager;
Expand All @@ -44,6 +36,7 @@
import org.opensearch.core.rest.RestStatus;
import org.opensearch.security.auth.HTTPAuthenticator;
import org.opensearch.security.user.AuthCredentials;
import org.opensearch.security.util.KeyUtil;

public class HTTPJwtAuthenticator implements HTTPAuthenticator {

Expand All @@ -64,45 +57,7 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator {
public HTTPJwtAuthenticator(final Settings settings, final Path configPath) {
super();

final JwtParserBuilder _jwtParserBuilder = Jwts.parserBuilder();

try {
String signingKey = settings.get("signing_key");

if (signingKey == null || signingKey.length() == 0) {
log.error("signingKey must not be null or empty. JWT authentication will not work");
} else {

signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "");
signingKey = signingKey.replace("-----END PUBLIC KEY-----", "");

byte[] decoded = Decoders.BASE64.decode(signingKey);
Key key = null;

try {
key = getPublicKey(decoded, "RSA");
} catch (Exception e) {
log.debug("No public RSA key, try other algos ({})", e.toString());
}

try {
key = getPublicKey(decoded, "EC");
} catch (Exception e) {
log.debug("No public ECDSA key, try other algos ({})", e.toString());
}

if (key != null) {
_jwtParserBuilder.setSigningKey(key);
} else {
_jwtParserBuilder.setSigningKey(decoded);
}

}
} catch (Throwable e) {
log.error("Error creating JWT authenticator. JWT authentication will not work", e);
throw new RuntimeException(e);
}

String signingKey = settings.get("signing_key");
jwtUrlParameter = settings.get("jwt_url_parameter");
jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION);
isDefaultAuthHeader = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(jwtHeaderName);
Expand All @@ -111,28 +66,20 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) {
requireAudience = settings.get("required_audience");
requireIssuer = settings.get("required_issuer");

if (requireAudience != null) {
_jwtParserBuilder.requireAudience(requireAudience);
}

if (requireIssuer != null) {
_jwtParserBuilder.requireIssuer(requireIssuer);
}

final SecurityManager sm = System.getSecurityManager();

if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
JwtParserBuilder jwtParserBuilder = KeyUtil.createJwtParserBuilderFromSigningKey(signingKey, log);
if (jwtParserBuilder == null) {
jwtParser = null;
} else {
if (requireAudience != null) {
jwtParserBuilder = jwtParserBuilder.require("aud", requireAudience);
}

JwtParser parser = AccessController.doPrivileged(new PrivilegedAction<JwtParser>() {
@Override
public JwtParser run() {
return _jwtParserBuilder.build();
if (requireIssuer != null) {
jwtParserBuilder = jwtParserBuilder.require("iss", requireIssuer);
}
});

jwtParser = parser;
jwtParser = jwtParserBuilder.build();
}
}

@Override
Expand Down Expand Up @@ -296,11 +243,4 @@ protected String[] extractRoles(final Claims claims, final RestRequest request)
return roles;
}

private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException,
InvalidKeySpecException {
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance(algo);
return kf.generatePublic(spec);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,15 @@
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.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;
Expand All @@ -117,6 +121,7 @@
import org.opensearch.search.query.QuerySearchResult;
import org.opensearch.security.action.configupdate.ConfigUpdateAction;
import org.opensearch.security.action.configupdate.TransportConfigUpdateAction;
import org.opensearch.security.action.onbehalf.CreateOnBehalfOfTokenAction;
import org.opensearch.security.action.whoami.TransportWhoAmIAction;
import org.opensearch.security.action.whoami.WhoAmIAction;
import org.opensearch.security.auditlog.AuditLog;
Expand Down Expand Up @@ -144,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.resolver.IndexResolverReplacer;
Expand Down Expand Up @@ -191,7 +198,12 @@
import org.opensearch.transport.TransportService;
import org.opensearch.watcher.ResourceWatcherService;

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

private static final String KEYWORD = ".keyword";
private static final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace");
Expand All @@ -207,10 +219,13 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
private volatile UserService userService;
private volatile ThreadPool threadPool;
private volatile ConfigurationRepository cr;
private volatile SecuritySubject subject = new SecuritySubject();
private volatile SecurityTokenManager tokenManager;
private volatile AdminDNs adminDns;
private volatile ClusterService cs;
private volatile AtomicReference<DiscoveryNode> localNode = new AtomicReference<>();
private volatile AuditLog auditLog;
private volatile DynamicConfigFactory dcf;
private volatile BackendRegistry backendRegistry;
private volatile SslExceptionHandler sslExceptionHandler;
private volatile Client localClient;
Expand All @@ -223,6 +238,17 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
private volatile Salt salt;
private volatile OpensearchDynamicSetting<Boolean> transportPassiveAuthSetting;

public static Setting RESERVED_INDICES_SETTING = Setting.listSetting(
"reserved_indices",
List.of(),
Function.identity(),
Property.ExtensionScope
);

public static Setting PERMISSIONS_SETTING = Setting.groupSetting("permissions.", Property.ExtensionScope);

public static Setting SEND_BACKEND_ROLES_SETTING = Setting.boolSetting("send_backend_roles", false, Property.ExtensionScope);

public static boolean isActionTraceEnabled() {
return actionTrace.isTraceEnabled();
}
Expand Down Expand Up @@ -538,6 +564,9 @@ public List<RestHandler> getRestHandlers(
principalExtractor
)
);
CreateOnBehalfOfTokenAction cobot = new CreateOnBehalfOfTokenAction(settings, threadPool, Objects.requireNonNull(cs));
dcf.registerDCFListener(cobot);
handlers.add(cobot);
handlers.addAll(
SecurityRestApiActions.getHandler(
settings,
Expand Down Expand Up @@ -953,7 +982,7 @@ public Collection<Object> createComponents(
// Register opensearch dynamic settings
transportPassiveAuthSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings());

final ClusterInfoHolder cih = new ClusterInfoHolder();
final ClusterInfoHolder cih = new ClusterInfoHolder(this.cs.getClusterName().value());
this.cs.addListener(cih);
this.salt = Salt.from(settings);

Expand Down Expand Up @@ -998,8 +1027,12 @@ public Collection<Object> createComponents(

cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog);

subject.setThreadContext(threadPool.getThreadContext());

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

tokenManager = new SecurityTokenManager(cs, threadPool, userService);

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

Expand Down Expand Up @@ -1041,13 +1074,14 @@ public Collection<Object> createComponents(
compatConfig
);

final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih);
dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih);
dcf.registerDCFListener(backendRegistry);
dcf.registerDCFListener(compatConfig);
dcf.registerDCFListener(irr);
dcf.registerDCFListener(xffResolver);
dcf.registerDCFListener(evaluator);
dcf.registerDCFListener(securityRestHandler);
dcf.registerDCFListener(tokenManager);
if (!(auditLog instanceof NullAuditLog)) {
// Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog
dcf.registerDCFListener(auditLog);
Expand Down Expand Up @@ -1110,6 +1144,15 @@ public Settings additionalSettings() {
return builder.build();
}

@Override
Copy link
Contributor

Choose a reason for hiding this comment

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

So the way Craig set this up, the idea is to register a set of extension security settings when an extension is installed/registered as part of the core initialization process. To make this work with service accounts the general idea would be to make sure that a service account is only able to operate on the indices listed in the reserved_indices list and affiliated with themselves--this should use a mapping or some other mechanism.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have removed BACKEND_ROLES setting for now. In SecurityIndexAccessEvaluator we check by looking up extension settings by username (should match extension name), and then matching the requested index with extension's reserved index to determine whether access should be granted

public List<Setting<?>> getExtensionSettings() {
List<Setting<?>> settings = new ArrayList<Setting<?>>();
settings.add(RESERVED_INDICES_SETTING);
settings.add(SEND_BACKEND_ROLES_SETTING);
DarshitChanpura marked this conversation as resolved.
Show resolved Hide resolved
settings.add(PERMISSIONS_SETTING);
return settings;
}

@Override
public List<Setting<?>> getSettings() {
List<Setting<?>> settings = new ArrayList<Setting<?>>();
Expand Down Expand Up @@ -1897,6 +1940,16 @@ private static String handleKeyword(final String field) {
return field;
}

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

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

public static class GuiceHolder implements LifecycleComponent {

private static RepositoriesService repositoriesService;
Expand Down
Loading
Loading