Sentinel: Introduction to Sentinel Language – Part-1

Share At:

複雑なポリシーを適切に管理する、HashiCorp Vault SSH CA動的シークレットエンジンとSentinel | セキュリティ対策のラック

This Article only discusses about following topics in Sentinel Language:

  • Variables
  • Values
  • Lists
  • Maps

Sentinel policies are written using the Sentinel language. This language is easy to learn and easy to write. You can learn the Sentinel language and be productive within an hour. Learning Sentinel doesn’t require any formal programming experience.

Simplest Example

The example below is about the simplest practical example of Sentinel. It is reasonable to imagine this as a realistic policy. This shows that in most cases, Sentinel will be extremely simple:

main = rule { request.method is "GET" and request.headers contains "X-Key" }

Files

Sentinel policies are single files that end in the .sentinel file extension. There is currently no built-in mechanism to Sentinel for merging multiple files. This is purposefully done to make Sentinel policies easy to submit to systems that support Sentinel policies.

Ordering

Sentinel policies are executed top-down. For example:

a = 1      // a = 1 here
b = a + 1  // b = 2 here
a = 3      // a = 3, b = 2

In this example, the value of a and b is shown at each line. Since Sentinel executes values top-down, the final value of a is 3 and b is 2. b does not become 4.

Main

Sentinel expects there to be a main rule. The value of this rule is the result of the entire policy.

The result of the policy depends on the evaluated contents of the main rule. For booleans, a policy passes on a true value, and fails on a false value. Other types generally follow a zero or zero-length pattern for determining success.

TypePassing ConditionFailing Condition
Booleantruefalse
String""Any non-zero length string
Integer0Any non-zero value
Float0.0Any non-zero value
List[]Any non-zero length list
Map{}Any non-zero length map

A value of main that falls outside of the above types will result in a policy error. As a special case, if main evaluates to an undefined value, the error message will indicate as such, with a reference to where the undefined value was encountered.

More Complex Example

The simple example above is a full working example. In our experience with Sentinel, many policies can be representing using this simple form. However, to show more features of the language, a more complex example is shown below. This example is also a realistic example of what Sentinel may be used for.

import "units"

memory = func(job) {
  result = 0
  for job.groups as g {
    for g.tasks as t {
      result += t.resources.memory else 0
    }
  }

  return result
}

main = rule {
  memory(job) < 1 * units.gigabyte
}

Language: Variables

Variables store values that can be used later.

Most notably, a variable assignment of main is required for all Sentinel policies. This is the main rule that is executed to determine the result of a policy. The main rule may use other variables that are assigned in the policy.

Example:

a = 1
b = a + 1

In this example, two variables are assigned: a and ba is given the value of “1” and b uses the a to calculate its own value.

Syntax

The syntax for assigning a variable is:

IDENTIFIER = VALUE

On the left of the equal sign is an identifier. This is a name for your variable and will be how you reference it later. An identifier is any combination of letters and digits, but must start with a letter. An underscore _ is also a valid letter.

On the right is any valid Sentinel value or expression. A value is a literal value such as a number or string. An expression is some computed value such as doing math, calling a function, etc.

Assignment and Reassignment

A variable is assigned when it is given a value. You can also reassign variables at any time by setting it to a new value. The new value takes effect for any subsequent use of that variable. A variable can be reassigned to a different type.

For example:

a = 1       // a = 1
b = a       // b = 1
a = "value" // a = "value", b = 1
c = a       // c = "value", b = 1

In the above example you can see that the variables are set and reassigned. Notice that the value of a variable is the current value, and that reassigning a variable only affects future uses of that variable. You can see this with c and b being different values.

Unassigned Variables

Using a variable that is unassigned is an error.

In the example below, the first line would result in the policy erroring. Sentinel is executed top-down and the value of c is not available yet on the first line.

a = c // Error!
c = 1

Type Conversion

Variables can be assigned (or-reassigned) a different value type via type conversion.

s = "1.1"
a = int(s)   // a = 1
a = float(s) // a = 1.1

a = 1
s = string(a) // s = "1"

Language: Values

Values are the data that Sentinel policies operate on. You can create this data yourself, for example by just typing the number 42, or you may access this data from an external source to make policy decisions.

Values have a type, which determines what kind of operatins can be performed on the data. Example types are booleans (true and false), numbers, and strings (text).

This section documents all the available value types.

