forked from opensearch-project/security
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Stephen Crawford <[email protected]>
- Loading branch information
1 parent
6daa697
commit c3c7743
Showing
5 changed files
with
770 additions
and
0 deletions.
There are no files selected for viewing
179 changes: 179 additions & 0 deletions
179
src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
72 changes: 72 additions & 0 deletions
72
src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.