temporary commit
This commit is contained in:
6
gbcs-api/src/main/java/module-info.java
Normal file
6
gbcs-api/src/main/java/module-info.java
Normal file
@@ -0,0 +1,6 @@
|
||||
module net.woggioni.gbcs.api {
|
||||
requires static lombok;
|
||||
requires java.xml;
|
||||
exports net.woggioni.gbcs.api;
|
||||
exports net.woggioni.gbcs.api.exception;
|
||||
}
|
11
gbcs-api/src/main/java/net/woggioni/gbcs/api/Cache.java
Normal file
11
gbcs-api/src/main/java/net/woggioni/gbcs/api/Cache.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
|
||||
import net.woggioni.gbcs.api.exception.ContentTooLargeException;
|
||||
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
public interface Cache extends AutoCloseable {
|
||||
ReadableByteChannel get(String key);
|
||||
|
||||
void put(String key, byte[] content) throws ContentTooLargeException;
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
public interface CacheProvider<T extends Configuration.Cache> {
|
||||
|
||||
String getXmlSchemaLocation();
|
||||
|
||||
String getXmlNamespace();
|
||||
|
||||
String getXmlType();
|
||||
|
||||
T deserialize(Element parent);
|
||||
|
||||
Element serialize(Document doc, T cache);
|
||||
}
|
138
gbcs-api/src/main/java/net/woggioni/gbcs/api/Configuration.java
Normal file
138
gbcs-api/src/main/java/net/woggioni/gbcs/api/Configuration.java
Normal file
@@ -0,0 +1,138 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
|
||||
|
||||
import lombok.Value;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Value
|
||||
public class Configuration {
|
||||
String host;
|
||||
int port;
|
||||
String serverPath;
|
||||
Map<String, User> users;
|
||||
Map<String, Group> groups;
|
||||
Cache cache;
|
||||
Authentication authentication;
|
||||
Tls tls;
|
||||
boolean useVirtualThread;
|
||||
|
||||
@Value
|
||||
public static class Group {
|
||||
String name;
|
||||
Set<Role> roles;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class User {
|
||||
String name;
|
||||
String password;
|
||||
Set<Group> groups;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
public Set<Role> getRoles() {
|
||||
return groups.stream()
|
||||
.flatMap(group -> group.getRoles().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface UserExtractor {
|
||||
User extract(X509Certificate cert);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface GroupExtractor {
|
||||
Group extract(X509Certificate cert);
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class Tls {
|
||||
KeyStore keyStore;
|
||||
TrustStore trustStore;
|
||||
boolean verifyClients;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class KeyStore {
|
||||
Path file;
|
||||
String password;
|
||||
String keyAlias;
|
||||
String keyPassword;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class TrustStore {
|
||||
Path file;
|
||||
String password;
|
||||
boolean checkCertificateStatus;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class TlsCertificateExtractor {
|
||||
String rdnType;
|
||||
String pattern;
|
||||
}
|
||||
|
||||
public interface Authentication {}
|
||||
|
||||
public static class BasicAuthentication implements Authentication {}
|
||||
|
||||
@Value
|
||||
public static class ClientCertificateAuthentication implements Authentication {
|
||||
TlsCertificateExtractor userExtractor;
|
||||
TlsCertificateExtractor groupExtractor;
|
||||
}
|
||||
|
||||
public interface Cache {
|
||||
net.woggioni.gbcs.api.Cache materialize();
|
||||
String getNamespaceURI();
|
||||
String getTypeName();
|
||||
}
|
||||
|
||||
// @Value
|
||||
// public static class FileSystemCache implements Cache {
|
||||
// Path root;
|
||||
// Duration maxAge;
|
||||
// }
|
||||
|
||||
public static Configuration of(
|
||||
String host,
|
||||
int port,
|
||||
String serverPath,
|
||||
Map<String, User> users,
|
||||
Map<String, Group> groups,
|
||||
Cache cache,
|
||||
Authentication authentication,
|
||||
Tls tls,
|
||||
boolean useVirtualThread
|
||||
) {
|
||||
return new Configuration(
|
||||
host,
|
||||
port,
|
||||
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
|
||||
users,
|
||||
groups,
|
||||
cache,
|
||||
authentication,
|
||||
tls,
|
||||
useVirtualThread
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,237 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class ConfigurationParser {
|
||||
|
||||
public static Configuration parse(Document document) {
|
||||
Element root = document.getDocumentElement();
|
||||
Configuration.Cache cache = null;
|
||||
String host = "127.0.0.1";
|
||||
int port = 11080;
|
||||
Map<String, Configuration.User> users = Collections.emptyMap();
|
||||
Map<String, Configuration.Group> groups = Collections.emptyMap();
|
||||
Configuration.Tls tls = null;
|
||||
String serverPath = root.getAttribute("path");
|
||||
boolean useVirtualThread = !root.getAttribute("useVirtualThreads").isEmpty() &&
|
||||
Boolean.parseBoolean(root.getAttribute("useVirtualThreads"));
|
||||
Configuration.Authentication authentication = null;
|
||||
|
||||
for (Node child : iterableOf(root)) {
|
||||
switch (child.getNodeName()) {
|
||||
case "authorization":
|
||||
for (Node gchild : iterableOf((Element) child)) {
|
||||
switch (gchild.getNodeName()) {
|
||||
case "users":
|
||||
users = parseUsers((Element) gchild);
|
||||
break;
|
||||
case "groups":
|
||||
Map.Entry<Map<String, Configuration.User>, Map<String, Configuration.Group>> pair = parseGroups((Element) gchild, users);
|
||||
users = pair.getKey();
|
||||
groups = pair.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "bind":
|
||||
Element bindEl = (Element) child;
|
||||
host = bindEl.getAttribute("host");
|
||||
port = Integer.parseInt(bindEl.getAttribute("port"));
|
||||
break;
|
||||
|
||||
case "cache":
|
||||
Element cacheEl = (Element) child;
|
||||
cacheEl.getAttributeNode("xs:type").getSchemaTypeInfo();
|
||||
if ("gbcs:fileSystemCacheType".equals(cacheEl.getAttribute("xs:type"))) {
|
||||
String cacheFolder = cacheEl.getAttribute("path");
|
||||
Path cachePath = !cacheFolder.isEmpty()
|
||||
? Paths.get(cacheFolder)
|
||||
: Paths.get(System.getProperty("user.home")).resolve(".gbcs");
|
||||
|
||||
String maxAgeStr = cacheEl.getAttribute("max-age");
|
||||
Duration maxAge = !maxAgeStr.isEmpty()
|
||||
? Duration.parse(maxAgeStr)
|
||||
: Duration.ofDays(1);
|
||||
|
||||
// cache = new Configuration.FileSystemCache(cachePath, maxAge);
|
||||
}
|
||||
break;
|
||||
|
||||
case "authentication":
|
||||
for (Node gchild : iterableOf((Element) child)) {
|
||||
switch (gchild.getNodeName()) {
|
||||
case "basic":
|
||||
authentication = new Configuration.BasicAuthentication();
|
||||
break;
|
||||
case "client-certificate":
|
||||
Configuration.TlsCertificateExtractor tlsExtractorUser = null;
|
||||
Configuration.TlsCertificateExtractor tlsExtractorGroup = null;
|
||||
|
||||
for (Node authChild : iterableOf((Element) gchild)) {
|
||||
Element authEl = (Element) authChild;
|
||||
switch (authChild.getNodeName()) {
|
||||
case "group-extractor":
|
||||
String groupAttrName = authEl.getAttribute("attribute-name");
|
||||
String groupPattern = authEl.getAttribute("pattern");
|
||||
tlsExtractorGroup = new Configuration.TlsCertificateExtractor(groupAttrName, groupPattern);
|
||||
break;
|
||||
case "user-extractor":
|
||||
String userAttrName = authEl.getAttribute("attribute-name");
|
||||
String userPattern = authEl.getAttribute("pattern");
|
||||
tlsExtractorUser = new Configuration.TlsCertificateExtractor(userAttrName, userPattern);
|
||||
break;
|
||||
}
|
||||
}
|
||||
authentication = new Configuration.ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "tls":
|
||||
Element tlsEl = (Element) child;
|
||||
boolean verifyClients = !tlsEl.getAttribute("verify-clients").isEmpty() &&
|
||||
Boolean.parseBoolean(tlsEl.getAttribute("verify-clients"));
|
||||
Configuration.KeyStore keyStore = null;
|
||||
Configuration.TrustStore trustStore = null;
|
||||
|
||||
for (Node gchild : iterableOf(tlsEl)) {
|
||||
Element tlsChild = (Element) gchild;
|
||||
switch (gchild.getNodeName()) {
|
||||
case "keystore":
|
||||
Path keyStoreFile = Paths.get(tlsChild.getAttribute("file"));
|
||||
String keyStorePassword = !tlsChild.getAttribute("password").isEmpty()
|
||||
? tlsChild.getAttribute("password")
|
||||
: null;
|
||||
String keyAlias = tlsChild.getAttribute("key-alias");
|
||||
String keyPassword = !tlsChild.getAttribute("key-password").isEmpty()
|
||||
? tlsChild.getAttribute("key-password")
|
||||
: null;
|
||||
keyStore = new Configuration.KeyStore(keyStoreFile, keyStorePassword, keyAlias, keyPassword);
|
||||
break;
|
||||
|
||||
case "truststore":
|
||||
Path trustStoreFile = Paths.get(tlsChild.getAttribute("file"));
|
||||
String trustStorePassword = !tlsChild.getAttribute("password").isEmpty()
|
||||
? tlsChild.getAttribute("password")
|
||||
: null;
|
||||
boolean checkCertificateStatus = !tlsChild.getAttribute("check-certificate-status").isEmpty() &&
|
||||
Boolean.parseBoolean(tlsChild.getAttribute("check-certificate-status"));
|
||||
trustStore = new Configuration.TrustStore(trustStoreFile, trustStorePassword, checkCertificateStatus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
tls = new Configuration.Tls(keyStore, trustStore, verifyClients);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Configuration.of(host, port, serverPath, users, groups, cache, authentication, tls, useVirtualThread);
|
||||
}
|
||||
|
||||
private static Set<Role> parseRoles(Element root) {
|
||||
return StreamSupport.stream(iterableOf(root).spliterator(), false)
|
||||
.map(node -> switch (node.getNodeName()) {
|
||||
case "reader" -> Role.Reader;
|
||||
case "writer" -> Role.Writer;
|
||||
default -> throw new UnsupportedOperationException("Illegal node '" + node.getNodeName() + "'");
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static Set<String> parseUserRefs(Element root) {
|
||||
return StreamSupport.stream(iterableOf(root).spliterator(), false)
|
||||
.filter(node -> "user".equals(node.getNodeName()))
|
||||
.map(node -> ((Element) node).getAttribute("ref"))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static Map<String, Configuration.User> parseUsers(Element root) {
|
||||
return StreamSupport.stream(iterableOf(root).spliterator(), false)
|
||||
.filter(node -> "user".equals(node.getNodeName()))
|
||||
.map(node -> {
|
||||
Element el = (Element) node;
|
||||
String username = el.getAttribute("name");
|
||||
String password = !el.getAttribute("password").isEmpty() ? el.getAttribute("password") : null;
|
||||
return new AbstractMap.SimpleEntry<>(username, new Configuration.User(username, password, Collections.emptySet()));
|
||||
})
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
private static Map.Entry<Map<String, Configuration.User>, Map<String, Configuration.Group>> parseGroups(Element root, Map<String, Configuration.User> knownUsers) {
|
||||
Map<String, Set<String>> userGroups = new HashMap<>();
|
||||
Map<String, Configuration.Group> groups = StreamSupport.stream(iterableOf(root).spliterator(), false)
|
||||
.filter(node -> "group".equals(node.getNodeName()))
|
||||
.map(node -> {
|
||||
Element el = (Element) node;
|
||||
String groupName = el.getAttribute("name");
|
||||
Set<Role> roles = Collections.emptySet();
|
||||
|
||||
for (Node child : iterableOf(el)) {
|
||||
switch (child.getNodeName()) {
|
||||
case "users":
|
||||
parseUserRefs((Element) child).stream()
|
||||
.map(knownUsers::get)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(user ->
|
||||
userGroups.computeIfAbsent(user.getName(), k -> new HashSet<>())
|
||||
.add(groupName));
|
||||
break;
|
||||
case "roles":
|
||||
roles = parseRoles((Element) child);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new AbstractMap.SimpleEntry<>(groupName, new Configuration.Group(groupName, roles));
|
||||
})
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
Map<String, Configuration.User> users = knownUsers.entrySet().stream()
|
||||
.map(entry -> {
|
||||
String name = entry.getKey();
|
||||
Configuration.User user = entry.getValue();
|
||||
Set<Configuration.Group> userGroupSet = userGroups.getOrDefault(name, Collections.emptySet()).stream()
|
||||
.map(groups::get)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
return new AbstractMap.SimpleEntry<>(name, new Configuration.User(name, user.getPassword(), userGroupSet));
|
||||
})
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
return new AbstractMap.SimpleEntry<>(users, groups);
|
||||
}
|
||||
|
||||
private static Iterable<Node> iterableOf(Element element) {
|
||||
return () -> new Iterator<Node>() {
|
||||
private Node current = element.getFirstChild();
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
while (current != null && !(current instanceof Element)) {
|
||||
current = current.getNextSibling();
|
||||
}
|
||||
return current != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
Node result = current;
|
||||
current = current.getNextSibling();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
5
gbcs-api/src/main/java/net/woggioni/gbcs/api/Role.java
Normal file
5
gbcs-api/src/main/java/net/woggioni/gbcs/api/Role.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
|
||||
public enum Role {
|
||||
Reader, Writer
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package net.woggioni.gbcs.api.exception;
|
||||
|
||||
public class ContentTooLargeException extends GbcsException {
|
||||
public ContentTooLargeException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package net.woggioni.gbcs.api.exception;
|
||||
|
||||
public class GbcsException extends RuntimeException {
|
||||
public GbcsException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user