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

feat(credentials): implement matchExpression-based credentials #1000

Merged
Merged
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
1 change: 1 addition & 0 deletions src/main/java/io/cryostat/Cryostat.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public static void main(String[] args) throws Exception {
CompletableFuture<Void> future = new CompletableFuture<>();
client.httpServer().addShutdownListener(() -> future.complete(null));

client.credentialsManager().migrate();
client.credentialsManager().load();
client.ruleRegistry().loadRules();
client.vertx()
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/io/cryostat/MainModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import javax.inject.Named;
import javax.inject.Singleton;
import javax.management.remote.JMXServiceURL;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import io.cryostat.configuration.ConfigurationModule;
import io.cryostat.configuration.Variables;
Expand Down Expand Up @@ -155,4 +157,10 @@ static Path provideSavedRecordingsPath(Logger logger, Environment env) {
logger.info("Local save path for flight recordings set as {}", archivePath);
return Paths.get(archivePath);
}

@Provides
@Singleton
public static ScriptEngine provideScriptEngine() {
return new ScriptEngineManager().getEngineByName("nashorn");
}
}
11 changes: 10 additions & 1 deletion src/main/java/io/cryostat/configuration/ConfigurationModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import io.cryostat.core.sys.FileSystem;
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.platform.PlatformClient;
import io.cryostat.rules.MatchExpressionEvaluator;

import com.google.gson.Gson;
import dagger.Module;
Expand All @@ -77,6 +78,7 @@ static Path provideConfigurationPath(Logger logger, Environment env) {
@Singleton
static CredentialsManager provideCredentialsManager(
@Named(CONFIGURATION_PATH) Path confDir,
MatchExpressionEvaluator matchExpressionEvaluator,
FileSystem fs,
PlatformClient platformClient,
NotificationFactory notificationFactory,
Expand All @@ -95,7 +97,14 @@ static CredentialsManager provideCredentialsManager(
PosixFilePermission.OWNER_EXECUTE)));
}
return new CredentialsManager(
credentialsDir, fs, platformClient, notificationFactory, gson, base32, logger);
credentialsDir,
matchExpressionEvaluator,
fs,
platformClient,
notificationFactory,
gson,
base32,
logger);
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand Down
234 changes: 211 additions & 23 deletions src/main/java/io/cryostat/configuration/CredentialsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,40 @@
*/
package io.cryostat.configuration;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.script.ScriptException;

import io.cryostat.core.log.Logger;
import io.cryostat.core.net.Credentials;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.platform.PlatformClient;
import io.cryostat.platform.ServiceRef;
import io.cryostat.rules.MatchExpressionEvaluator;

import com.google.gson.Gson;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.lang3.StringUtils;

