io.gravitee.node.api.secrets.model.SecretURL Maven / Gradle / Ivy
package io.gravitee.node.api.secrets.model;
import com.google.common.base.Splitter;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import java.util.*;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* A URL-like representation of a {@link SecretMount}
*
* @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
* @author GraviteeSource Team
*/
public record SecretURL(String provider, String path, String key, Multimap query) {
public static final char URL_SEPARATOR = '/';
private static final Splitter urlPathSplitter = Splitter.on(URL_SEPARATOR);
private static final Splitter queryParamSplitter = Splitter.on('&');
private static final Splitter queryParamKeyValueSplitter = Splitter.on('=');
private static final Splitter keyMapParamValueSplitter = Splitter.on(':');
public static final String SCHEME = "secret://";
/**
* Parse the string into a {@link SecretURL}
*
* the format is : secret://<provider>/<path or name>[:<key>][?option=value1&option=value2]
*
* secret://is mandatory
* provider is mandatory and should match a secret provider id
* "path or name" is mandatory, a free string that can contain forward slashes ('/').
* If an empty string or spaces are found between two forward slashes (eg. //
or / /
) parsing will fail.
* key is optional and cannot replace "name or path"
* query string is optional and is simply split into key/value pairs.
* Pair are always list as can be specified more than once. If no value is parsed, then true
is set
*
* @param url the string to parse
* @return SecretURL object
* @throws IllegalArgumentException when failing to parse
*/
public static SecretURL from(String url) {
url = Objects.requireNonNull(url).trim();
if (!url.startsWith(SCHEME)) {
throwFormatError(url);
}
String schemeLess = url.substring(SCHEME.length());
int firstSlash = schemeLess.indexOf('/');
if (firstSlash < 0 || firstSlash == schemeLess.length() - 1) {
throwFormatError(url);
}
String provider = schemeLess.substring(0, firstSlash).trim();
int questionMarkPos = schemeLess.indexOf('?');
if (questionMarkPos == firstSlash + 1) {
throwFormatError(url);
}
String path;
final String key;
final Multimap query;
if (questionMarkPos > 0) {
path = schemeLess.substring(provider.length() + 1, questionMarkPos).trim();
query = parseQuery(schemeLess.substring(questionMarkPos + 1));
} else {
path = schemeLess.substring(provider.length() + 1).trim();
query = MultimapBuilder.hashKeys().arrayListValues().build();
}
int columnIndex = path.lastIndexOf(':');
if (columnIndex > path.lastIndexOf(URL_SEPARATOR)) {
key = path.substring(columnIndex + 1);
path = path.substring(0, columnIndex);
} else {
key = null;
}
// remove trailing slashes
while (!path.isEmpty() && path.charAt(path.length() - 1) == URL_SEPARATOR) {
path = path.substring(0, path.length() - 1);
}
if (path.isBlank()) {
throwFormatError(url);
}
if (urlPathSplitter.splitToList(path).stream().anyMatch(String::isBlank)) {
throwFormatError(url);
}
return new SecretURL(provider, path, key, query);
}
private static void throwFormatError(String url) {
throw new IllegalArgumentException(
"Secret URL '%s' should have the following format %s/[:][?option=value1&option=value2]".formatted(
url,
SCHEME
)
);
}
private static Multimap parseQuery(String substring) {
Multimap query = MultimapBuilder.hashKeys().arrayListValues().build();
queryParamSplitter
.split(substring)
.forEach(pair -> {
Iterable parts = queryParamKeyValueSplitter.split(pair);
Iterator iterator = parts.iterator();
if (iterator.hasNext()) {
String key = iterator.next();
if (iterator.hasNext()) {
query.put(key, iterator.next());
} else {
query.put(key, "true");
}
}
});
return query;
}
/**
* Browser query string to find 'watch' with value 'true'
*
* @return true if watch=true
was found.
*/
public boolean isWatchable() {
return query()
.entries()
.stream()
.anyMatch(e -> Objects.equals(e.getKey(), WellKnownQueryParam.WATCH) && Boolean.parseBoolean(e.getValue()));
}
/**
* Extract the well-known keys from the keymap
query string.
* format is <well known key>:<key in secret>
* If the well known key is unknown then it is ignored.
*
* @return a map to help extracting well known keys out of the secret.
* @see SecretMap#handleWellKnownSecretKeys(Map)
* @see SecretMap.WellKnownSecretKey
*/
public Map wellKnowKeyMap() {
record Mapping(String secretKey, SecretMap.WellKnownSecretKey wellKnow) {}
return query()
.get(WellKnownQueryParam.KEYMAP)
.stream()
.map(keyMap -> {
List mapping = keyMapParamValueSplitter.splitToList(keyMap);
if (mapping.size() == 2) {
// eg. certificate:tls.crt
String wellKnown = mapping.get(0).trim().toUpperCase();
String secretKey = mapping.get(1).trim();
if (wellKnown.isEmpty() || secretKey.isEmpty()) {
throw new IllegalArgumentException("keymap '%s' is not valid".formatted(keyMap));
}
try {
return new Mapping(secretKey, SecretMap.WellKnownSecretKey.valueOf(wellKnown));
} catch (IllegalArgumentException e) {
// no op, will return "empty"
}
}
return new Mapping(null, null);
})
.filter(mapping -> mapping.wellKnow() != null)
.collect(Collectors.toMap(Mapping::secretKey, Mapping::wellKnow));
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class WellKnownQueryParam {
public static final String WATCH = "watch";
public static final String KEYMAP = "keymap";
public static final String NAMESPACE = "namespace";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy