Table of Contents

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.zeroArgBuiltin
  • custom.oneArgBuiltin
  • custom.twoArgBuiltin
  • custom.threeArgBuiltin
  • custom.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