public class CredentialsManager {

private final Path credentialsDir;
private final MatchExpressionEvaluator matchExpressionEvaluator;
private final FileSystem fs;
private final PlatformClient platformClient;
private final Gson gson;
Expand All @@ -72,13 +81,15 @@ public class CredentialsManager {

CredentialsManager(
Path credentialsDir,
MatchExpressionEvaluator matchExpressionEvaluator,
FileSystem fs,
PlatformClient platformClient,
NotificationFactory notificationFactory,
Gson gson,
Base32 base32,
Logger logger) {
this.credentialsDir = credentialsDir;
this.matchExpressionEvaluator = matchExpressionEvaluator;
this.fs = fs;
this.platformClient = platformClient;
this.gson = gson;
Expand All @@ -87,6 +98,37 @@ public class CredentialsManager {
this.credentialsMap = new HashMap<>();
}

public void migrate() throws Exception {
for (String file : this.fs.listDirectoryChildren(credentialsDir)) {
BufferedReader reader;
try {
Path path = credentialsDir.resolve(file);
reader = fs.readFile(path);
TargetSpecificStoredCredentials targetSpecificCredential =
gson.fromJson(reader, TargetSpecificStoredCredentials.class);

String targetId = targetSpecificCredential.getTargetId();
if (StringUtils.isNotBlank(targetId)) {
addCredentials(
targetIdToMatchExpression(targetSpecificCredential.getTargetId()),
targetSpecificCredential.getCredentials());
fs.deleteIfExists(path);
logger.info("Migrated {}", path);
}
} catch (IOException e) {
logger.warn(e);
continue;
}
}
}

public static String targetIdToMatchExpression(String targetId) {
if (StringUtils.isBlank(targetId)) {
return null;
}
return String.format("target.connectUrl == \"%s\"", targetId);
}

public void load() throws IOException {
this.fs.listDirectoryChildren(credentialsDir).stream()
.peek(n -> logger.trace("Credentials file: {}", n))
Expand All @@ -102,16 +144,17 @@ public void load() throws IOException {
})
.filter(Objects::nonNull)
.map(reader -> gson.fromJson(reader, StoredCredentials.class))
.forEach(sc -> credentialsMap.put(sc.getTargetId(), sc.getCredentials()));
.forEach(sc -> credentialsMap.put(sc.getMatchExpression(), sc.getCredentials()));
}

public boolean addCredentials(String targetId, Credentials credentials) throws IOException {
boolean replaced = credentialsMap.containsKey(targetId);
credentialsMap.put(targetId, credentials);
Path destination = getPersistedPath(targetId);
public boolean addCredentials(String matchExpression, Credentials credentials)
throws IOException {
boolean replaced = credentialsMap.containsKey(matchExpression);
credentialsMap.put(matchExpression, credentials);
Path destination = getPersistedPath(matchExpression);
fs.writeString(
destination,
gson.toJson(new StoredCredentials(targetId, credentials)),
gson.toJson(new StoredCredentials(matchExpression, credentials)),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
Expand All @@ -121,48 +164,193 @@ public boolean addCredentials(String targetId, Credentials credentials) throws I
return replaced;
}

public boolean removeCredentials(String targetId) throws IOException {
Credentials deleted = this.credentialsMap.remove(targetId);
fs.deleteIfExists(getPersistedPath(targetId));
public boolean removeCredentials(String matchExpression) throws IOException {
Credentials deleted = this.credentialsMap.remove(matchExpression);
fs.deleteIfExists(getPersistedPath(matchExpression));
return deleted != null;
}

public Credentials getCredentials(String targetId) {
return this.credentialsMap.get(targetId);
public Credentials getCredentialsByTargetId(String targetId) {
for (ServiceRef service : this.platformClient.listDiscoverableServices()) {
if (Objects.equals(targetId, service.getServiceUri().toString())) {
return getCredentials(service);
}
}
return null;
}

public Credentials getCredentials(ServiceRef serviceRef) {
return getCredentials(serviceRef.getServiceUri().toString());
for (Map.Entry<String, Credentials> entry : credentialsMap.entrySet()) {
try {
if (matchExpressionEvaluator.applies(entry.getKey(), serviceRef)) {
return entry.getValue();
}
} catch (ScriptException e) {
logger.error(e);
continue;
}
}
return null;
}

public Collection<ServiceRef> getServiceRefsWithCredentials() {
List<ServiceRef> result = new ArrayList<>();
for (ServiceRef service : this.platformClient.listDiscoverableServices()) {
Credentials credentials = getCredentials(service);
if (credentials != null) {
result.add(service);
}
}
return result;
}

public Collection<String> getMatchExpressions() {
return credentialsMap.keySet();
}

public List<ServiceRef> getCredentialKeys() {
return this.platformClient.listDiscoverableServices().stream()
.filter(target -> credentialsMap.containsKey(target.getServiceUri().toString()))
.collect(Collectors.toList());
public List<MatchedCredentials> getMatchExpressionsWithMatchedTargets() {
List<MatchedCredentials> result = new ArrayList<>();
List<ServiceRef> targets = platformClient.listDiscoverableServices();
for (String expr : getMatchExpressions()) {
Set<ServiceRef> matchedTargets = new HashSet<>();
for (ServiceRef target : targets) {
try {
if (matchExpressionEvaluator.applies(expr, target)) {
matchedTargets.add(target);
}
} catch (ScriptException e) {
logger.error(e);
continue;
}
}
MatchedCredentials match = new MatchedCredentials(expr, matchedTargets);
result.add(match);
}
return result;
}

private Path getPersistedPath(String targetId) {
private Path getPersistedPath(String matchExpression) {
return credentialsDir.resolve(
String.format(
"%s.json",
base32.encodeAsString(targetId.getBytes(StandardCharsets.UTF_8))));
base32.encodeAsString(matchExpression.getBytes(StandardCharsets.UTF_8))));
}

public static class StoredCredentials {
public static class MatchedCredentials {
private final String matchExpression;
private final Collection<ServiceRef> targets;

MatchedCredentials(String matchExpression, Collection<ServiceRef> targets) {
this.matchExpression = matchExpression;
this.targets = new HashSet<>(targets);
}

public String getMatchExpression() {
return matchExpression;
}

public Collection<ServiceRef> getTargets() {
return Collections.unmodifiableCollection(targets);
}

@Override
public int hashCode() {
return Objects.hash(matchExpression, targets);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MatchedCredentials other = (MatchedCredentials) obj;
return Objects.equals(matchExpression, other.matchExpression)
&& Objects.equals(targets, other.targets);
}
}

static class StoredCredentials {
private final String matchExpression;
private final Credentials credentials;

StoredCredentials(String matchExpression, Credentials credentials) {
this.matchExpression = matchExpression;
this.credentials = credentials;
}

String getMatchExpression() {
return this.matchExpression;
}

Credentials getCredentials() {
return this.credentials;
}

@Override
public int hashCode() {
return Objects.hash(credentials, matchExpression);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
StoredCredentials other = (StoredCredentials) obj;
return Objects.equals(credentials, other.credentials)
&& Objects.equals(matchExpression, other.matchExpression);
}
}

@Deprecated(since = "2.2", forRemoval = true)
static class TargetSpecificStoredCredentials {
private final String targetId;
private final Credentials credentials;

StoredCredentials(String targetId, Credentials credentials) {
TargetSpecificStoredCredentials(String targetId, Credentials credentials) {
this.targetId = targetId;
this.credentials = credentials;
}

public String getTargetId() {
String getTargetId() {
return this.targetId;
}

public Credentials getCredentials() {
Credentials getCredentials() {
return this.credentials;
}

@Override
public int hashCode() {
return Objects.hash(credentials, targetId);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TargetSpecificStoredCredentials other = (TargetSpecificStoredCredentials) obj;
return Objects.equals(credentials, other.credentials)
&& Objects.equals(targetId, other.targetId);
}
}
}
Loading