Themis represents a set of tools for managing and enforcing security policies along with framework to create such tools:
- pdp - Policy Decision Point (core component of Themis);
- pdpserver - standalone application server which runs PDP;
- proto, pdp-service, pdp-control - gRPC protocol definitions and implementations;
- pep - golang client package for "service" protocol (Policy Enforcement Point or PEP);
- pepcli - CLI application which implements simple PEP and performance measurement tool for PDP server;
- pdpctr-client - golang client package for "control" protocol (Policy Administration Point or PAP);
- papcli - CLI application which implements simple PAP;
- egen - error processing code generator (development tool).
Themis design is inspired by eXtensible Access Control Markup Language (XACML) [XACML-V3.0].
Policy Decision Point or PDP (according to [XACML-V3.0]) is an entity that evaluates applicable policy and renders an authorization decision. Themis provides PDP as a golang package.
To make a decision PDP evaluates policies it has on request context. A request context represents set of attributes together with local content (additional data which can be used by policies during the evaluation). Resulting decision consists of effect, status (reason) and set of obligations. Decision effect can be:
- Deny - request is denied;
- Permit - request is permitted;
- Not Applicable - no policy applicable to the request;
- Indeterminate - PDP can't evaluate particular effect;
- IndeterminateD - PDP can't evaluate effect but if it could it would be Deny;
- IndeterminateP - PDP can't evaluate effect but if it could it would be Permit;
- IndeterminateDP - PDP can't evaluate effect but if it could it would be only Deny or Permit; In case of any Indeterminate effect status contains textual representation of an issue.
Some application may require more details for particular decision. For example if application can write log it may need a flag attached to decision which says when to do it. These details can be delivered as obligations. The obligations are set of attributes like in request context. Each attribute has name, type and value. Attribute name is arbitrary string (request context requires pair of attribute name and type to be unique). Following types are defined:
- boolean;
- string;
- address - IPv4 or IPv6 address;
- network - IPv4 or IPv6 network address;
- domain - domain name;
- set of strings - ordered set of strings;
- set of domains - set of domains (unordered);
- set of networks - set of IPv4 or IPv6 network addresses (unordered);
- list of strings.
Boolean value is accepted as "1", "t", "T", "TRUE", "true", "True", "0", "f", "F", "FALSE", "false", "False" and serialized to "true" and "false". Address accepted in dotted decimal ("192.0.2.1") form or in IPv6 ("2001:db8::68") form and serialized respectively. Network is accepted as a CIDR notation IP address and prefix (for example "192.0.2.0/24" or "2001:db8::/32"). Domain name is accepted as string of labels separated by dots (string is converted from punycode to ASCII and validated with regualr expression "^[-._A-Za-z0-9]+$" (TODO: need to rework according to RFC1035, 2181 and 4343). Set of strings, set of domains, set of networks and list of strings aren't accepted in request context but can appear in responce's obligations as comma separated list of values.
PDP uses YAML based language (YAML Abstract Syntax Tree or YAST) or JSON based language (JSON Abstract Syntax Tree or JAST) to define policies and specifically constructed JSON to define local content (JSON Content or JCON). YAST can be converted to JAST (and vise versa) with any YAML to JSON convertor.
Any policies definition consists of attributes (optional) and policies (required) sections. Attributes section contains set of pairs attribute name and type. For example:
YAST
# Permit if x is "test" otherwise Not Applicable
attributes:
x: string
policies:
alg: FirstApplicableEffect
target:
- equal:
- attr: x
- val:
type: string
content: "test"
rules:
- effect: Permit
YAST
# All permit policy (without "attributes" section)
policies:
alg: FirstApplicableEffect
rules:
- effect: Permit
JAST
{
"attributes": {
"x": "string"
},
"policies": {
"alg": "FirstApplicableEffect",
"target": [
{
"equal": [
{
"attr": "x"
},
{
"val": {
"type": "string",
"content": "test"
}
}
]
}
],
"rules": [
{
"effect": "Permit"
}
]
}
}
JAST
{
"policies": {
"alg": "FirstApplicableEffect",
"rules": [
{
"effect": "Permit"
}
]
}
}
Policies section contains root policy or policy set. Policy holds rules under its "rules" field while policy set is able to contain both inner policies or policy sets under its "policies" field.
Policy Set holds set of policies or inner policy sets and defines how to combine them. It has following fields:
- id - policy id (optional, if not defined policy is hidden);
- target - target expression which defines if policy set is applicable to request (optional, if not defined policy set is applicable to any request);
- policies - set of inner policies and policy sets;
- alg - policy combining algorithm (any of FirstApplicableEffect, DenyOverrides and Mapper);
- obligations - set of obligations (optional).
Example of policy set with all its fields (it contains one hidden policy set and one hidden policy):
# Policy set with all its fields
...
id: "Test Policy Set"
target: # x == "test"
- equal:
- attr: x
- val:
type: string
content: "test"
alg: FirstApplicableEffect
policies:
- alg: FirstApplicableEffect
target:
- equal: # z == "example"
- attr: z
- val:
type: string
content: "example"
policies:
- alg: FirstApplicableEffect
rules:
- effect: Permit
- alg: FirstApplicableEffect
rules:
- effect: Deny
obligations:
- a: "192.0.2.1"
Policy stores set of rules and defines how to combine them. It has following fields:
- id - policy id (optional, if not defined policy is hidden);
- target - target expression which defines if policy is applicable to request (optional, if not defined policy is applicable to any request);
- rules - set of rules;
- alg - rule combining algorithm (the same as for policy set);
- obligations - set of obligations (optional).
Here is an example of policy with all fields defined (it contains one hidden rule):
# Policy with all its fields
...
id: "Test Policy"
target: # x == "test"
- equal:
- attr: x
- val:
type: string
content: "test"
alg: FirstApplicableEffect
rules:
- effect: Permit
obligations:
- a: "192.0.2.1"
Rule defines decision effect. Possible fields of a rule:
- id - rule id (optional, if not defined policy is hidden);
- target - target expression (optional);
- condition - any boolean expression which together with target defines if rule is applicable to the request (optional, if not defined rule is applicable when target matches);
- effect - Deny or Permit;
- obligations - set of obligations.
For example a rule with all fields:
# Rule with all its fields
...
id: "Test Rule"
target: # x == "test"
- equal:
- attr: x
- val:
type: string
content: "test"
condition: # not (c contains 192.0.2.1 or b)
not:
- or:
- contains:
- attr: c
- val:
type: address
content: "192.0.2.1"
- attr: b
effect: Permit
obligations:
- a: "192.0.2.1"
Any particular policy set or policy or rule is applicable only if request matches its target. Target is a list of any expressions. Any expression is a list of all expressions and all expression is a list of match expression. Match expression is a boolean expression of two arguments. One of arguments should be a request attribute and other should be a immediate value. Only two functions can represent match expression equal and contains. If list of match expressions for particular all expression contains single element all keyword can be dropped. Similarly if list of all expressions for particular any expression consists of one element any keyword can be dropped.
Request matches target when all any expressions match (if one or more of any expression doesn't match, target also doesn't match). Any expression matches request if one or more of its all expressions match the request (if all all expressions don't match, any expression doesn't match as well). And similarly to target all expression matches if all its inner expressions match as well. If during target evaluation error occurs the policy set, policy or rule effect becomes indeterminate (if rule effect is permit it is indeterminateP if deny - indeterminateD for policy and policy set kind of indeterminate depends on combining algorithm (see below).
Below an example of policy with different kinds of targets:
# Target examples
...
# ((x == test and c contains address(192.0.2.1)) or
# x == example) and
# (network(192.0.2.0/28) contains a or network(192.0.2.16/28) contains a)
target:
- any:
- all:
- equal:
- attr: x
- val:
type: string
content: "test"
- contains:
- attr: c
- val:
type: address
content: 192.0.2.1
- equal:
- attr: x
- val:
type: string
content: "example"
- any:
- contains:
- val:
type: network
content: 192.0.2.0/28
- attr: a
- contains:
- val:
type: network
content: 192.0.2.16/28
- attr: a
...
# (x == test or x == example) and (network(192.0.2.0/28) contains a or network(192.0.2.16/28) contains a)
target:
- any:
- equal:
- attr: x
- val:
type: string
content: "test"
- equal:
- attr: x
- val:
type: string
content: "example"
- any:
- contains:
- val:
type: network
content: 192.0.2.0/28
- attr: a
- contains:
- val:
type: network
content: 192.0.2.16/28
- attr: a
...
# x == test and network(192.0.2.0/24) contains a
target:
- equal:
- attr: x
- val:
type: string
content: "test"
- contains:
- val:
type: network
content: 192.0.2.0/24
- attr: a
...
# x == test
target:
- equal:
- attr: x
- val:
type: string
content: "test"
Condition is rule field which can be any boolean expression (for example see above "Rule with all its fields"). Following functions available to make such expression:
- equal - expects two string arguments (result is true if the arguments are equal)
- contains:
- string contains substring - expects two string arguments first is a string to search in and second is a substring to serach for;
- network constains address;
- set of strings contains string;
- set of networks contains address;
- set of domains contains doman;
- not - boolean not (expects boolean as its single argument);
- and, or - boolean and and or (expect booleans as its arguments (requires at least one).
In any expression attribute can be referred with attr keyword and immediate value with val keyword (see below). There is special selector expression which is described below.
Immediate value can be reffered with val keyword and has fields:
- type - value type (any of available types);
- content - value data. For obligations only data itself requires as type can be derived from attribute definition.
Example of policy with all possible values:
# All values example
...
# String
val:
type: string
content: test
...
obligations:
- s: example
...
# Address
val:
type: address
content: 192.0.2.1
...
obligations:
- a: 192.0.2.2
...
# Network
val:
type: network
content: 192.0.2.0/28
...
obligations:
- c: 192.0.2.16/28
...
# Domain
val:
type: domain
content: example.com
...
obligations:
- d: example.net
...
# Set of Strings
val:
type: set of strings
content:
- first
- second
...
obligations:
- ss:
- first
- second
...
# Set of Networks
val:
type: set of networks
content:
- 192.0.2.0/28
- 192.0.2.16/28
...
obligations:
- sn:
- 192.0.2.0/28
- 192.0.2.16/28
...
# Set of Domains
val:
type: set of domains
content:
- example.com
- example.net
...
obligations:
- sd:
- example.org
- example.edu
...
# List of Strings
val:
type: list of strings
content:
- first
- second
...
obligations:
- ls:
- first
- second
Selector expression is an expression to access additionally supplied data. Selector uses uri field to locate source of such data. Currently only "local" URI schema is supported which defines local selector.
Local selector uses local content data (see below) and has following fields:
- uri - URI of local content ("local:<content-id>/<content-item-id>);
- path - defines path to data in local content (optional, if not set selector extracts immediate value from content item). Path represents a list of expressions. It should match to content item keys (see below). Selector calculates path expressions one by one and extracts value from next mapping step of content item until reaches desired value;
- type - type of data in local content (any of available types).
Example of local selector:
# Selector example
...
selector:
uri: "local:content/domain-addresses"
path:
- val:
type: string
content: good
- attr: d
type: set of networks
Content for the example:
{
"id": "content",
"items": {
"domain-addresses": {
"keys": ["string", "domain"],
"type": "set of networks",
"data": {
"good": {
"example.com": ["192.0.2.16/28", "192.0.2.32/28"],
"test.com": ["192.0.2.48/28", "192.0.2.64/28"]
},
"bad": {
"example.com": ["2001:db8:1000::/40", "2001:db8:2000::/40"],
"test.com": ["2001:db8:3000::/40", "2001:db8:4000::/40"]
}
}
}
}
}
Local content is a set of content items (see example above). It's identified by id field which can be any string with no slash character (/
). Each content item also has id (key of "items" JSON object) and following fields:
- keys - list of types of nested maps (optional, if not present data should contain immediate value of type);
- type - any possible type;
- data - list of nested maps with keys of mentioned types or immediate value of given type.
Local content supports string map (key type "string"), domain map (key type "domain") and network map (key type "network" or "address"). Selector expectes string expression as path item for string map, domain - for domain map and address or network - for network map ("address" expression is allowed even if content key is "network" and vice verse).
Policy and rule combinig algorithms define how to use child policies or rules of given policy set or policy and how to combine their effects, statuses and obligations. Themis supports following algorithms:
- FirstApplicableEffect - evaluates child policies or rules one by one until meets any other than NotApplicable effect (see details below);
- DenyOverrides - evaluates child policies or rules one by one until meets Deny effect;
- Mapper - evaluates map expression and uses result to find child policy or rule to evaluate.
For any algorithm if effect of children evaluation is Deny or Permit policy or policy set adds its obligation to what it got from children.
The algorithm iterates child policies or rules and evaluates them one by one. User should not relay on any particular order (however for now it goes sequentionaly from first to the last). If any child evaluation effect is not NotApplicable the effect becomes overall policy or policy set result.
The algorithm as well evaluates child policies or rules one by one. User should not relay on any particular order (however for now it goes sequentionaly from first to the last). If any effect is Deny the effect becomes overall policy or policy set result and any other evaluation results are dropped. Other effects are combined as following:
Effects | Result |
---|---|
at least one IndeterminateDP or at least one IndeterminateD with at least one Permit or at least one IndeterminateP and any NotApplicable | IndeterminateDP |
at least one IndeterminateD and any NotApplicable | IndeterminateD |
at least one Permit and any IndeterminateP or NotApplicable | Permit |
at least one IndeterminateP and any NotApplicable | IndeterminateP |
only NotApplicable | NotApplicable |
In case of any Indeterminate result all statuses are combined together.
The algorithm is capable to select particular child policy or rule with no evaluation other children one by one. It has some parameters:
- id - always "mapper" for the algorithm;
- map - expression to get resulting policy or rule id (it can be string, set of strings or list of strings expression);
- default - policy or rule id to evaluate if result of map expression doesn't match any child id (optional, if absent mapper policy effect is Indeterminate);
- error - policy or rule id to evaluate if map expression can't be evaluated (optional, if absent mapper policy effect is Indeterminate);
- alg - nested algorithm to use if map expression result is set of strings or list of strings (required if map is set of strings or list of strings expression, otherwise ignored). Other mapper can be used here (but for it default and error fields are ignored).
Any hidden child policy or rule is ignored by mapper algorithm.
For example policies:
# Mapper example
...
alg:
id: Mapper
map:
attr: p
default: DenyPolicy
error: ErrorPolicy
...
alg:
id: Mapper
map:
selector:
uri: "local:content/domain-policies"
path:
- attr: d
type: list of strings
default: DenyRule
alg: FirstApplicableEffect
and content for them:
{
"id": "content",
"items": {
"domain-policies": {
"keys": ["domain"],
"type": "list of strings",
"data": {
"example.com": ["PermitCom", "DenyCom"],
"example.net": ["PermitNet", "DenyNet"]
}
}
}
}
PDP server allows to run and control PDP. Additionally the server provides endpoint for healthcheck and supports OpenZipkin tracing. Started with no options pdpservers gets no initial policies and content. Policies and content in the case should be provided by control interface. Option -p
provides initial policy for the server from given YAML file. Option -j
provides content (it can be specified several times). For example (-v 3
sets maximal log level):
$ pdpserver -v 3 -p policy.yaml -j mapper.json -j content.json
INFO[0000] Starting PDP server
INFO[0000] Loading policy policy=policy.yaml
INFO[0000] Parsing policy policy=policy.yaml
INFO[0000] Opening content content=mapper.json
INFO[0000] Parsing content content=mapper.json
INFO[0000] Opening content content=content.json
INFO[0000] Parsing content content=content.json
INFO[0000] Opening service port address="0.0.0.0:5555"
INFO[0000] Opening control port address="0.0.0.0:5554"
INFO[0000] Creating service protocol handler
INFO[0000] Creating control protocol handler
INFO[0000] Serving decision requests
INFO[0000] Serving control requests
Other pdpserver options:
-c
- listen for policies on given address:port (default "0.0.0.0:5554");-health
- health check endpoint;-l
- listen for decision requests on given address:port (default "0.0.0.0:5555");-pprof
- performance profiler endpoint (see go tool pprof);-t
- OpenZipkin tracing endpoint;-v
- log verbosity (0 - error, 1 - warn (default), 2 - info, 3 - debug).
To make decision requests there are 3 options. Create client from scratch which implements protocol defined by proto/service.proto
, use golang client package themis\pep
to implement client application (see contrib/coredns/policy
) and for debug use simple PEPCLI client. To use PEPCLI client create requests YAML file for example:
attributes:
s: string
a: address
requests:
- s: Local Test
a: 127.0.0.1
- s: Example
a: 192.0.2.1
start PDP server with some policy (use for example "All permit policy" above):
$ pdpserver -v 3 -p all-permit-policy.yaml
INFO[0000] Starting PDP server
INFO[0000] Loading policy policy=all-permit-policy.yaml
INFO[0000] Parsing policy policy=all-permit-policy.yaml
INFO[0000] Opening service port address="0.0.0.0:5555"
INFO[0000] Opening control port address="0.0.0.0:5554"
INFO[0000] Creating service protocol handler
INFO[0000] Creating control protocol handler
INFO[0000] Serving decision requests
INFO[0000] Serving control requests
in the other terminal run PEPCLI:
$ pepcli -i requests.yaml test
Got 2 requests. Sending...
- effect: PERMIT
reason: "Ok"
- effect: PERMIT
reason: "Ok"
PEPCLI sends two requests which are listed in the file ang gets all permitted as instructed by the policy. In PDP's terminal you can see respective logs:
...
INFO[0000] Serving decision requests
INFO[0000] Serving control requests
INFO[0089] Validating context
DEBU[0089] Request context context=attributes:
- a.(Address): 127.0.0.1
- s.(String): "Local Test"
INFO[0089] Returning response
DEBU[0089] Response effect=PERMIT obligation=no attributes reason=Ok
INFO[0089] Validating context
DEBU[0089] Request context context=attributes:
- s.(String): "Example"
- a.(Address): 192.0.2.1
INFO[0089] Returning response
DEBU[0089] Response effect=PERMIT obligation=no attributes reason=Ok
...
PDP Server accepts control requests to upload and update policies or content. Themis user can implement her own client from scratch using protocol definition from proto/control.proto
or using golang package themis/pdpctrl-client
. To make control requests for debug purpose Themis provides PAPCLI tool.
Update is a list of commands each contains three fields:
- op - add or delete;
- path - list of ids;
- entity - contains an entity to add as child to entity defined by path.
PDP supporst add and delete commands. In case of add path should point to parent item and entity should contain appropriate child. For example if it is policy update and path points to policy set entity can be policy or other policy set. If path points to policy then only rule can be accepted as entity.
For example in one terminal start PDP server with no policies:
$ pdpserver -v 3
INFO[0000] Starting PDP server
INFO[0000] Opening service port address="0.0.0.0:5555"
INFO[0000] Opening control port address="0.0.0.0:5554"
INFO[0000] Creating service protocol handler
INFO[0000] Creating control protocol handler
INFO[0000] Serving decision requests
INFO[0000] Serving control requests
Then upload policy with PAPCLI ("All permit policy"):
$ papcli -s 127.0.0.1:5554 -p all-permit-policy.yaml
INFO[0000] Requesting data upload to PDP servers...
INFO[0000] Uploading data to PDP servers...
PDP got the data:
...
INFO[0000] Serving decision requests
INFO[0000] Serving control requests
INFO[0004] Got new control request
INFO[0004] Got new data stream
INFO[0004] Got apply command
INFO[0004] New policy has been applied id=1
...
PDP Server doesn't accept updates to policy with no tag so upload other policy and set tag to it to make update later (here policy similar to "permit if x is test" is used but with ids (because commands add and delete doen't see hidden policies or rules):
# Permit if x is "test" otherwise Not Applicable
attributes:
x: string
policies:
id: Root
alg: FirstApplicableEffect
target:
- equal:
- attr: x
- val:
type: string
content: "test"
rules:
- id: First Rule
effect: Permit
Run PAPCLI with the policy and initial tag (option -vt
the tag should be correct UUID):
$ papcli -s 127.0.0.1:5554 -p permit-test-x-policy.yaml -vt 823f79f2-0001-4eb2-9ba0-2a8c1b284443
INFO[0000] Requesting data upload to PDP servers...
INFO[0000] Uploading data to PDP servers...
PDP Server accepts the policy:
...
INFO[0016] Got new control request
INFO[0016] Got new data stream
INFO[0016] Got apply command
INFO[0016] New policy has been applied id=1 tag=823f79f2-0001-4eb2-9ba0-2a8c1b284443
...
Then policy can be updated (with following update which removes "First Rule" and adds other one):
- op: add
path:
- Root
entity:
id: Permit Rule With Obligation
effect: Permit
obligations:
- x: example
- op: delete
path:
- Root
- First Rule
Run PAPCLI with the update (you need to specify previous tag with option -vf
and new tag with option -vt
, when both options present PDP server considers data as update and checks if -vf
tag matches to tag current tag of updated to maintain update consistency):
$ papcli -s 127.0.0.1:5554 -p permit-test-x-policy-update.yaml -vf 823f79f2-0001-4eb2-9ba0-2a8c1b284443 -vt 93a17ce2-788d-476f-bd11-a5580a2f35f3
INFO[0000] Requesting data upload to PDP servers...
INFO[0000] Uploading data to PDP servers...
PDP accepts the update:
...
INFO[0373] Got new control request
INFO[0373] Got new data stream
DEBU[0373] Policy update update=policy update: 823f79f2-0001-4eb2-9ba0-2a8c1b284443 - 93a17ce2-788d-476f-bd11-a5580a2f35f3
commands:
- Add ("Root")
- Delete ("Root"/"First Rule")
INFO[0373] Got apply command
INFO[0373] Policy update has been applied curr-tag=93a17ce2-788d-476f-bd11-a5580a2f35f3 id=3 prev-tag=823f79f2-0001-4eb2-9ba0-2a8c1b284443
...
Consider content update. For example use "Selector example" policy. Start PDP with the policy:
$ pdpserver -v 3 -p selector-examle.yaml
INFO[0000] Starting PDP server
INFO[0000] Loading policy policy=selector-examle.yaml
INFO[0000] Parsing policy policy=selector-examle.yaml
INFO[0000] Opening service port address="0.0.0.0:5555"
INFO[0000] Opening control port address="0.0.0.0:5554"
INFO[0000] Creating service protocol handler
INFO[0000] Serving decision requests
INFO[0000] Creating control protocol handler
INFO[0000] Serving control requests
Then upload content with some tag (to be able to update it):
$ papcli -s 127.0.0.1:5554 -j selector-examle.json -vt 823f79f2-0001-4eb2-9ba0-2a8c1b284443
INFO[0000] Requesting data upload to PDP servers...
INFO[0000] Uploading data to PDP servers...
PDP server accepts upload:
...
INFO[0265] Got new control request
INFO[0265] Got new data stream
INFO[0265] Got apply command
INFO[0265] New content has been applied id=1 tag=823f79f2-0001-4eb2-9ba0-2a8c1b284443
...
Now lets move IPv4 addresses from "good" to "bad" map and IPv6 from "bad" to "good" for "example.com":
[
{
"op": "delete",
"path": ["domain-addresses", "good", "example.com"]
},
{
"op": "add",
"path": ["domain-addresses", "good", "example.com"],
"entity": {
"type": "set of networks",
"data": ["2001:db8:1000::/40", "2001:db8:2000::/40"]
}
},
{
"op": "delete",
"path": ["domain-addresses", "bad", "example.com"]
},
{
"op": "add",
"path": ["domain-addresses", "bad", "example.com"],
"entity": {
"type": "set of networks",
"data": ["192.0.2.16/28", "192.0.2.32/28"]
}
}
]
Note that update's entities doesn't contain keys field as data is immediate value (and has no any mappings). If update add some entity with mapping entity should have a keys filed. For example:
[
{
"op": "delete",
"path": ["domain-addresses", "good"]
},
{
"op": "add",
"path": ["domain-addresses", "good"],
"entity": {
"type": "set of networks",
"keys": ["domain"],
"data": {
"example.com": ["2001:db8:1000::/40", "2001:db8:2000::/40"],
"test.com": ["2001:db8:3000::/40", "2001:db8:4000::/40"]
}
}
}
]
Run PAPCLI with content update file:
$ papcli -s 127.0.0.1:5554 -id content -j selector-examle-update.json -vf 823f79f2-0001-4eb2-9ba0-2a8c1b284443 -vt 93a17ce2-788d-476f-bd11-a5580a2f35f3
INFO[0000] Requesting data upload to PDP servers...
INFO[0000] Uploading data to PDP servers...
Check PDP logs:
...
INFO[2190] Got new control request
INFO[2190] Got new data stream
DEBU[2190] Content update update=content update: 823f79f2-0001-4eb2-9ba0-2a8c1b284443 - 93a17ce2-788d-476f-bd11-a5580a2f35f3
content: "content"
commands:
- Delete ("domain-addresses"/"good"/"example.com")
- Add ("domain-addresses"/"good"/"example.com")
- Delete ("domain-addresses"/"bad"/"example.com")
- Add ("domain-addresses"/"bad"/"example.com")
INFO[2190] Got apply command
INFO[2190] Content update has been applied cid=content curr-tag=93a17ce2-788d-476f-bd11-a5580a2f35f3 id=5
...
Contents with different ids and policies can be updated independently and in paralel.
[XACML-V3.0] eXtensible Access Control Markup Language (XACML) Version 3.0. 22 January 2013. OASIS Standard. http://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html.