Skip to content

Commit

Permalink
no tests
Browse files Browse the repository at this point in the history
Signed-off-by: Stephen Crawford <[email protected]>
  • Loading branch information
stephen-crawford committed Sep 14, 2023
1 parent 6daa697 commit c3c7743
Show file tree
Hide file tree
Showing 5 changed files with 770 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.action.onbehalf;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableList;
import org.greenrobot.eventbus.Subscribe;

import org.opensearch.client.node.NodeClient;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.rest.BaseRestHandler;
import org.opensearch.rest.BytesRestResponse;
import org.opensearch.rest.NamedRoute;
import org.opensearch.rest.RestChannel;
import org.opensearch.rest.RestRequest;
import org.opensearch.core.rest.RestStatus;
import org.opensearch.security.authtoken.jwt.JwtVendor;
import org.opensearch.security.securityconf.ConfigModel;
import org.opensearch.security.securityconf.DynamicConfigModel;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.user.User;
import org.opensearch.threadpool.ThreadPool;

import static org.opensearch.rest.RestRequest.Method.POST;
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;

public class CreateOnBehalfOfTokenAction extends BaseRestHandler {

private static final List<Route> routes = addRoutesPrefix(
ImmutableList.of(new NamedRoute.Builder().method(POST).path("/generateonbehalfoftoken").uniqueName("security:obo/create").build()),
"/_plugins/_security/api"
);

private JwtVendor vendor;
private final ThreadPool threadPool;
private final ClusterService clusterService;

private ConfigModel configModel;

private DynamicConfigModel dcm;

public static final Integer OBO_DEFAULT_EXPIRY_SECONDS = 5 * 60;
public static final Integer OBO_MAX_EXPIRY_SECONDS = 10 * 60;

public static final String DEFAULT_SERVICE = "self-issued";

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

@Subscribe
public void onDynamicConfigModelChanged(DynamicConfigModel dcm) {
this.dcm = dcm;

Settings settings = dcm.getDynamicOnBehalfOfSettings();

Boolean enabled = Boolean.parseBoolean(settings.get("enabled"));
String signingKey = settings.get("signing_key");
String encryptionKey = settings.get("encryption_key");

if (!Boolean.FALSE.equals(enabled) && signingKey != null && encryptionKey != null) {
this.vendor = new JwtVendor(settings, Optional.empty());
} else {
this.vendor = null;
}
}

public CreateOnBehalfOfTokenAction(final Settings settings, final ThreadPool threadPool, final ClusterService clusterService) {
this.threadPool = threadPool;
this.clusterService = clusterService;
}

@Override
public String getName() {
return getClass().getSimpleName();
}

@Override
public List<Route> routes() {
return routes;
}

@Override
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
switch (request.method()) {
case POST:
return handlePost(request, client);
default:
throw new IllegalArgumentException(request.method() + " not supported");
}
}

private RestChannelConsumer handlePost(RestRequest request, NodeClient client) throws IOException {
return new RestChannelConsumer() {
@Override
public void accept(RestChannel channel) throws Exception {
final XContentBuilder builder = channel.newBuilder();
BytesRestResponse response;
try {
if (vendor == null) {
channel.sendResponse(
new BytesRestResponse(
RestStatus.SERVICE_UNAVAILABLE,
"The OnBehalfOf token generating API has been disabled, see {link to doc} for more information on this feature." /* TODO: Update the link to the documentation website */
)
);
return;
}

final String clusterIdentifier = clusterService.getClusterName().value();

final Map<String, Object> requestBody = request.contentOrSourceParamParser().map();
final String description = (String) requestBody.getOrDefault("description", null);

final Integer tokenDuration = Optional.ofNullable(requestBody.get("durationSeconds"))
.map(value -> (String) value)
.map(Integer::parseInt)
.map(value -> Math.min(value, OBO_MAX_EXPIRY_SECONDS)) // Max duration seconds are 600
.orElse(OBO_DEFAULT_EXPIRY_SECONDS); // Fallback to default

final Boolean roleSecurityMode = Optional.ofNullable(requestBody.get("roleSecurityMode"))
.map(value -> (Boolean) value)
.orElse(true); // Default to false if null

final String service = (String) requestBody.getOrDefault("service", DEFAULT_SERVICE);
final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
Set<String> mappedRoles = mapRoles(user);

builder.startObject();
builder.field("user", user.getName());

final String token = vendor.createJwt(
clusterIdentifier,
user.getName(),
service,
tokenDuration,
mappedRoles.stream().collect(Collectors.toList()),
user.getRoles().stream().collect(Collectors.toList()),
roleSecurityMode
);
builder.field("authenticationToken", token);
builder.field("durationSeconds", tokenDuration);
builder.endObject();

response = new BytesRestResponse(RestStatus.OK, builder);
} catch (final Exception exception) {
builder.startObject().field("error", exception.toString()).endObject();

response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder);
}
builder.close();
channel.sendResponse(response);
}
};
}

private Set<String> mapRoles(final User user) {
return this.configModel.mapSecurityRoles(user, null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 {

private final Cipher encryptCipher;
private final Cipher decryptCipher;

public EncryptionDecryptionUtil(final String secret) {
this.encryptCipher = createCipherFromSecret(secret, CipherMode.ENCRYPT);
this.decryptCipher = createCipherFromSecret(secret, CipherMode.DECRYPT);
}

public String encrypt(final String data) {
byte[] encryptedBytes = processWithCipher(data.getBytes(StandardCharsets.UTF_8), encryptCipher);
return Base64.getEncoder().encodeToString(encryptedBytes);
}

public String decrypt(final String encryptedString) {
byte[] decodedBytes = Base64.getDecoder().decode(encryptedString);
return new String(processWithCipher(decodedBytes, decryptCipher), 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(), e);
}
}

private static byte[] processWithCipher(final byte[] data, final Cipher cipher) {
try {
return cipher.doFinal(data);
} catch (final Exception e) {
throw new RuntimeException("Error processing data with cipher", e);
}
}

private enum CipherMode {
ENCRYPT(Cipher.ENCRYPT_MODE),
DECRYPT(Cipher.DECRYPT_MODE);

private final int opmode;

private CipherMode(final int opmode) {
this.opmode = opmode;
}
}
}
Loading

0 comments on commit c3c7743

Please sign in to comment.