Access Control
Table of contents
In this example, we will create a simple access control policy based on following demands:
- if user role is “user”, then allow access only on working hours (9:00 - 17:00) at working days (Monday - Friday)
- if user role is “admin”, then allow access on any day and time
- after successfull Policy execution, provide following message:
- “Access has been granted for {username}” if user has been granted access
- “Access has been denied for {username}” if user has been denied access
To resolve such demand, we need to extract user role from the context and check if current time is within working hours. Working hour check should be done only if user role is “user”. Depending on policy result resolution we shall build message and save it to data store.
Example code can be found in the repository.
Policy Catalog
In order to achieve this, we will create a PolicyCatalog with following entities:
Policy Variables
We need 4 PolicyVariables:
- role - to find out user role
- currentTime - to find out current time
- dayOfWeek - to find out current day of the week
- username - to find out user username in order to build a message
role
To find a user role, we need a dynamic variable that will extract role from the context. Security related properties are usually defined in the subject store, so we shall create a variable that will extract role from the subject store and related PolicyVariable resolver. Resolver could have been created as an embedded entity, but for the sake of an example, we will create it as a managed entity.
role PolicyVariable
{
"id": "role",
"description": "Provided role",
"resolvers": [
{
"id": "roleResolver",
"refType": "PolicyVariableResolverRef"
}
],
"type": "string"
}
role PolicyVariableResolver
{
"id": "roleResolver",
"description": "Extracts role from subject store",
"source": "subject",
"key": "role"
}
currentTime
Current time is a variable saved in Environment store, under key localTime
. We will create a variable that will extract current time from the environment with embedded PolicyVariableResolver. Variable type in runtime will be LocalTime.
currentTime PolicyVariable
{
"id": "currentTime",
"description": "Current time",
"resolvers": [
{
"source": "environment",
"key": "localTime"
}
],
"type": "string",
"format": "time"
}
dayOfWeek
Current day in the week is a variable saved in Environment store, under key dayOfWeek
. We will create a variable that will extract day of week from the environment with embedded PolicyVariableResolver. Variable type in runtime will be Int. Possible values are 1-7.
currentTime PolicyVariable
{
"id": "dayOfWeek",
"description": "Current day of week",
"resolvers": [
{
"source": "environment",
"key": "dayOfWeek"
}
],
"type": "int"
}
username
Username as a variable is not defined by itself, but as a part of JQ definition to build a message
key in data store. It is created as an embedded variable inside PolicyAction definition. In order to test JQ command manually, you can use any JQ online tester and apply JQ commands below on the following JSON:
{
"role": "user",
"username": "user1"
}
This JSON is in shape of Subject store from the context.
accept message
{
"resolvers": [
{
"source": "subject",
"path": "\"Access has been granted for \" + .username",
"engine": "JQ"
}
],
"type": "string"
}
deny message
{
"resolvers": [
{
"source": "subject",
"path": "\"Access has been denied for \" + .username",
"engine": "JQ"
}
],
"type": "string"
}
Policy Conditions
In order to check if user is the admin, we need only one atomic condition that checks if user role is “admin”.
If we want to check if user is a regular user, we need to check if user role is “user”, if current day is a working day and if current time is within working hours.
We could also check if current day is public holiday, but for the sake of simplicity, we will not implement it. It could be done by setting public holiday dates in the environment store and then creating one more condition to check if currentDay exists in public holiday list.
isAdmin
This condition will compare static PolicyVariable of value “admin” with dynamic PolicyVariable defined in role PolicyVariable definition for equality. StringIgnoreCase is set to true to make comparison case-insensitive.
isAdmin PolicyCondition
{
"id": "isAdmin",
"description": "Checks if provided role is equal to 'admin'",
"operation": "Equals",
"args": [
{
"type": "string",
"value": "admin"
},
{
"id": "role",
"refType": "PolicyVariableRef"
}
],
"stringIgnoreCase": true
}
isUser
This condition will compare static PolicyVariable of value “user” with dynamic PolicyVariable defined in role PolicyVariable definition for equality. StringIgnoreCase is set to true to make comparison case-insensitive.
isUser PolicyCondition
{
"id": "isUser",
"description": "Checks if provided role is equal to 'user'",
"operation": "Equals",
"args": [
{
"type": "string",
"value": "user"
},
{
"id": "role",
"refType": "PolicyVariableRef"
}
],
"stringIgnoreCase": true
}
isWorkingDay
This condition will compare dynamic PolicyVariable defined in dayOfWeek PolicyVariable definition to check if it is lower or equal to 5 (which will be defined as a static PolicyVariable of type int). If dayOfWeek variable has a value of 1-5, it will be considered as a working day.
isWorkingDay PolicyCondition
{
"id": "isWorkingDay",
"description": "Checks if it is working day currently (Mon-Fri)",
"operation": "LessThanEqual",
"args": [
{
"id": "dayOfWeek",
"refType": "PolicyVariableRef"
},
{
"type": "int",
"value": 5
}
]
}
isWorkingHour
This composite condition will compare dynamic PolicyVariable defined in currentTime PolicyVariable definition to check if it is higher or equal to “09:00” (which will be defined as a static PolicyVariable of type string, format time and custom timeFormat value) and lower or equal to “17:00” (which will be defined as a static PolicyVariable of type string, format time and custom timeFormat value).
This composite PolicyCondition will resolve to true only if both embedded PolicyConditions resolve to true, as defined in conditionCombinationLogic value allOf
.
isWorkingHour PolicyCondition
{
"id": "isWorkingHour",
"description": "Checks if it is working hour currently (09:00-17:00)",
"conditionCombinationLogic": "allOf",
"conditions": [
{
"operation": "GreaterThanEqual",
"args": [
{
"id": "currentTime",
"refType": "PolicyVariableRef"
},
{
"type": "string",
"format": "time",
"timeFormat": "HH:mm",
"value": "09:00"
}
]
},
{
"operation": "LessThanEqual",
"args": [
{
"id": "currentTime",
"refType": "PolicyVariableRef"
},
{
"type": "string",
"format": "time",
"timeFormat": "HH:mm",
"value": "17:00"
}
]
}
]
}
regularUserAccess
This composite PolicyCondition will take upper conditions and combine them using conditionCombinationLogic value allOf
to check if user role is “user”, if current day is a working day and if current time is within working hours.
regularUserAccess PolicyCondition
{
"id": "regularUserAccess",
"description": "Checks if user has role 'user' and if it is a working day and working hour",
"conditionCombinationLogic": "allOf",
"conditions": [
{
"id": "isUser",
"refType": "PolicyConditionRef"
},
{
"id": "isWorkingDay",
"refType": "PolicyConditionRef"
},
{
"id": "isWorkingHour",
"refType": "PolicyConditionRef"
}
]
}
Policies
Three Policy definitions should be defined in the PolicyCatalog
userAccess
This Policy will check regularUserAccess
PolicyCondition. If it resolves to true, it will evaluate to permit
. If it resolves to false, it will evaluate to deny
, as strictTargetEffect flag is set to true. Note that this Policy doesn’t have any PolicyAction defined.
userAccess Policy
{
"id": "userAccess",
"description": "Allows access to regular user if it is working day and working hour",
"targetEffect": "permit",
"condition": {
"id": "regularUserAccess",
"refType": "PolicyConditionRef"
},
"strictTargetEffect": true
}
adminAccess
This Policy will check isAdmin
PolicyCondition. If it resolves to true, it will evaluate to permit
. If it resolves to false, it will evaluate to deny
, as strictTargetEffect flag is set to true. Note that this Policy doesn’t have any PolicyAction defined.
adminAccess Policy
{
"id": "adminAccess",
"description": "Allows access to admin user",
"targetEffect": "permit",
"condition": {
"id": "isAdmin",
"refType": "PolicyConditionRef"
},
"strictTargetEffect": true
}
checkAccess
This PolicySet will take upper Policies and combine them using policyCombinationLogic denyUnlessPermit
to check if user is either admin or if user is regular user and it is working day and working hour. It could also be possible that user role is “guest”, in which case it will resolve to deny by default.
“adminAccess” Policy will be evaluated first (as priority is set to 10), then “userAccess” Policy will be evaluated (with default priority of 0). This is optimization, to avoid unnecessary evaluation of “userAccess” Policy that has more conditions to check if “adminAccess” Policy resolves to permit.
PolicyActions are defined in this PolicySet, one to execute if Policy resolves to permit, and another to execute if Policy resolves to deny.
checkAccess Policy
{
"id": "checkAccess",
"description": "Checks if user has access",
"actions": [
{
"executionMode": [
"onDeny"
],
"action": {
"id": "setForbiddenMessage",
"refType": "PolicyActionRef"
}
},
{
"executionMode": [
"onPermit"
],
"action": {
"id": "setAllowedMessage",
"refType": "PolicyActionRef"
}
}
],
"policyCombinationLogic": "denyUnlessPermit",
"policies": [
{
"policy": {
"id": "userAccess",
"refType": "PolicyRef"
}
},
{
"priority": 10,
"policy": {
"id": "adminAccess",
"refType": "PolicyRef"
}
}
]
}
Policy Actions
Two PolicyActions are defined in the PolicyCatalog, one to set forbidden message and another to set allowed message.
setForbiddenMessage
This PolicyActionSave will set message
variable to "Access has been denied for {username}"
template by using JQ parser.
setForbiddenMessage PolicyAction
{
"id": "setForbiddenMessage",
"description": "Sets message for user for which access has been denied",
"key": "message",
"value": {
"resolvers": [
{
"source": "subject",
"path": "\"Access has been denied for \" + .username",
"engine": "JQ"
}
],
"type": "string"
},
"type": "save"
}
setAllowedMessage
This PolicyActionSave will set message
variable to "Access has been granted for {username}"
template by using JQ parser.
{
"id": "setAllowedMessage",
"description": "Sets message for user who has been granted access",
"key": "message",
"value": {
"resolvers": [
{
"source": "subject",
"path": "\"Access has been granted for \" + .username",
"engine": "JQ"
}
],
"type": "string"
},
"type": "save"
}
PolicyEngine execution
PolicyEngine will be instantiated with PolicyCatalog containing previously defined entities. In order to provide consistent tests, mock clock will be used to provide exact date and time. Subject store in Context will be populated with “role” and “username” fields as a Map. Event handler will be set to provide details of each event. Events presented below will be manually formatted for better readability.
All tests will have similar pattern for PolicyEngine execution.
val engine = PolicyEngine(catalogJson)
val instant = Instant.parse("2024-08-23T13:42:56+00:00")
val clock = Clock.fixed(instant, ZoneOffset.ofHours(0))
val options = Options(clock = clock)
val context =
Context(
subject = mapOf("role" to "user", "username" to "user1"),
options = options,
event = InMemoryEventHandler(EventLevelEnum.DETAILS))
val result = engine.evaluatePolicy("checkAccess", context = context)
Access granted for user
In this test current time will be set to working hour and user role will be set to “user”.
test("should allow user in working hours") {
val engine = PolicyEngine(catalogJson)
val instant = Instant.parse("2024-08-23T13:42:56+00:00")
val clock = Clock.fixed(instant, ZoneOffset.ofHours(0))
val options = Options(clock = clock)
val context =
Context(
subject = mapOf("role" to "user", "username" to "user1"),
options = options,
event = InMemoryEventHandler(EventLevelEnum.DETAILS))
val result = engine.evaluatePolicy("checkAccess", context = context)
context.id shouldNotBe null
result.first shouldBe PolicyResultEnum.PERMIT
context.dataStore().containsKey("message") shouldBe true
context.dataStore()["message"] shouldBe "Access has been granted for user1"
logger.info("result: $result")
logger.info("context events:\n{}", context.event.list())
logger.info("context cache:\n{}", context.cache)
logger.info("context data store:\n{}", context.dataStore())
}
After execution of PolicyEngine, result will be pair permit,true
, which means that checkAccess
Policy resolved to permit and all actions were executed successfully.
Context data store will contain one entry with key message
and value "Access has been granted for user1"
.
Context cache will contain following entries:
HashMapCache(
policyStore={
adminAccess=deny,
userAccess=permit,
checkAccess=permit},
valueStore={
role=VariableValue(type=STRING, body=user),
dayOfWeek=VariableValue(type=INT, body=5),
currentTime=VariableValue(type=TIME, body=13:42:56)
},
conditionStore={
isAdmin=false,
isUser=true,
isWorkingDay=true,
isWorkingHour=true,
regularUserAccess=true
},
keyValueAsJsonNode={},
keyValueAsString={SUBJECT::"Access has been granted for " + .username={"role":"user","username":"user1"}})
keyValueAsString
cache entry is optimization for JQ processing.
Context event will contain following entries:
entity=ENGINE_START, entityId=access-control:2024-02-17, message=null, success=true, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/0, message=VariableValue(type=STRING, body=admin), success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)/resolvers/0(roleResolver), message=user, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role), message=VariableValue(type=STRING, body=user), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin), message=false, success=true, fromCache=false
entity=POLICY, entityId=checkAccess/policies/1(adminAccess), message=deny, success=false, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/0, message=VariableValue(type=STRING, body=user), success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/1(role), message=VariableValue(type=STRING, body=user), success=true, fromCache=true
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser), message=true, success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek)/resolvers/0, message=5, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek), message=VariableValue(type=INT, body=5), success=true, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/1, message=VariableValue(type=INT, body=5), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay), message=true, success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime)/resolvers/0, message=13:42:56, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime), message=VariableValue(type=TIME, body=13:42:56), success=true, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/1, message=VariableValue(type=TIME, body=09:00), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0, message=true, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/0(currentTime), message=VariableValue(type=TIME, body=13:42:56), success=true, fromCache=true
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/1, message=VariableValue(type=TIME, body=17:00), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1, message=true, success=true, fromCache=false
entity=CONDITION_COMPOSITE, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour), message=true, success=true, fromCache=false
entity=CONDITION_COMPOSITE, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess), message=true, success=true, fromCache=false
entity=POLICY, entityId=checkAccess/policies/0(userAccess), message=permit, success=true, fromCache=false
entity=POLICY_SET, entityId=checkAccess, message=permit, success=false, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/actions/1(setAllowedMessage)/source/resolvers/0, message=Access has been granted for user1, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/actions/1(setAllowedMessage)/source, message=VariableValue(type=STRING, body=Access has been granted for user1), success=true, fromCache=false
entity=POLICY_ACTION_SAVE, entityId=checkAccess/actions/1(setAllowedMessage), message=Access has been granted for user1, success=true, fromCache=false
entity=POLICY_ACTION, entityId=checkAccess, message=true, success=true, fromCache=false
entity=ENGINE_END, entityId=access-control:2024-02-17, message=(permit, true), success=true, fromCache=false
In this event flow we can see following steps:
- PolicyEngine starts
- “admin” static PolicyVariable is defined on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/0
- “role” resolver is invoked to fetch value of “role” PolicyVariable on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)/resolvers/0(roleResolver)
- “role” PolicyVariable is defined on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)
- “isAdmin” PolicyCondition is checked and resolves to false on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)
- “adminAccess” Policy resolves to deny on path
checkAccess/policies/1(adminAccess)
- “user” static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/0
- “role” PolicyVariable on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/1(role)
is pulled from cache - “isUser” PolicyCondition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)
- “dayOfWeek” resolver is invoked on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek)/resolvers/0
- “dayOfWeek” PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek)
- 5 as a static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/1
- “isWorkingDay” PolicyCondition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)
- “currentTime” resolver is invoked on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime)/resolvers/0
- “currentTime” PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime)
- “09:00” static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/1
- “isWorkingHour” first sub-condition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0
- “currentTime” PolicyVariable is pulled from cache on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/0(currentTime)
- “17:00” static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/1
- “isWorkingHour” second sub-condition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1
- “isWorkingHour” PolicyCondition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)
- “regularUserAccess” PolicyCondition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)
- “userAccess” Policy resolves to permit on path
checkAccess/policies/0(userAccess)
- “checkAccess” PolicySet resolves to permit on path
checkAccess
- Allowed message PolicyVariableResolver is invoked on path
checkAccess/actions/1(setAllowedMessage)/source/resolvers/0
and contains value “Access has been granted for user1” - Dynamic PolicyVariable on path
checkAccess/actions/1(setAllowedMessage)/source
is set to “Access has been granted for user1” - “setAllowedMessage” PolicyActionSave is invoked on path
checkAccess/actions/1(setAllowedMessage)
- “checkAccess” PolicyActions are completed on path
checkAccess
- PolicyEngine stops
Access denied for user
In this test current time will be set outside of working hours and user role will be set to “user”.
test("should forbid user outside of working hours") {
val engine = PolicyEngine(catalogJson)
val instant = Instant.parse("2024-08-23T23:42:56+00:00")
val clock = Clock.fixed(instant, ZoneOffset.ofHours(0))
val options = Options(clock = clock)
val context =
Context(
subject = mapOf("role" to "user", "username" to "user1"),
options = options,
event = InMemoryEventHandler(EventLevelEnum.DETAILS))
val result = engine.evaluatePolicy("checkAccess", context = context)
context.id shouldNotBe null
result.first shouldBe PolicyResultEnum.DENY
context.dataStore().containsKey("message") shouldBe true
context.dataStore()["message"] shouldBe "Access has been denied for user1"
logger.info("result: $result")
logger.info("context events:\n{}", context.event.list())
logger.info("context cache:\n{}", context.cache)
logger.info("context data store:\n{}", context.dataStore())
}
After execution of PolicyEngine, result will be pair deny,true
, which means that checkAccess
Policy resolved to deny and all actions were executed successfully.
Context data store will contain one entry with key message
and value "Access has been denied for user1"
.
Context cache will contain following entries:
HashMapCache(
policyStore={
adminAccess=deny,
userAccess=deny,
checkAccess=deny},
valueStore={
role=VariableValue(type=STRING, body=user),
dayOfWeek=VariableValue(type=INT, body=5),
currentTime=VariableValue(type=TIME, body=23:42:56)
},
conditionStore={
isAdmin=false,
isUser=true,
isWorkingDay=true,
isWorkingHour=false,
regularUserAccess=false
},
keyValueAsJsonNode={},
keyValueAsString={SUBJECT::"Access has been denied for " + .username={"role":"user","username":"user1"}})
keyValueAsString
cache entry is optimization for JQ processing.
Context event will contain following entries:
entity=ENGINE_START, entityId=access-control:2024-02-17, message=null, success=true, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/0, message=VariableValue(type=STRING, body=admin), success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)/resolvers/0(roleResolver), message=user, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role), message=VariableValue(type=STRING, body=user), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin), message=false, success=true, fromCache=false
entity=POLICY, entityId=checkAccess/policies/1(adminAccess), message=deny, success=false, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/0, message=VariableValue(type=STRING, body=user), success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/1(role), message=VariableValue(type=STRING, body=user), success=true, fromCache=true
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser), message=true, success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek)/resolvers/0, message=5, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek), message=VariableValue(type=INT, body=5), success=true, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/1, message=VariableValue(type=INT, body=5), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay), message=true, success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime)/resolvers/0, message=23:42:56, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime), message=VariableValue(type=TIME, body=23:42:56), success=true, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/1, message=VariableValue(type=TIME, body=09:00), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0, message=true, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/0(currentTime), message=VariableValue(type=TIME, body=23:42:56), success=true, fromCache=true
entity=VARIABLE_STATIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/1, message=VariableValue(type=TIME, body=17:00), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1, message=false, success=true, fromCache=false
entity=CONDITION_COMPOSITE, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour), message=false, success=true, fromCache=false
entity=CONDITION_COMPOSITE, entityId=checkAccess/policies/0(userAccess)/condition(regularUserAccess), message=false, success=true, fromCache=false
entity=POLICY, entityId=checkAccess/policies/0(userAccess), message=deny, success=false, fromCache=false
entity=POLICY_SET, entityId=checkAccess, message=deny, success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/actions/0(setForbiddenMessage)/source/resolvers/0, message=Access has been denied for user1, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/actions/0(setForbiddenMessage)/source, message=VariableValue(type=STRING, body=Access has been denied for user1), success=true, fromCache=false
entity=POLICY_ACTION_SAVE, entityId=checkAccess/actions/0(setForbiddenMessage), message=Access has been denied for user1, success=true, fromCache=false
entity=POLICY_ACTION, entityId=checkAccess, message=true, success=true, fromCache=false
entity=ENGINE_END, entityId=access-control:2024-02-17, message=(deny, true), success=true, fromCache=false
In this event flow we can see following steps:
- PolicyEngine starts
- “admin” static PolicyVariable is defined on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/0
- “role” resolver is invoked to fetch value of “role” PolicyVariable on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)/resolvers/0(roleResolver)
- “role” PolicyVariable is defined on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)
- “isAdmin” PolicyCondition is checked and resolves to false on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)
- “adminAccess” Policy resolves to deny on path
checkAccess/policies/1(adminAccess)
- “user” static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/0
- “role” PolicyVariable on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)/args/1(role)
is pulled from cache - “isUser” PolicyCondition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/0(isUser)
- “dayOfWeek” resolver is invoked on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek)/resolvers/0
- “dayOfWeek” PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/0(dayOfWeek)
- 5 as a static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)/args/1
- “isWorkingDay” PolicyCondition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/1(isWorkingDay)
- “currentTime” resolver is invoked on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime)/resolvers/0
- “currentTime” PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/0(currentTime)
- “09:00” static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0/args/1
- “isWorkingHour” first sub-condition is checked and resolves to true on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/0
- “currentTime” PolicyVariable is pulled from cache on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/0(currentTime)
- “17:00” static PolicyVariable is defined on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1/args/1
- “isWorkingHour” second sub-condition is checked and resolves to false on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)/conditions/1
- “isWorkingHour” PolicyCondition is checked and resolves to false on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)/conditions/2(isWorkingHour)
- “regularUserAccess” PolicyCondition is checked and resolves to false on path
checkAccess/policies/0(userAccess)/condition(regularUserAccess)
- “userAccess” Policy resolves to deny on path
checkAccess/policies/0(userAccess)
- “checkAccess” PolicySet resolves to deny on path
checkAccess
- Allowed message PolicyVariableResolver is invoked on path
checkAccess/actions/0(setForbiddenMessage)/source/resolvers/0
and contains value “Access has been denied for user1” - Dynamic PolicyVariable on path
checkAccess/actions/0(setForbiddenMessage)/source
is set to “Access has been denied for user1” - “setForbiddenMessage” PolicyActionSave is invoked on path
checkAccess/actions/0(setForbiddenMessage)
- “checkAccess” PolicyActions are completed on path
checkAccess
- PolicyEngine stops
Access granted for admin
In this test current time will be outside of working hour and user role will be set to “admin”.
test("should allow admin outside of working hours") {
val engine = PolicyEngine(catalogJson)
val instant = Instant.parse("2024-08-23T23:42:56+00:00")
val clock = Clock.fixed(instant, ZoneOffset.ofHours(0))
val options = Options(clock = clock)
val context =
Context(
subject = mapOf("role" to "admin", "username" to "admin1"),
options = options,
event = InMemoryEventHandler(EventLevelEnum.DETAILS))
val result = engine.evaluatePolicy("checkAccess", context = context)
context.id shouldNotBe null
result.first shouldBe PolicyResultEnum.PERMIT
context.dataStore().containsKey("message") shouldBe true
context.dataStore()["message"] shouldBe "Access has been granted for admin1"
logger.info("result: $result")
logger.info("context events:\n{}", context.event.list())
logger.info("context cache:\n{}", context.cache)
logger.info("context data store:\n{}", context.dataStore())
}
After execution of PolicyEngine, result will be pair permit,true
, which means that checkAccess
Policy resolved to permit and all actions were executed successfully.
Context data store will contain one entry with key message
and value "Access has been granted for admin1"
.
Context cache will contain following entries:
HashMapCache(
policyStore={
adminAccess=permit,
checkAccess=permit},
valueStore={
role=VariableValue(type=STRING, body=admin)
},
conditionStore={
isAdmin=true
},
keyValueAsJsonNode={},
keyValueAsString={SUBJECT::"Access has been granted for " + .username={"role":"admin","username":"admin1"}})
keyValueAsString
cache entry is optimization for JQ processing.
Context event will contain following entries:
entity=ENGINE_START, entityId=access-control:2024-02-17, message=null, success=true, fromCache=false
entity=VARIABLE_STATIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/0, message=VariableValue(type=STRING, body=admin), success=true, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)/resolvers/0(roleResolver), message=admin, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role), message=VariableValue(type=STRING, body=admin), success=true, fromCache=false
entity=CONDITION_ATOMIC, entityId=checkAccess/policies/1(adminAccess)/condition(isAdmin), message=true, success=true, fromCache=false
entity=POLICY, entityId=checkAccess/policies/1(adminAccess), message=permit, success=true, fromCache=false
entity=POLICY_SET, entityId=checkAccess, message=permit, success=false, fromCache=false
entity=VALUE_RESOLVER, entityId=checkAccess/actions/1(setAllowedMessage)/source/resolvers/0, message=Access has been granted for admin1, success=true, fromCache=false
entity=VARIABLE_DYNAMIC, entityId=checkAccess/actions/1(setAllowedMessage)/source, message=VariableValue(type=STRING, body=Access has been granted for admin1), success=true, fromCache=false
entity=POLICY_ACTION_SAVE, entityId=checkAccess/actions/1(setAllowedMessage), message=Access has been granted for admin1, success=true, fromCache=false
entity=POLICY_ACTION, entityId=checkAccess, message=true, success=true, fromCache=false
entity=ENGINE_END, entityId=access-control:2024-02-17, message=(permit, true), success=true, fromCache=false
In this event flow we can see following steps:
- PolicyEngine starts
- “admin” static PolicyVariable is defined on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/0
- “role” resolver is invoked to fetch value of “role” PolicyVariable on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)/resolvers/0(roleResolver)
- “role” PolicyVariable is defined on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)/args/1(role)
- “isAdmin” PolicyCondition is checked and resolves to true on path
checkAccess/policies/1(adminAccess)/condition(isAdmin)
- “adminAccess” Policy resolves to permit on path
checkAccess/policies/1(adminAccess)
- “checkAccess” PolicySet resolves to permit on path
checkAccess
- Allowed message PolicyVariableResolver is invoked on path
checkAccess/actions/1(setAllowedMessage)/source/resolvers/0
and contains value “Access has been granted for admin1” - Dynamic PolicyVariable on path
checkAccess/actions/1(setAllowedMessage)/source
is set to “Access has been granted for admin1” - “setAllowedMessage” PolicyActionSave is invoked on path
checkAccess/actions/1(setAllowedMessage)
- “checkAccess” PolicyActions are completed on path
checkAccess
- PolicyEngine stops
We can see in these list of events that PolicyEngine works in optimized way and skips unnecessary calculations. In this case, it is calculation of “userAccess” Policy, as “adminAccess” Policy resolved to permit first (it had higher priority in “checkAccess” PolicySet).