Boolean

A boolean is a value that is either true or false.

A boolean value is created literally with true or false.

As a policy language, booleans are central to the behavior of Sentinel. Booleans are used as conditions in if statements, are the result of rules, and more. The ultimate result of a Sentinel policy is true or false.

Integer

An integer is a whole number.

An integer is a 64-bit value. This means it can represent numbers from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,808.

Integers are created by typing them out literally with no separators (such as a comma). An optional prefix can set a non-decimal base: 0 for octal, and 0x for hexadecimal. In hexadecimal literals, letters a-f and A-F represent values 10 to 15.

Example integers are shown below:

42
0600
0xBadFace
170141183460469

Integers are used for math and numerical comparison.

Float

A float is a number with a decimal point. It has an integer part, a decimal point, a fractional part, and optionally an exponent part. Example floats are shown below:

0.
72.40
072.40  // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5

Floats are IEEE-754 64-bit floating point numbers.

String

Strings are text values.

Strings are created by wrapping text in double quotes, such as "hello". Within the quotes, any character may appear except newline and an unescaped double quote. The text between the quotes is the value.

Because Sentinel policies must be UTF-8 encoded text, strings themselves are UTF-8 encoded text. This means you can put any value UTF-8 character into a string, such as "日本語".

You can have a string with a literal double-quote by escaping it with a backslash. For example: "they said \"hello\"" turns into the value they said "hello".

Backslash escapes can be used for much more than only escaping a double quote. Newlines are \n, a literal backslash is \\, and many more

Example strings:

`abc`                // same as "abc"
`\n
\n`                  // same as "\\n\n\\n"
"\n"
"\""                 // same as `"`
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800"             // illegal: surrogate half
"\U00110000"         // illegal: invalid Unicode code point

Strings support indexing. Indexing a string will access that byte in the string as if the string were a byte array. This is an important distinction: it will not access the character at that position if you’re using multi-byte characters.

Strings support slicing to efficiently create substrings.

Type Conversion

The built-in functions intfloatstring, and bool convert a value to a value of that type. Some examples are shown below followed by a list of exact rules for type conversion.

int(42)   // 42
int("42") // 42
int(42.8) // 42
int(true) // 1

float(1.2)   // 1.2
float(1)     // 1.0
float("4.2") // 4.2
float(true)  // 1.0

string("foo") // "foo"
string(88)    // "88"
string(0xF)   // "15"
string(true)  // "true"

bool("true")  // true
bool(1)       // true
bool(-1)      // true
bool(0.1)     // true
bool("false") // false
bool(0)       // false

For int:

  • Integer values are unchanged
  • String values are converted according to the syntax of integer literals
  • Float values are rounded down to their nearest integer value
  • Boolean values are converted to 1 for true, and 0 for false

For float:

  • Float values are unchanged
  • Integer values are converted to the nearest equivalent floating point value
  • String values are converted according to the syntax of float literals
  • Boolean values are converted to 1.0 for true, and 0.0 for false

For string:

  • String values are unchanged
  • Integer values are converted to the base 10 string representation
  • Float values are converted to a string formatted xxx.xxx with a precision of 6. This is equivalent to %f for C’s sprintf.
  • Boolean values are converted to "true" for true, and "false" for false

For bool:

  • The following string values convert to true"1""t""T""TRUE""true", and "True"
  • The following string values convert to false"0""f""F""FALSE""false", and "False"
  • Any non-zero integer or float value converts to true
  • Any zero integer or float value converts to false

For any other unspecified type, the result is the undefined value.

Language: Lists

Lists are a collection of zero or more values.

Lists can be created using by wrapping values in [] and separating them by commas. An optional trailing comma is allowed. List elements can be differing types. Examples:

[]                  // An empty list
["foo"]             // Single element list
["foo", 1, 2, true] // Multi element list with different types
["foo", [1, 2]]     // List containing another list

A list can be sliced to create sublists. The set operators can be used to test for value inclusion in a list.

Accessing Elements

List elements can be accessed with the syntax name[index] where index is zero-indexed.

A negative index accesses the list in reverse. It is the same as reversing a list and then using a positive index. Similar to a positive index, it is bounded by the length of the list as a negative value.

Accessing beyond the length of the list results in undefined.

Examples:

a = ["foo", 1, true, [1, 2]]

