wiki:AclSyntax

Design goals

  • We need to put them into our configuration conveniently. Our configuration works on top of JSON. This means the preferred syntax would be based on JSON as well, because it can be put there as it is (without quoting in strings, etc) and would look consistent with the rest. It also means it can be easily represented in anything that has dicts and lists (python, perl, javascript, …).
  • They should be flexible. If we find out sometime in future we need to extend them, it shouldn't need a huge hack to do it, anything new should just nicely fit in.
  • It should be possible to load and „compile“ them into something that can be just executed/called to get yes/no fast (eg. not reading the configuration each time, ACLs will be called often).
  • It should be user friendly, readable and intuitive (bad example could be BIND9's acl "not-these-ips" { !192.168.0.0/24;!10.0.0.0/16; }, which accepts nothing at all, as said by one source, or would accept anything out of the 192.168.0.0/24 range, even 10.1.1.1 by other explanations of the rules how they apply).
  • It should be expressive, so users specify what they need even in non-trivial cases.
  • We need to allow adding ACLs as plugins (either C++, python from file and python snippet directly from configuration string).

Simplest form

In the simplest form, ACL is a dict containing single name-value pair. The name sets the property (or some kind of ACL, we would have a „plugin“ kind of ACL probably) being checked by this ACL (for example the IP address of client, TSIG key, message size, bank account number, …). The value is, well, the expected value (or any other parameter of the ACL). So, this is how an ACL could look like (it would be stored somewhere in the configuration, with a name, but that's out of the scope of this page):

{
	"max-msg-size": 128
}

Composition of ACLs

Now, to make anything more complicated, we allow to compose more ACLs together. We do it simply by creating three kinds of ACLs, which is AND, OR and NOT (more composition operators could be added in future, of course).

The NOT would take single sub-ACL as it's value. This ACL accepts if the sub-ACL does not accept and vice versa.

The AND and OR would take list of sub-ACLs. The AND ACL accepts if all its sub-ACLs accept. The OR accepts if at last one accepts. They reject otherwise.

Each sub-ACL could be either input directly, as the dict describing it, or by it's name from configuration (note that we can easily distinguish between these, as we know the type of the element ‒ dict is ACL, string is name). So, assuming the example from previous section would be called small, this could be a valid ACL we could call large:

{
	"NOT": "small"
}

As well as this:

{
	"NOT": { "max-msg-size": 128 }
}

And this would reject queries from evil address:

{
	"NOT": {"ip": "666.666.666.666"}
}

This could be ACL accepting local addresses:

{
	"OR": [
		{ "ip": "192.168.0.0/16" },
		{ "ip": "10.0.0.0/8" },
		{ "ip": "172.16.0.0/12" }
	]
}

And this might be our peer for zone transfers (note that we use TSIG key directly here, it is expected that they could be referred by a name as well, but accepted values of any of the properties is not the interesting stuff here):

{
	"AND": [
		{ "ip": "1.2.3.4" },
		{ "tsig": "secret:c2VjcmV0Cg==" }
	]
}

For convenience, we introduce two special preset ACLs, called ACCEPT and REJECT. It is obvious what they do, just note that they could be implemented like this:

  • ACCEPT: {"AND": []}
  • REJECT: {"OR": []}

This is correct by the definition, but the use of the presets is more convenient and readable.

We allow this nesting to go deep to any level (well, see implementation note). This allows for creation of arbitrary complex expressions, combined with the fact the syntax allows to test any properties we're bold enough to name, it should be strong enough.

Abbreviated form

The above examples have one major disadvantage. They are wordy. So, we introduce two methods of abbreviation:

Multiple properties within the same dict

We said that an ACL contains single name-value pair. We now lower the restriction and say there's at last one pair. If there are more, all of them must match. So, this would be equivalent of the zone transfer example:

{
	"ip": "1.2.3.4",
	"tsig": "secret:c2VjcmV0Cg=="
}

This still looks intuitive and users usually want to match the query against multiple properties and they must satisfy them all.

Multiple values of a property

If a property does not accept a list itself (AND accepts a list, while tsig doesn't), a list would mean the value of property could be any of the listed values. So, multiple things in { } are kind of AND, multiple values in [ ] would be kind of OR.

So, this would be equivalent of the local IP address ACL:

{
	"ip": ["192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12"]
}

This is again based on expected usage, as users will want to provide possibilities of allowed (or disallowed) values.

Of course, these two abbreviated forms can be combined, so one can write an ACL accepting all zone transfer peers, each having an IP address and TSIG key in a short way:

{
	"ip": ["1.2.3.4", "5.6.7.8"],
	"tsig": ["secret:c2VjcmV0Cg==", "secret2:c2VjcmV0Mgo="]
}

The logic composition is still needed, these abbreviated forms are weaker ‒ for example if there were two zone transfer peers, one having a TSIG (so we authenticate it by that), other having some stone-age software without the ability (so we authenticate it by IP address only), we need explicit OR.

First-match approach

It was pointed out that the first-match approach can be more convenient in some situations, for example a donut hole rules. Also, many people would feel sentimental for the good old times. So we add the ability to use BIND9-like first-match rule chains. Such chain could look like this:

{
  "FIRST": [
    {"ACCEPT-IF": {"ip": "132.147.67.16"}},
    {"DENY-IF": {"ip": "132.147.67.0/24"}},
    {"ACCEPT-IP": {"ip": "132.147.0./16"}},
    "DENY"
  ]
}

The sub-rules are checked from top to bottom. If the sub-ACL matches, a result for the whole ACL is set (the actual value of result depends on if it is "ACCEPT-IF" or "DENY-IF") and no more sub-rules are tested. There may be unconditional "ACCEPT" or "DENY" (note that it makes no sense to put any rules below these, as they always terminate the first-match chain).

But note, that you can use any ACL (including composition forms, other "FIRST" ACLs, etc) in the sub-ACLs and that the matching of these rules influence only the result of this single ACL element (eg. this might be nested inside logical expressions), so this is valid:

{
  "FIRST": [
    {"ACCEPT-IF":
      {"OR": [
        {"ip": "132.147.67.16"},
        {"max-msg-size": 128}
      ]}
    },
    {"DENY-IF": {"ip": "132.147.67.0/24"}},
    {"ACCEPT-IP": {"ip": "132.147.0./16"}},
    "DENY"
  ],
  "tsig": ["secret:c2VjcmV0Cg==", "secret2:c2VjcmV0Mgo="]
}

This could be considered a composed ACL as well, but because it has a different nature than the rest, it is showed separately.

Notes

Many existing ACL systems are procedural-based (eg. iptables ‒ each rule can either accept, reject or let it pass on), where the order does matter. This syntax does not care about order and is not procedural on purpose ‒ it seems this is simpler to read than following a complicated flow of rules and sub-rules. This way user describes how an accepted message must look like, not how to filter it (the exception being the first-match rule).

Each simple ACL would be represented as object with virtual function to check if the ACL accepts some message. This allows to easily load the ACLs into memory and handle them on abstract level. It also allows easy creation of the logic composition (each AND, OR or NOT would hold pointers to it's sub-ACLs and run them when needed). We might need to do some optimisations on them later, if this approach turns to be slow.

To support plugins, each one would provide a factory function and a name of the property (and if it wants to participate in the auto-abbreviation with lists of values).

The abbreviations are only in syntax, eg. the abbreviated form would create the same objects as non-abreviated (but it could provide further hints for optimisations).

We could provide aliases for property names (eg. address could be the same as ip).

Disadvantages

As always, there are few catches (if you see more, point them out, please):

  • There's need to check for cycles. Not only it would be problematic with shared pointers, running an ACL with cycle would create an infinite recursion. This can be done by either limiting the recursion depth or by some well-known graph algorithm, like DFS at load time.
  • This syntax would request some kind of more free-form configuration, which is not possible with current spec files. For example the arbitrary depth of expression or the fact that we can have virtually any name of item and it's value can be anything (depending on the name/propety, but unknown at the time of writing the spec file) would request for allowing anything and checking it somewhere that the structure can actually be loaded.
  • We will want to implement the ACLs in C++, but we'll need to load them from python as well. That means we'll need to pass the configuration between the languages, which is not directly possible now. The simplest way to do it is probably converting the python dicts, etc, back to JSON and pass it to C++ as string, parse there again (this is not a performance problem, this would happen only at reconfiguration). Also, we'll want to be able to have python ACL plugins (either inline, the program of it directly in the configuration or call to some external file/function for longer ones). We'll have to do the conversion the other way there probably.
Last modified 6 years ago Last modified on Jun 1, 2011, 8:41:13 AM