Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class BuiltinRegistry {
HexBuiltins.class,
StringBuiltins.class,
PrintBuiltins.class,
UUIDBuiltins.class,
};

public static final Map<String, BiFunction<EvaluationContext, RegoValue[], RegoValue>>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package io.github.open_policy_agent.opa.ast.builtin.impls;

import static io.github.open_policy_agent.opa.ast.builtin.impls.utils.ArgHelper.getArg;

import io.github.open_policy_agent.opa.ast.builtin.OpaBuiltin;
import io.github.open_policy_agent.opa.ast.builtin.OpaType;
import io.github.open_policy_agent.opa.ast.types.RegoBigInt;
import io.github.open_policy_agent.opa.ast.types.RegoInt32;
import io.github.open_policy_agent.opa.ast.types.RegoObject;
import io.github.open_policy_agent.opa.ast.types.RegoString;
import io.github.open_policy_agent.opa.ast.types.RegoUndefined;
import io.github.open_policy_agent.opa.ast.types.RegoValue;
import io.github.open_policy_agent.opa.rego.EvaluationContext;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiFunction;

public class UUIDBuiltins {

private static final long UUID_EPOCH_OFFSET_100NS = 122192928000000000L;

public static Map<String, BiFunction<EvaluationContext, RegoValue[], RegoValue>> builtins() {
UUIDBuiltins instance = new UUIDBuiltins();
return Map.of(
"uuid.parse", instance::parse,
"uuid.rfc4122", instance::rfc4122);
}

@OpaBuiltin(
name = "uuid.parse",
description = "Parses a UUID string into its RFC 4122 metadata.",
categories = {"uuid"},
args = {@OpaType(type = "string", name = "uuid", description = "UUID string to parse")},
result = @OpaType(type = "object", name = "result", description = "parsed UUID metadata"))
public RegoValue parse(EvaluationContext ctx, RegoValue[] args) {
String input = getArg(args, 0, RegoString.class).getValue();
UUID uuid = parseUuid(input);
if (uuid == null) {
return RegoUndefined.INSTANCE;
}

RegoObject result = new RegoObject();
int version = uuid.version();
result.setProperty("variant", new RegoString(variantName(uuid.variant())));
result.setProperty("version", RegoInt32.of(version));

if (version == 1 || version == 2) {
byte[] bytes = toBytes(uuid);
result.setProperty("clocksequence", RegoInt32.of(clockSequence(uuid)));
result.setProperty("macvariables", new RegoString(macVariables(bytes[10])));
result.setProperty("nodeid", new RegoString(nodeId(bytes)));
result.setProperty("time", new RegoBigInt((timestamp(uuid) - UUID_EPOCH_OFFSET_100NS) * 100L));

if (version == 2) {
result.setProperty("domain", new RegoString(domain(bytes[9])));
result.setProperty("id", RegoInt32.of(ByteBuffer.wrap(bytes, 0, 4).getInt()));
}
}

return result;
}

@OpaBuiltin(
name = "uuid.rfc4122",
description = "Returns a version 4 RFC 4122 UUID.",
categories = {"uuid"},
args = {
@OpaType(
type = "string",
name = "key",
description = "cache key for deterministic UUID generation during evaluation")
},
result = @OpaType(type = "string", name = "uuid", description = "RFC 4122 UUID"),
nondeterministic = true)
public RegoString rfc4122(EvaluationContext ctx, RegoValue[] args) {
if (ctx != null && ctx.getNdBuiltinCache() != null) {
RegoValue cachedValue = ctx.getNdBuiltinCache().get("uuid.rfc4122", args);
if (cachedValue != null) {
return (RegoString) cachedValue;
}
}

String key = getArg(args, 0, RegoString.class).getValue();
RegoString result = new RegoString(deterministicVersion4Uuid(key));

if (ctx != null) {
if (ctx.getNdBuiltinCache() != null) {
ctx.getNdBuiltinCache().put("uuid.rfc4122", args, result);
}
ctx.recordNdCacheValue("uuid.rfc4122", args, result);
}

return result;
}

private UUID parseUuid(String input) {
String normalized = normalize(input);
if (normalized == null) {
return null;
}

try {
return UUID.fromString(normalized);
} catch (IllegalArgumentException e) {
return null;
}
}

private String normalize(String input) {
String value = input;
if (value.startsWith("urn:uuid:")) {
value = value.substring("urn:uuid:".length());
}
if (value.startsWith("{") && value.endsWith("}")) {
value = value.substring(1, value.length() - 1);
}
if (value.length() == 32) {
value =
value.substring(0, 8)
+ "-"
+ value.substring(8, 12)
+ "-"
+ value.substring(12, 16)
+ "-"
+ value.substring(16, 20)
+ "-"
+ value.substring(20);
}
return value;
}

private long timestamp(UUID uuid) {
long most = uuid.getMostSignificantBits();
long timeLow = (most >>> 32) & 0xffffffffL;
long timeMid = (most >>> 16) & 0xffffL;
long timeHigh = most & 0x0fffL;
return (timeHigh << 48) | (timeMid << 32) | timeLow;
}

private int clockSequence(UUID uuid) {
return (int) ((uuid.getLeastSignificantBits() >>> 48) & 0x3fffL);
}

private String variantName(int variant) {
switch (variant) {
case 0:
return "Reserved";
case 2:
return "RFC4122";
case 6:
return "Microsoft";
default:
return "Future";
}
}

private String domain(byte value) {
switch (Byte.toUnsignedInt(value)) {
case 0:
return "Person";
case 1:
return "Group";
case 2:
return "Org";
default:
return "Invalid";
}
}

private String macVariables(byte value) {
int bits = value & 0b11;
switch (bits) {
case 0b11:
return "local:multicast";
case 0b01:
return "global:multicast";
case 0b10:
return "local:unicast";
default:
return "global:unicast";
}
}

private String nodeId(byte[] bytes) {
StringBuilder result = new StringBuilder(17);
for (int i = 10; i < bytes.length; i++) {
if (i > 10) {
result.append("-");
}
result.append(String.format("%02x", Byte.toUnsignedInt(bytes[i])));
}
return result.toString();
}

private byte[] toBytes(UUID uuid) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
return buffer.array();
}