a[0]    // "foo"
a[2]    // true
a[4]    // undefined
a[-2]   // true
a[-4]   // "foo"
a[-5]   // undefined
a[3][1] // 2

List Append

Values can be appended to a list using the built-in append function.

This modifies the list in-place and returns undefined. For more information on why append behaves this way, please read the full documentation for the append function.

append([1,2], 3)      // [1, 2, 3]
append([1,2], "foo")  // [1, 2, "foo"]
append([1,2], [3])    // [1, 2, [3]]
append(1, 3)          // error()

List Concatenation

Two lists can be concatenated using the + operator or the shorthand += assignment operator. For the + operator, a new list is returned. For +=, the left-hand list is modified in place.

Examples:

[1] + [2]   // [1, 2]
[1] + [[1]] // [1, [1]]
[1] + 1     // error

a = [1]
a += [2]    // a = [1, 2]
a += 3      // error

List Length

The length of a list can be retrieved using the length function.

Examples:

length([])      // 0
length(["foo"]) // 1

Removing Items From a List

You can use a combination of list concatenation and slices to remove elements from a list.

a = [1, 2, 3, 4, 5]
a = a[:2] + a[3:] // [1, 2, 4, 5]

The shorthand shown here is effectively the same as a[0:2] + a[3:length(a)], which creates a new list out of the concatenation two sub-lists composed of the first two elements, and the rest of the list starting at index 3. This effectively removes the 3rd element from the list (index 2).

List Comparison

Lists may be compared for equality. Lists are equal if they are of equal length and their corresponding elements are comparable and equal. Lists with the same elements but in a different order are not equal.

[1, 2] is [1, 2]                       // true
[1, 2] is [2, 1]                       // false
["a"] is ["a", "b"]                    // false
["a", ["b", "c"]] is ["a", ["b", "c"]] // true

List comparison speed is O(N), meaning that the speed of the comparison is linearly proportional to the number of elements in the list. The more elements, the more iterations that are necessary to verify equality.

The N-value quoted above should account for the sum of the elements of all lists in the subjects of comparison, as list comparison will recurse into these lists to check for equality.

Language: Maps

Maps are a collection of zero or more key/value pairs. These are useful for storing values that are looked up by a unique key.

Maps can be created using {} and specifying key/value pairs. Keys and values can be differing types. An optional trailing comma is allowed. Examples:

// Empty map
{}

// Map with a single value on one line
{ "key": "value" }

// Map with multiple values with differing types on multiple lines
{
    "key": "value",
    42: true,
}

Maps are unordered. When looping over a map, the key/value pairs can be returned in any order.

The set operators can be used to test for key inclusion in a map.

Accessing Elements

Map elements can be accessed with the syntax name[key]. This looks up a value by a key.

Accessing a key that doesn’t exist results in undefined.

Examples:

map = { "key": "value", 42: true, }

map["key"] // "value"
map[42]    // true
map[0]     // undefined

Modifying or Adding Elements

Elements can be added or modified in a map by assigning to name[key]. If the key doesn’t exist, the value is added. If the key already exists, the value is overridden.

map = { "key": "value" }

map[42] = true   // Add a new key/value
map["key"] = 12  // Modify the value of "key"

Deleting Elements

An element can be deleted from a map using the delete function.

Examples:

map = { "key": "value" }
delete(map, "key")    // map is now empty
delete(map, "other")  // no effect for non-existent key

Keys and Values

The keys and values of a map can be retrieved as lists using the keys and values functions.

Because maps are unordered, the keys and values are returned in an unspecified order. It should not be assumed that keys and values will be returned in a consistent order.

data = { "a": 2, "b": 3 }
keys(data)       // ["b", "a"]
values(data)     // [2, 3]

Map Comparison

Maps may be compared for equality. Maps are equal if they are of equal length and both their corresponding keys and values are comparable and equal.

{"foo": "bar"} is {"foo": "bar"}               // true
{"foo": "bar"} is {"baz": "bar"}               // false
{"foo": "bar"} is {"foo": "baz"}               // false
{"foo": "bar"} is {"foo": "bar", "baz": "qux"} // false
{1: "a"} is {1.0: "a"}                         // true (int/float comparable)

// also true (maps are not ordered):
{"m": {"a": "b"}, "l": ["a"]} is {"l": ["a"], "m": {"a": " b"}}

Happy Learning !!!


Share At:
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
Back To Top

Contact Us