package com.xfestudio.xfeservermanager.core.policy;
import com.xfestudio.xfeservermanager.api.policy.PolicyRule;
import com.xfestudio.xfeservermanager.api.policy.PolicySnapshot;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;
import java.util.List;
import java.util.Base64;
public final class PolicySnapshotFactory {
private PolicySnapshotFactory() {
}
public static PolicySnapshot create(
long version, Instant createdAt, String createdBy, String description, List<PolicyRule> rules) {
return new PolicySnapshot(version, createdAt, createdBy, description, rules,
checksum(version, createdAt, createdBy, description, rules, true));
}
public static boolean hasValidChecksum(PolicySnapshot snapshot) {
String expected = create(snapshot.version(), snapshot.createdAt(), snapshot.createdBy(),
snapshot.description(), snapshot.rules()).checksum();
if (equalChecksum(expected, snapshot.checksum())) return true;
// Existing v1 snapshots did not serialize policy-group metadata. Accept their checksum
// only when every rule resolves to the exact backward-compatible default group.
if (snapshot.rules().stream().anyMatch(rule ->
!PolicyRule.DEFAULT_GROUP_ID.equals(rule.groupId())
|| !PolicyRule.DEFAULT_GROUP_NAME.equals(rule.groupName())
|| !rule.groupEnabled())) {
return false;
}
return equalChecksum(checksum(snapshot.version(), snapshot.createdAt(), snapshot.createdBy(),
snapshot.description(), snapshot.rules(), false), snapshot.checksum());
}
static String checksum(long version, Instant createdAt, String createdBy, String description,
List<PolicyRule> rules, boolean includeGroups) {
String canonical = field(version) + '|' + field(createdAt) + '|' + field(createdBy) + '|'
+ field(description) + '\n' + canonicalize(rules, includeGroups);
return sha256(canonical);
}
private static String canonicalize(List<PolicyRule> rules, boolean includeGroups) {
StringBuilder result = new StringBuilder();
for (PolicyRule rule : rules.stream().sorted(java.util.Comparator.comparing(PolicyRule::id)).toList()) {
result.append(field(rule.id())).append('|')
.append(field(rule.enabled())).append('|');
if (includeGroups) {
result.append(field(rule.groupId())).append('|')
.append(field(rule.groupName())).append('|')
.append(field(rule.groupEnabled())).append('|');
}
result.append(field(rule.tier())).append('|')
.append(sorted(rule.subject().actorIds())).append('|')
.append(sorted(rule.subject().roles())).append('|')
.append(field(rule.subject().minimumOpLevel())).append('|')
.append(field(rule.subject().maximumOpLevel())).append('|')
.append(sorted(rule.subject().sources())).append('|')
.append(field(rule.subject().onlineMode())).append('|')
.append(sorted(rule.command().commandIds())).append('|')
.append(sequence(rule.command().pathPrefix())).append('|')
.append(field(rule.effect())).append('|')
.append(sorted(rule.constraints().selector().deniedSelectorTypes())).append('|')
.append(sorted(rule.constraints().selector().deniedSorts())).append('|')
.append(field(rule.constraints().selector().allowBroadSelectorWithExplicitType())).append('|')
.append(field(rule.constraints().selector().maximumTargets())).append('|')
.append(field(rule.constraints().selector().maximumDistance())).append('|')
.append(sorted(rule.constraints().selector().allowedEntityTypes())).append('|')
.append(sorted(rule.constraints().selector().deniedEntityTypes())).append('|')
.append(field(rule.constraints().selector().allowTagFilters())).append('|')
.append(field(rule.constraints().selector().allowNbtFilters())).append('|')
.append(field(rule.constraints().selector().allowPredicateFilters())).append('|')
.append(sorted(rule.constraints().item().allowedItemIds())).append('|')
.append(sorted(rule.constraints().item().deniedItemIds())).append('|')
.append(sorted(rule.constraints().item().allowedItemTags())).append('|')
.append(sorted(rule.constraints().item().deniedItemTags())).append('|')
.append(field(rule.constraints().item().maximumCount())).append('|')
.append(sorted(rule.constraints().item().allowedDataKeys())).append('|')
.append(sorted(rule.constraints().item().deniedDataKeys())).append('|')
.append(sorted(rule.constraints().allowedFunctionIds())).append('|')
.append(field(rule.constraints().denyUnlistedFunctions())).append('|')
.append(field(rule.reason())).append('\n');
}
return result.toString();
}
private static boolean equalChecksum(String first, String second) {
return MessageDigest.isEqual(
first.getBytes(StandardCharsets.US_ASCII),
second.getBytes(StandardCharsets.US_ASCII));
}
private static String sorted(java.util.Collection<?> values) {
return values.stream().map(String::valueOf).sorted().map(PolicySnapshotFactory::field)
.collect(java.util.stream.Collectors.joining(",", "[", "]"));
}
private static String sequence(java.util.Collection<?> values) {
return values.stream().map(String::valueOf).map(PolicySnapshotFactory::field)
.collect(java.util.stream.Collectors.joining(",", "[", "]"));
}
private static String field(Object value) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(
String.valueOf(value).getBytes(StandardCharsets.UTF_8));
}
private static String sha256(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required by the Java runtime", exception);
}
}
}
package com.xfestudio.xfeservermanager.core.policy;
import com.xfestudio.xfeservermanager.api.policy.PolicyRule;
import com.xfestudio.xfeservermanager.api.policy.PolicySnapshot;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;
import java.util.List;
import java.util.Base64;
public final class PolicySnapshotFactory {
private PolicySnapshotFactory() {
}
public static PolicySnapshot create(
long version, Instant createdAt, String createdBy, String description, List<PolicyRule> rules) {
return new PolicySnapshot(version, createdAt, createdBy, description, rules,
checksum(version, createdAt, createdBy, description, rules, true));
}
public static boolean hasValidChecksum(PolicySnapshot snapshot) {
String expected = create(snapshot.version(), snapshot.createdAt(), snapshot.createdBy(),
snapshot.description(), snapshot.rules()).checksum();
if (equalChecksum(expected, snapshot.checksum())) return true;
// Existing v1 snapshots did not serialize policy-group metadata. Accept their checksum
// only when every rule resolves to the exact backward-compatible default group.
if (snapshot.rules().stream().anyMatch(rule ->
!PolicyRule.DEFAULT_GROUP_ID.equals(rule.groupId())
|| !PolicyRule.DEFAULT_GROUP_NAME.equals(rule.groupName())
|| !rule.groupEnabled())) {
return false;
}
return equalChecksum(checksum(snapshot.version(), snapshot.createdAt(), snapshot.createdBy(),
snapshot.description(), snapshot.rules(), false), snapshot.checksum());
}
static String checksum(long version, Instant createdAt, String createdBy, String description,
List<PolicyRule> rules, boolean includeGroups) {
String canonical = field(version) + '|' + field(createdAt) + '|' + field(createdBy) + '|'
+ field(description) + '\n' + canonicalize(rules, includeGroups);
return sha256(canonical);
}
private static String canonicalize(List<PolicyRule> rules, boolean includeGroups) {
StringBuilder result = new StringBuilder();
for (PolicyRule rule : rules.stream().sorted(java.util.Comparator.comparing(PolicyRule::id)).toList()) {
result.append(field(rule.id())).append('|')
.append(field(rule.enabled())).append('|');
if (includeGroups) {
result.append(field(rule.groupId())).append('|')
.append(field(rule.groupName())).append('|')
.append(field(rule.groupEnabled())).append('|');
}
result.append(field(rule.tier())).append('|')
.append(sorted(rule.subject().actorIds())).append('|')
.append(sorted(rule.subject().roles())).append('|')
.append(field(rule.subject().minimumOpLevel())).append('|')
.append(field(rule.subject().maximumOpLevel())).append('|')
.append(sorted(rule.subject().sources())).append('|')
.append(field(rule.subject().onlineMode())).append('|')
.append(sorted(rule.command().commandIds())).append('|')
.append(sequence(rule.command().pathPrefix())).append('|')
.append(field(rule.effect())).append('|')
.append(sorted(rule.constraints().selector().deniedSelectorTypes())).append('|')
.append(sorted(rule.constraints().selector().deniedSorts())).append('|')
.append(field(rule.constraints().selector().allowBroadSelectorWithExplicitType())).append('|')
.append(field(rule.constraints().selector().maximumTargets())).append('|')
.append(field(rule.constraints().selector().maximumDistance())).append('|')
.append(sorted(rule.constraints().selector().allowedEntityTypes())).append('|')
.append(sorted(rule.constraints().selector().deniedEntityTypes())).append('|')
.append(field(rule.constraints().selector().allowTagFilters())).append('|')
.append(field(rule.constraints().selector().allowNbtFilters())).append('|')
.append(field(rule.constraints().selector().allowPredicateFilters())).append('|')
.append(sorted(rule.constraints().item().allowedItemIds())).append('|')
.append(sorted(rule.constraints().item().deniedItemIds())).append('|')
.append(sorted(rule.constraints().item().allowedItemTags())).append('|')
.append(sorted(rule.constraints().item().deniedItemTags())).append('|')
.append(field(rule.constraints().item().maximumCount())).append('|')
.append(sorted(rule.constraints().item().allowedDataKeys())).append('|')
.append(sorted(rule.constraints().item().deniedDataKeys())).append('|')
.append(sorted(rule.constraints().allowedFunctionIds())).append('|')
.append(field(rule.constraints().denyUnlistedFunctions())).append('|')
.append(field(rule.reason())).append('\n');
}
return result.toString();
}
private static boolean equalChecksum(String first, String second) {
return MessageDigest.isEqual(
first.getBytes(StandardCharsets.US_ASCII),
second.getBytes(StandardCharsets.US_ASCII));
}
private static String sorted(java.util.Collection<?> values) {
return values.stream().map(String::valueOf).sorted().map(PolicySnapshotFactory::field)
.collect(java.util.stream.Collectors.joining(",", "[", "]"));
}
private static String sequence(java.util.Collection<?> values) {
return values.stream().map(String::valueOf).map(PolicySnapshotFactory::field)
.collect(java.util.stream.Collectors.joining(",", "[", "]"));
}
private static String field(Object value) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(
String.valueOf(value).getBytes(StandardCharsets.UTF_8));
}
private static String sha256(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required by the Java runtime", exception);
}
}
}