Anyone can develop a Sentinel Import plugin using the Sentinel
SDK. The SDK contains a high-level
framework for writing plugins in Go including a test
framework. Additionally, the SDK contains the low-level gRPC protocol buffers
definition
for writing plugins in other languages.

The primary reasons to write an import plugin are:

You want to expose or access new information within your Sentinel policies.
For example, you may want to query information from an external service.

You want to expose new functions to Sentinel policies.

This page will document how to write a new Sentinel import in Go using the
Sentinel SDK and the high-level framework provided. You are expected to already
know the basics of Go and have a properly configured Go installation.

Throughout this page, we'll be developing a simple import to access time values.

NOTE: The example here is not reflective of the actual implementation of
the time standard import, so make sure to not
get the two confused.

As a general overview:
framework.Import
implements the
sdk.Import
interface and therefore is a valid import plugin. You must implement the
framework.Root
interface to configure the import. The root may then delegate nested access to
various
framework.Namespace
implementations. Other interfaces are implemented to provide functionality past
what is provided by the basic framework.Namespace implementation - these are
documented below.

This all may sound like a lot of interfaces, but each interface typically only
requires a single function implementation and you're only required to implement
a single namespace (framework.Namespace).

To begin, you must implement the
framework.Root
interface. This is the interface representing the root of your import. The root
itself must be implement
framework.Namespace,
which allows values to be accessed. Note that the root can optionally implement
other interfaces, but they're more advanced and omitted here for simplicity.

The
framework.Namespace
interface implements a namespace of values. You saw above that
framework.Root
must itself be a namespace. However, a namespace Get implementation may
further return namespaces to access nested values.

This enables behavior such as time.month.string vs. time.month.index.
Notice each of these examples accesses a nested value within month. This can
be modeled as namespaces within the framework.

In the example below, we return a new namespace for month to do just this:

A namespace may also return any primitive Go type as well as structs. The
framework automatically exposes primitive values as you would expect. For
structs, exported fields are lowercased and exposed to the policies. The
sentinel struct tag can be used to control how Sentinel can access the field.

In the example below, we expose a struct directly from the root namespace:

The following are optional interfaces that can be implemented on top of
framework.Namespace.
All of these interfaces can be implemented at any level, except for
framework.New, which only works at the root level.

The
framework.Call
interface can be implemented to support function calls.

Implementing this interface is very similar to attribute access, except instead
of returning the attribute value, you return a function. The framework uses Go
reflection to determine the argument and result types and calls it. If the
argument types do not match or the signature is otherwise invalid, an error is
returned.

For example, let's implement a function to add months to the current month:

The optional
framework.New
interface can be implemented on a root namespace to add methods to your
namespaces.

Data returned from a namespace is memoized and returned as a map, and is
normally not callable - to call a function to operate on the data, you would
need to create a new namespace from the top-level of the import, and then make a
function call on the result within the same expression.

Let's consider the root example. Let's say,
instead of hard-coding the time value at configuration, we wanted to load it
on-demand using a time.now key, and allow add_month to be callable on that
value. Our root and de-coupled time namespace would now look like:

Via this example, we can now create and assign a namespaceTime using t =
time.now, and then call t.add_month(months_to_add) to return a
namespaceMonth for the corresponding month.

Methods on a namespace can also modfiy the receiver data. So you can, for
example, add a method named increment_month that takes no arguments, but
increments the time stored in namespaceTime.Time by a month. Subsequent
statements using the receiver would see the new value.

Note that there are some restrictions and guidelines that you should take into
account when using framework.New:

Only data that makes it back to a policy can be used as receiver data to
instantiate new namespaces. As such it's recommended to make use of
framework.Map
and
framework.MapFromKeys
to return all of the attribute data you need to construct new objects.

framework.New is only supported on the root namespace and has no effect on
namespaces below the root. Check all possible cases of receiver data within
the top-level New function and return the appropriate namespace based on the
input data.

Do not return general errors from your New method. When an unknown lookup is
made off of memoized data, it will hit your New method for possible
instantiation and key calls. This will ensure undefined is correctly
returned for these cases.

framework.New is designed to add utility to imports where calling methods on
a value is more intuitive than just making a function call, or just returning
all of a data set as a memoized map. Use it sparingly and avoid using it on
recursively complex data sets.

The SDK exposes a testing framework to verify your plugin works as
expected. This test framework integrates directly into go test so it
is part of a familiar workflow.

The test framework works by dynamically building your import and
running it against the sentinel CLI to verify it behaves as expected.
This ensures that your plugin builds, your plugin communicates via RPC
correctly, and that the behavior within a policy is also correct.

The example below shows a complete ready table-driven test for our time plugin.
You can run this with a normal go test.