///|
/// High-level operation guarded by an access policy.
pub(all) enum AccessOperation {
ReadOperation
WriteOperation
DiagnosticsOperation
IdentificationOperation
} derive(Debug, Eq)
///|
/// Role used by a deployment policy.
pub(all) enum AccessRole {
Observer
Operator
Engineer
Administrator
} derive(Debug, Eq)
///|
pub fn access_operation_name(value : AccessOperation) -> String {
match value {
ReadOperation => "read"
WriteOperation => "write"
DiagnosticsOperation => "diagnostics"
IdentificationOperation => "identification"
}
}
///|
pub fn access_role_rank(value : AccessRole) -> Int {
match value {
Observer => 0
Operator => 1
Engineer => 2
Administrator => 3
}
}
///|
/// A rule matching unit ids and function codes.
pub(all) struct AccessRule {
name : String
role : AccessRole
operation : AccessOperation
unit_start : Byte
unit_end : Byte
functions : Array[Byte]
mut enabled : Bool
}
///|
pub fn AccessRule::new(
name : String,
role : AccessRole,
operation : AccessOperation,
unit_start : Byte,
unit_end : Byte,
functions : Array[Byte],
) -> Result[AccessRule, ModbusError] {
if name == "" ||
unit_start > unit_end ||
!is_valid_unit_id(unit_start, broadcast=false) ||
!is_valid_unit_id(unit_end, broadcast=false) ||
functions.length() == 0 {
return Err(InvalidData)
}
for function in functions {
if !supported_function(function) {
return Err(Unsupported)
}
}
Ok({
name,
role,
operation,
unit_start,
unit_end,
functions: copy_bytes(functions),
enabled: true,
})
}
///|
pub fn AccessRule::name(self : AccessRule) -> String {
self.name
}
///|
pub fn AccessRule::role(self : AccessRule) -> AccessRole {
self.role
}
///|
pub fn AccessRule::operation(self : AccessRule) -> AccessOperation {
self.operation
}
///|
pub fn AccessRule::enabled(self : AccessRule) -> Bool {
self.enabled
}
///|
pub fn AccessRule::set_enabled(self : AccessRule, value : Bool) -> Unit {
self.enabled = value
}
///|
pub fn AccessRule::matches(
self : AccessRule,
role : AccessRole,
operation : AccessOperation,
frame : Frame,
) -> Bool {
if !self.enabled ||
access_role_rank(role) < access_role_rank(self.role) ||
operation != self.operation {
return false
}
if frame.unit_id < self.unit_start || frame.unit_id > self.unit_end {
return false
}
let ordinary = ordinary_function(frame.pdu.function)
for function in self.functions {
if function == ordinary {
return true
}
}
false
}
///|
pub fn AccessRule::summary(self : AccessRule) -> String {
self.name +
" role=" +
access_role_rank(self.role).to_string() +
" operation=" +
access_operation_name(self.operation)
}
///|
/// A first-match access policy with an explicit default decision.
pub(all) struct AccessPolicy {
rules : Array[AccessRule]
max_rules : Int
mut default_allow : Bool
}
///|
pub fn AccessPolicy::new(
default_allow? : Bool = false,
max_rules? : Int = 256,
) -> Result[AccessPolicy, ModbusError] {
if max_rules < 1 {
Err(CapacityExceeded)
} else {
Ok({ rules: [], max_rules, default_allow })
}
}
///|
pub fn AccessPolicy::add(
self : AccessPolicy,
rule : AccessRule,
) -> Result[Unit, ModbusError] {
if self.rules.length() >= self.max_rules {
Err(CapacityExceeded)
} else {
self.rules.push(rule)
Ok(())
}
}
///|
pub fn AccessPolicy::remove(
self : AccessPolicy,
name : String,
) -> Result[AccessRule, ModbusError] {
for index, rule in self.rules {
if rule.name() == name {
ignore(self.rules.remove(index))
return Ok(rule)
}
}
Err(InvalidData)
}
///|
pub fn AccessPolicy::length(self : AccessPolicy) -> Int {
self.rules.length()
}
///|
pub fn AccessPolicy::allows(
self : AccessPolicy,
role : AccessRole,
operation : AccessOperation,
frame : Frame,
) -> Bool {
for rule in self.rules {
if rule.matches(role, operation, frame) {
return true
}
}
self.default_allow
}
///|
pub fn AccessPolicy::decision(
self : AccessPolicy,
role : AccessRole,
operation : AccessOperation,
frame : Frame,
) -> Result[Unit, ModbusError] {
if self.allows(role, operation, frame) {
Ok(())
} else {
Err(Unsupported)
}
}
///|
pub fn AccessPolicy::rules(self : AccessPolicy) -> Array[AccessRule] {
let result : Array[AccessRule] = []
for rule in self.rules {
result.push(rule)
}
result
}
///|
pub fn AccessPolicy::set_default(self : AccessPolicy, allow : Bool) -> Unit {
self.default_allow = allow
}
///|
pub fn AccessPolicy::summary(self : AccessPolicy) -> String {
"rules=" +
self.rules.length().to_string() +
" default=" +
(if self.default_allow { "allow" } else { "deny" })
}
///|
/// A fixed-size request window for rate limiting a transport or unit.
pub(all) struct RateWindow {
width : Int
limit : Int
timestamps : Array[Int]
}
///|
pub fn RateWindow::new(
width : Int,
limit : Int,
) -> Result[RateWindow, ModbusError] {
if width < 1 || limit < 1 {
Err(InvalidData)
} else {
Ok({ width, limit, timestamps: [] })
}
}
///|
pub fn RateWindow::width(self : RateWindow) -> Int {
self.width
}
///|
pub fn RateWindow::limit(self : RateWindow) -> Int {
self.limit
}
///|
pub fn RateWindow::prune(self : RateWindow, now : Int) -> Unit {
while self.timestamps.length() > 0 && now - self.timestamps[0] >= self.width {
ignore(self.timestamps.remove(0))
}
}
///|
pub fn RateWindow::allow(self : RateWindow, now : Int) -> Bool {
if now < 0 {
return false
}
self.prune(now)
if self.timestamps.length() >= self.limit {
false
} else {
self.timestamps.push(now)
true
}
}
///|
pub fn RateWindow::remaining(self : RateWindow, now : Int) -> Int {
self.prune(now)
self.limit - self.timestamps.length()
}
///|
pub fn RateWindow::reset(self : RateWindow) -> Unit {
self.timestamps.clear()
}
///|
pub fn RateWindow::count(self : RateWindow, now : Int) -> Int {
self.prune(now)
self.timestamps.length()
}
///|
/// A token bucket with integer tokens, suitable for frame-level quotas.
pub(all) struct TokenBucket {
capacity : Int
refill_interval : Int
mut tokens : Int
mut last_refill : Int
}
///|
pub fn TokenBucket::new(
capacity : Int,
refill_interval : Int,
now : Int,
) -> Result[TokenBucket, ModbusError] {
if capacity < 1 || refill_interval < 1 || now < 0 {
Err(InvalidData)
} else {
Ok({ capacity, refill_interval, tokens: capacity, last_refill: now })
}
}
///|
pub fn TokenBucket::capacity(self : TokenBucket) -> Int {
self.capacity
}
///|
pub fn TokenBucket::tokens(self : TokenBucket, now : Int) -> Int {
self.refill(now)
self.tokens
}
///|
pub fn TokenBucket::refill(self : TokenBucket, now : Int) -> Unit {
if now <= self.last_refill {
return
}
let added = (now - self.last_refill) / self.refill_interval
if added > 0 {
self.tokens = if self.tokens + added > self.capacity {
self.capacity
} else {
self.tokens + added
}
self.last_refill += added * self.refill_interval
}
}
///|
pub fn TokenBucket::take(self : TokenBucket, amount : Int, now : Int) -> Bool {
if amount < 1 {
return false
}
self.refill(now)
if self.tokens < amount {
false
} else {
self.tokens -= amount
true
}
}
///|
pub fn TokenBucket::reset(
self : TokenBucket,
now : Int,
) -> Result[Unit, ModbusError] {
if now < 0 {
Err(InvalidData)
} else {
self.tokens = self.capacity
self.last_refill = now
Ok(())
}
}
///|
/// Security audit record generated by a policy decision.
pub(all) struct SecurityAudit {
sequence : Int
timestamp : Int
principal : String
role : AccessRole
operation : AccessOperation
unit_id : Byte
function : Byte
allowed : Bool
reason : String
}
///|
pub fn SecurityAudit::new(
sequence : Int,
timestamp : Int,
principal : String,
role : AccessRole,
operation : AccessOperation,
frame : Frame,
allowed : Bool,
reason : String,
) -> Result[SecurityAudit, ModbusError] {
if sequence < 1 || timestamp < 0 || principal == "" || reason == "" {
Err(InvalidData)
} else {
Ok({
sequence,
timestamp,
principal,
role,
operation,
unit_id: frame.unit_id,
function: frame.pdu.function,
allowed,
reason,
})
}
}
///|
pub fn SecurityAudit::allowed(self : SecurityAudit) -> Bool {
self.allowed
}
///|
pub fn SecurityAudit::sequence(self : SecurityAudit) -> Int {
self.sequence
}
///|
pub fn SecurityAudit::summary(self : SecurityAudit) -> String {
self.principal +
" " +
access_operation_name(self.operation) +
" unit=" +
self.unit_id.to_string() +
" function=" +
self.function.to_string() +
" allowed=" +
(if self.allowed { "true" } else { "false" })
}
///|
pub fn evaluate_access(
policy : AccessPolicy,
role : AccessRole,
operation : AccessOperation,
principal : String,
frame : Frame,
timestamp : Int,
sequence : Int,
) -> Result[SecurityAudit, ModbusError] {
let allowed = policy.allows(role, operation, frame)
SecurityAudit::new(
sequence,
timestamp,
principal,
role,
operation,
frame,
allowed,
if allowed {
"policy-allow"
} else {
"policy-deny"
},
)
}