private String deterministicVersion4Uuid(String key) {
try {
byte[] bytes =
MessageDigest.getInstance("SHA-256").digest(key.getBytes(StandardCharsets.UTF_8));
bytes[6] = (byte) ((bytes[6] & 0x0f) | 0x40);
bytes[8] = (byte) ((bytes[8] & 0x3f) | 0x80);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
return new UUID(buffer.getLong(), buffer.getLong()).toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 digest is not available", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.github.open_policy_agent.opa.ast.builtin.impls;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.github.open_policy_agent.opa.ast.builtin.BuiltinRegistry;
import io.github.open_policy_agent.opa.ast.types.RegoBigInt;
import io.github.open_policy_agent.opa.ast.types.RegoInt32;
import io.github.open_policy_agent.opa.ast.types.RegoObject;
import io.github.open_policy_agent.opa.ast.types.RegoString;
import io.github.open_policy_agent.opa.ast.types.RegoUndefined;
import io.github.open_policy_agent.opa.ast.types.RegoValue;
import java.util.UUID;
import org.junit.jupiter.api.Test;

class UUIDBuiltinsTest {

private final UUIDBuiltins builtins = new UUIDBuiltins();

@Test
void registersUuidBuiltins() {
assertTrue(BuiltinRegistry.AllBuiltIns.containsKey("uuid.parse"));
assertTrue(BuiltinRegistry.AllBuiltIns.containsKey("uuid.rfc4122"));
}

@Test
void parsesVersion4Uuid() {
RegoObject result =
(RegoObject)
builtins.parse(
null, new RegoValue[] {new RegoString("00000000-0000-4000-8000-000000000000")});

assertEquals(new RegoString("RFC4122"), result.getProperty("variant"));
assertEquals(RegoInt32.of(4), result.getProperty("version"));
}

@Test
void parsesVersion2UuidMetadata() {
RegoObject result =
(RegoObject)
builtins.parse(
null, new RegoValue[] {new RegoString("000003e8-48b9-21ee-b200-325096b39f47")});

assertEquals(RegoInt32.of(12800), result.getProperty("clocksequence"));
assertEquals(new RegoString("Person"), result.getProperty("domain"));
assertEquals(RegoInt32.of(1000), result.getProperty("id"));
assertEquals(new RegoString("local:unicast"), result.getProperty("macvariables"));
assertEquals(new RegoString("32-50-96-b3-9f-47"), result.getProperty("nodeid"));
assertEquals(new RegoBigInt(1693566990121469600L), result.getProperty("time"));
assertEquals(new RegoString("RFC4122"), result.getProperty("variant"));
assertEquals(RegoInt32.of(2), result.getProperty("version"));
}

@Test
void parsesAcceptedInputFormats() {
assertEquals(
RegoInt32.of(4),
((RegoObject)
builtins.parse(
null,
new RegoValue[] {new RegoString("{00000000-0000-4000-8000-000000000000}")}))
.getProperty("version"));
assertEquals(
RegoInt32.of(2),
((RegoObject)
builtins.parse(
null,
new RegoValue[] {
new RegoString("urn:uuid:000003e8-48b9-21ee-b200-325096b39f47")
}))
.getProperty("version"));
assertEquals(
RegoInt32.of(3),
((RegoObject)
builtins.parse(
null, new RegoValue[] {new RegoString("38074da40b00388d9c3c362de965547a")}))
.getProperty("version"));
}

@Test
void parseReturnsUndefinedForInvalidUuid() {
assertSame(RegoUndefined.INSTANCE, builtins.parse(null, new RegoValue[] {new RegoString("123")}));
}

@Test
void rfc4122ReturnsConsistentVersion4UuidForSameKey() {
RegoString first = builtins.rfc4122(null, new RegoValue[] {new RegoString("key")});
RegoString second = builtins.rfc4122(null, new RegoValue[] {new RegoString("key")});
UUID parsed = UUID.fromString(first.getValue());

assertEquals(first, second);
assertEquals(4, parsed.version());
assertEquals(2, parsed.variant());
}
}