Custom Built-in Functions
OpaDotNet can be extended with custom built-in functions.
OPA supports built-in functions for simple operations like string manipulation and arithmetic as well as more complex operations like JWT verification and executing HTTP requests. If you need to extend OPA with custom built-in functions for use cases or integrations that are not supported out-of-the-box you can supply the function definitions when you prepare queries.
Using custom built-in functions involves providing a declaration and implementation. The declaration tells OPA the function’s type signature and the implementation provides the callback that OPA can execute during query evaluation.
Implementation
Implement IOpaCustomBuiltins and mark built-in methods with
OpaCustomBuiltin attribute (v3.0+). In v2.5 custom built-ins
were implemented by extending DefaultOpaImportsAbi class. See Advanced Built-ins
for details on supported arguments and return types.
In the following sample, we define 5 custom functions:
custom.zeroArgBuiltincustom.oneArgBuiltincustom.twoArgBuiltincustom.threeArgBuiltincustom.fourArgBuiltin
Note
OPA supports custom functions with up to four arguments.
public class OpaCustomBuiltinsV32 : IOpaCustomBuiltins
{
public void Reset()
{
}
// Built-in with zero arguments.
[OpaCustomBuiltin("custom.zeroArgBuiltin")]
public static string ZeroArgBuiltin() => "hello";
// Built-in with one argument.
[OpaCustomBuiltin("custom.oneArgBuiltin")]
public static string OneArgBuiltin(string arg1) => $"hello {arg1}";
// Built-in with two arguments.
[OpaCustomBuiltin("custom.twoArgBuiltin")]
public static string TwoArgBuiltin(string arg1, string arg2) => $"hello {arg1} {arg2}";
// Async built-in with three arguments.
[OpaCustomBuiltin("custom.threeArgBuiltin")]
public static Task<string> ThreeArgBuiltin(string arg1, string arg2, string arg3)
=> Task.FromResult($"hello {arg1} {arg2} {arg3}");
// Async built-in with four arguments with cancellation.
[OpaCustomBuiltin("custom.fourArgBuiltin")]
public static async Task<string> FourArgBuiltin(string arg1, string arg2, string arg3, string arg4, IOpaCustomBuiltinsContext context)
{
await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken);
return $"hello {arg1} {arg2} {arg3} {arg4}";
}
}
Declaration
To make OPA aware of custom built-ins we need to add capabilities file containing the types for declarations and runtime objects passed to your implementation.
Important
Custom built-ins are supported only for policy bundles.
capabilities.json
{
"builtins": [
{
"name": "custom.zeroArgBuiltin",
"decl": {
"type": "function",
"args": [],
"result": {
"type": "string"
}
}
},
{
"name": "custom.oneArgBuiltin",
"decl": {
"type": "function",
"args": [
{
"type": "string"
}
],
"result": {
"type": "string"
}
}
},
{
"name": "custom.oneArgObjectBuiltin",
"decl": {
"type": "function",
"args": [
{
"type": "object"
}
],
"result": {
"type": "string"
}
}
},
{
"name": "custom.twoArgBuiltin",
"decl": {
"type": "function",
"args": [
{
"type": "string"
},
{
"type": "string"
}
],
"result": {
"type": "string"
}
}
},
{
"name": "custom.threeArgBuiltin",
"decl": {
"type": "function",
"args": [
{
"type": "string"
},
{
"type": "string"
},
{
"type": "string"
}
],
"result": {
"type": "string"
}
}
},
{
"name": "custom.fourArgBuiltin",
"decl": {
"type": "function",
"args": [
{
"type": "string"
},
{
"type": "string"
},
{
"type": "string"
},
{
"type": "string"
}
],
"result": {
"type": "string"
}
}
}
]
}
For more information on capabilities see
Policy
policy.rego
package custom_builtins
zero_arg = x {
x = custom.zeroArgBuiltin()
}
one_arg = x {
x = custom.oneArgBuiltin(input.args[0])
}
two_arg = x {
x = custom.twoArgBuiltin(input.args[0], input.args[1])
}
three_arg = x {
x = custom.threeArgBuiltin(input.args[0], input.args[1], input.args[2])
}
four_arg = x {
x = custom.fourArgBuiltin(input.args[0], input.args[1], input.args[2], input.args[3])
}
Compilation
Place policy and capabilities files into bundle directory.
Next, we need to compile the policy bundle and make it aware of our custom built-ins:
var compilationParameters = new CompilationParameters
{
// Custom built-ins will be merged with capabilities v0.53.1.
CapabilitiesVersion = "v0.53.1",
// Provide built-ins capabilities for the compiler.
CapabilitiesFilePath = Path.Combine("builtins", "capabilities.json"),
Entrypoints =
[
"custom_builtins/zero_arg",
"custom_builtins/one_arg",
"custom_builtins/two_arg",
"custom_builtins/three_arg",
"custom_builtins/four_arg",
],
};
var compiler = new RegoInteropCompiler();
var policy = await compiler.CompileBundleAsync(
"builtins",
compilationParameters,
cancellationToken
);
var opts = new WasmPolicyEngineOptions();
opts.ConfigureBuiltins(p => p.CustomBuiltins.Add(new OpaCustomBuiltinsV32()));
using var factory = new OpaBundleEvaluatorFactory(policy, opts);
var engine = factory.Create();
Execution
Now we can evaluate policies using custom built-ins:
var resultZeroArg = engine.Evaluate<object, string>(new object(), "custom_builtins/zero_arg");
Console.WriteLine(resultZeroArg.Result);
var resultOneArg = engine.Evaluate<object, string>(
new { args = new[] { "arg0" } },
"custom_builtins/one_arg"
);
Console.WriteLine(resultOneArg.Result);
var resultTwoArg = engine.Evaluate<object, string>(
new { args = new[] { "arg0", "arg1" } },
"custom_builtins/two_arg"
);
Console.WriteLine(resultTwoArg.Result);
var resultThreeArg = engine.Evaluate<object, string>(
new { args = new[] { "arg0", "arg1", "arg2" } },
"custom_builtins/three_arg"
);
Console.WriteLine(resultThreeArg.Result);
var resultFourArg = engine.Evaluate<object, string>(
new { args = new[] { "arg0", "arg1", "arg2", "arg3" } },
"custom_builtins/four_arg"
);
Console.WriteLine(resultFourArg.Result);
If you executed this code the output would be:
hello
hello arg0
hello arg0 arg1
hello arg0 arg1 arg2
hello arg0 arg1 arg2 arg3