///|
/// Roles recognized by the operational authorization layer.
pub enum OperatorRole {
ViewerRole
EngineerRole
DispatcherRole
AdministratorRole
ServiceRole
} derive(Eq, Debug)
///|
/// Fine-grained operations that can be authorized.
pub enum OperationalPermission {
ViewTelemetry
ViewDiagnostics
ReadFiles
WriteFiles
SubmitCommands
ConfirmCommands
ManageStations
ManageRoutes
ManagePolicies
PerformFailover
} derive(Eq, Debug)
///|
pub fn role_permissions(role : OperatorRole) -> Array[OperationalPermission] {
match role {
ViewerRole => [ViewTelemetry, ViewDiagnostics]
EngineerRole => [ViewTelemetry, ViewDiagnostics, ReadFiles, WriteFiles]
DispatcherRole =>
[ViewTelemetry, ViewDiagnostics, SubmitCommands, ConfirmCommands]
AdministratorRole =>
[
ViewTelemetry,
ViewDiagnostics,
ReadFiles,
WriteFiles,
SubmitCommands,
ConfirmCommands,
ManageStations,
ManageRoutes,
ManagePolicies,
PerformFailover,
]
ServiceRole => [ViewTelemetry, ViewDiagnostics, ReadFiles]
}
}
///|
pub struct OperatorIdentity {
id : String
display_name : String
role : OperatorRole
stations : Array[CommonAddress]
active : Bool
} derive(Eq, Debug)
///|
pub fn OperatorIdentity::new(
id : String,
display_name : String,
role : OperatorRole,
stations? : Array[CommonAddress] = [],
active? : Bool = true,
) -> Result[OperatorIdentity, String] {
if id.length() == 0 || id.length() > 128 {
Err("operator id must contain between 1 and 128 characters")
} else if display_name.length() == 0 {
Err("operator display name is empty")
} else {
Ok({ id, display_name, role, stations: stations.copy(), active })
}
}
///|
pub fn OperatorIdentity::id(self : OperatorIdentity) -> String {
self.id
}
///|
pub fn OperatorIdentity::display_name(self : OperatorIdentity) -> String {
self.display_name
}
///|
pub fn OperatorIdentity::role(self : OperatorIdentity) -> OperatorRole {
self.role
}
///|
pub fn OperatorIdentity::stations(
self : OperatorIdentity,
) -> Array[CommonAddress] {
self.stations.copy()
}
///|
pub fn OperatorIdentity::active(self : OperatorIdentity) -> Bool {
self.active
}
///|
pub fn OperatorIdentity::covers(
self : OperatorIdentity,
station : CommonAddress,
) -> Bool {
self.stations.length() == 0 || self.stations.contains(station)
}
///|
/// A bounded maintenance interval during which risky actions are blocked.
pub struct MaintenanceWindow {
id : Int
station : CommonAddress?
start : Int
end : Int
reason : String
blocks_commands : Bool
blocks_file_writes : Bool
mut enabled : Bool
} derive(Eq, Debug)
///|
pub fn MaintenanceWindow::new(
id : Int,
start : Int,
end : Int,
reason : String,
station? : CommonAddress,
blocks_commands? : Bool = true,
blocks_file_writes? : Bool = true,
) -> Result[MaintenanceWindow, String] {
if id < 1 || start < 0 || end <= start {
Err("maintenance window bounds are invalid")
} else if reason.length() == 0 {
Err("maintenance window reason is empty")
} else {
Ok({
id,
station,
start,
end,
reason,
blocks_commands,
blocks_file_writes,
enabled: true,
})
}
}
///|
pub fn MaintenanceWindow::id(self : MaintenanceWindow) -> Int {
self.id
}
///|
pub fn MaintenanceWindow::station(self : MaintenanceWindow) -> CommonAddress? {
self.station
}
///|
pub fn MaintenanceWindow::start(self : MaintenanceWindow) -> Int {
self.start
}
///|
pub fn MaintenanceWindow::end(self : MaintenanceWindow) -> Int {
self.end
}
///|
pub fn MaintenanceWindow::reason(self : MaintenanceWindow) -> String {
self.reason
}
///|
pub fn MaintenanceWindow::blocks_commands(self : MaintenanceWindow) -> Bool {
self.blocks_commands
}
///|
pub fn MaintenanceWindow::blocks_file_writes(self : MaintenanceWindow) -> Bool {
self.blocks_file_writes
}
///|
pub fn MaintenanceWindow::enabled(self : MaintenanceWindow) -> Bool {
self.enabled
}
///|
pub fn MaintenanceWindow::contains(
self : MaintenanceWindow,
station : CommonAddress,
timestamp : Int,
) -> Bool {
let station_ok = match self.station {
Some(value) => value == station
None => true
}
self.enabled && station_ok && timestamp >= self.start && timestamp < self.end
}
///|
pub fn MaintenanceWindow::disable(self : MaintenanceWindow) -> Unit {
self.enabled = false
}
///|
/// A station/type-specific policy rule.
pub struct PolicyRule {
id : Int
permission : OperationalPermission
station : CommonAddress?
type_ids : Array[ApplicationType]
roles : Array[OperatorRole]
mut enabled : Bool
reason : String
} derive(Eq, Debug)
///|
pub fn PolicyRule::new(
id : Int,
permission : OperationalPermission,
reason : String,
station? : CommonAddress,
type_ids? : Array[ApplicationType] = [],
roles? : Array[OperatorRole] = [],
) -> Result[PolicyRule, String] {
if id < 1 || reason.length() == 0 {
Err("policy rule id and reason are required")
} else {
Ok({
id,
permission,
station,
type_ids: type_ids.copy(),
roles: roles.copy(),
enabled: true,
reason,
})
}
}
///|
pub fn PolicyRule::id(self : PolicyRule) -> Int {
self.id
}
///|
pub fn PolicyRule::permission(self : PolicyRule) -> OperationalPermission {
self.permission
}
///|
pub fn PolicyRule::station(self : PolicyRule) -> CommonAddress? {
self.station
}
///|
pub fn PolicyRule::type_ids(self : PolicyRule) -> Array[ApplicationType] {
self.type_ids.copy()
}
///|
pub fn PolicyRule::roles(self : PolicyRule) -> Array[OperatorRole] {
self.roles.copy()
}
///|
pub fn PolicyRule::enabled(self : PolicyRule) -> Bool {
self.enabled
}
///|
pub fn PolicyRule::reason(self : PolicyRule) -> String {
self.reason
}
///|
pub fn PolicyRule::disable(self : PolicyRule) -> Unit {
self.enabled = false
}
///|
pub struct PolicyDecision {
allowed : Bool
permission : OperationalPermission
operator : String
station : CommonAddress
rule_id : Int?
reason : String
} derive(Eq, Debug)
///|
pub fn PolicyDecision::allowed(self : PolicyDecision) -> Bool {
self.allowed
}
///|
pub fn PolicyDecision::permission(
self : PolicyDecision,
) -> OperationalPermission {
self.permission
}
///|
pub fn PolicyDecision::operator(self : PolicyDecision) -> String {
self.operator
}
///|
pub fn PolicyDecision::station(self : PolicyDecision) -> CommonAddress {
self.station
}
///|
pub fn PolicyDecision::rule_id(self : PolicyDecision) -> Int? {
self.rule_id
}
///|
pub fn PolicyDecision::reason(self : PolicyDecision) -> String {
self.reason
}
///|
pub enum AuditAction {
AuthorizationChecked
PolicyChanged
MaintenanceChanged
CommandAuthorized
FileAuthorized
StationConfigured
FailoverAuthorized
} derive(Eq, Debug)
///|
pub struct PolicyAudit {
sequence : Int
timestamp : Int
action : AuditAction
operator : String
station : CommonAddress?
object : InformationAddress?
allowed : Bool
detail : String
} derive(Eq, Debug)
///|
pub fn PolicyAudit::sequence(self : PolicyAudit) -> Int {
self.sequence
}
///|
pub fn PolicyAudit::timestamp(self : PolicyAudit) -> Int {
self.timestamp
}
///|
pub fn PolicyAudit::action(self : PolicyAudit) -> AuditAction {
self.action
}
///|
pub fn PolicyAudit::operator(self : PolicyAudit) -> String {
self.operator
}
///|
pub fn PolicyAudit::station(self : PolicyAudit) -> CommonAddress? {
self.station
}
///|
pub fn PolicyAudit::object(self : PolicyAudit) -> InformationAddress? {
self.object
}
///|
pub fn PolicyAudit::allowed(self : PolicyAudit) -> Bool {
self.allowed
}
///|
pub fn PolicyAudit::detail(self : PolicyAudit) -> String {
self.detail
}
///|
/// Bounded audit journal shared by authorization and management APIs.
pub struct PolicyAuditJournal {
entries : Array[PolicyAudit]
limit : Int
mut next_sequence : Int
} derive(Debug)
///|
pub fn PolicyAuditJournal::new(
limit? : Int = 4096,
) -> Result[PolicyAuditJournal, String] {
if limit < 1 {
Err("policy audit limit must be positive")
} else {
Ok({ entries: [], limit, next_sequence: 1 })
}
}
///|
pub fn PolicyAuditJournal::len(self : PolicyAuditJournal) -> Int {
self.entries.length()
}
///|
pub fn PolicyAuditJournal::all(self : PolicyAuditJournal) -> Array[PolicyAudit] {
self.entries.copy()
}
///|
pub fn PolicyAuditJournal::append(
self : PolicyAuditJournal,
timestamp : Int,
action : AuditAction,
operator : String,
station : CommonAddress?,
object : InformationAddress?,
allowed : Bool,
detail : String,
) -> PolicyAudit {
let entry = {
sequence: self.next_sequence,
timestamp,
action,
operator,
station,
object,
allowed,
detail,
}
self.next_sequence += 1
self.entries.push(entry)
while self.entries.length() > self.limit {
ignore(self.entries.remove(0))
}
entry
}
///|
/// Central policy evaluator for operator-facing services.
pub struct OperationalPolicy {
rules : Array[PolicyRule]
windows : Array[MaintenanceWindow]
identities : Map[String, OperatorIdentity]
journal : PolicyAuditJournal
mut revision : Int
} derive(Debug)
///|
pub fn OperationalPolicy::new(
audit_limit? : Int = 4096,
) -> Result[OperationalPolicy, String] {
match PolicyAuditJournal::new(limit=audit_limit) {
Err(error) => Err(error)
Ok(journal) =>
Ok({ rules: [], windows: [], identities: {}, journal, revision: 0 })
}
}
///|
pub fn OperationalPolicy::revision(self : OperationalPolicy) -> Int {
self.revision
}
///|
pub fn OperationalPolicy::journal(
self : OperationalPolicy,
) -> PolicyAuditJournal {
self.journal
}
///|
pub fn OperationalPolicy::add_identity(
self : OperationalPolicy,
identity : OperatorIdentity,
) -> Result[Unit, String] {
if self.identities.contains(identity.id()) {
Err("operator identity already exists")
} else {
self.identities[identity.id()] = identity
self.revision += 1
Ok(())
}
}
///|
pub fn OperationalPolicy::remove_identity(
self : OperationalPolicy,
id : String,
) -> Result[Unit, String] {
if self.identities.contains(id) {
self.identities.remove(id)
self.revision += 1
Ok(())
} else {
Err("operator identity does not exist")
}
}
///|
pub fn OperationalPolicy::identity(
self : OperationalPolicy,
id : String,
) -> OperatorIdentity? {
self.identities.get(id)
}
///|
pub fn OperationalPolicy::add_rule(
self : OperationalPolicy,
rule : PolicyRule,
) -> Result[Unit, String] {
for current in self.rules {
if current.id() == rule.id() {
return Err("policy rule id already exists")
}
}
self.rules.push(rule)
self.revision += 1
Ok(())
}
///|
pub fn OperationalPolicy::add_window(
self : OperationalPolicy,
window : MaintenanceWindow,
) -> Result[Unit, String] {
for current in self.windows {
if current.id() == window.id() {
return Err("maintenance window id already exists")
}
}
self.windows.push(window)
self.windows.sort_by((left, right) => left.start() - right.start())
self.revision += 1
Ok(())
}
///|
pub fn OperationalPolicy::windows(
self : OperationalPolicy,
) -> Array[MaintenanceWindow] {
self.windows.copy()
}
///|
fn rule_matches(
rule : PolicyRule,
identity : OperatorIdentity,
permission : OperationalPermission,
station : CommonAddress,
type_id : ApplicationType?,
) -> Bool {
let station_ok = match rule.station() {
Some(value) => value == station
None => true
}
let type_ok = match type_id {
Some(value) =>
rule.type_ids().length() == 0 || rule.type_ids().contains(value)
None => true
}
let role_ok = rule.roles().length() == 0 ||
rule.roles().contains(identity.role())
rule.enabled() &&
rule.permission() == permission &&
station_ok &&
type_ok &&
role_ok
}
///|
fn OperationalPolicy::policy_blocked_by_window(
self : OperationalPolicy,
permission : OperationalPermission,
station : CommonAddress,
timestamp : Int,
) -> Bool {
for window in self.windows {
if window.contains(station, timestamp) &&
(
(permission == SubmitCommands && window.blocks_commands()) ||
(permission == WriteFiles && window.blocks_file_writes())
) {
return true
}
}
false
}
///|
pub fn OperationalPolicy::authorize(
self : OperationalPolicy,
operator_id : String,
permission : OperationalPermission,
station : CommonAddress,
type_id : ApplicationType?,
timestamp : Int,
) -> PolicyDecision {
let identity = self.identities.get(operator_id)
match identity {
None => {
ignore(
self.journal.append(
timestamp,
AuthorizationChecked,
operator_id,
None,
None,
false,
"unknown operator",
),
)
{
allowed: false,
permission,
operator: operator_id,
station,
rule_id: None,
reason: "unknown operator",
}
}
Some(value) =>
if !value.active() || !value.covers(station) {
ignore(
self.journal.append(
timestamp,
AuthorizationChecked,
value.id(),
Some(station),
None,
false,
"operator is inactive or out of scope",
),
)
{
allowed: false,
permission,
operator: value.id(),
station,
rule_id: None,
reason: "operator is inactive or out of scope",
}
} else if self.policy_blocked_by_window(permission, station, timestamp) {
ignore(
self.journal.append(
timestamp,
AuthorizationChecked,
value.id(),
Some(station),
None,
false,
"maintenance window blocks operation",
),
)
{
allowed: false,
permission,
operator: value.id(),
station,
rule_id: None,
reason: "maintenance window blocks operation",
}
} else {
let direct = role_permissions(value.role()).contains(permission)
let mut matched : Int? = None
for rule in self.rules {
if rule_matches(rule, value, permission, station, type_id) {
matched = Some(rule.id())
}
}
let allowed = direct && (matched is Some(_) || self.rules.length() == 0)
let reason = if allowed {
"operation authorized"
} else {
"operation denied by policy"
}
ignore(
self.journal.append(
timestamp,
AuthorizationChecked,
value.id(),
Some(station),
None,
allowed,
reason,
),
)
{
allowed,
permission,
operator: value.id(),
station,
rule_id: matched,
reason,
}
}
}
}
///|
pub fn OperatorRole::examples() -> Array[OperatorRole] {
[ViewerRole, EngineerRole, DispatcherRole, AdministratorRole, ServiceRole]
}
///|
pub fn operational_permission_examples() -> Array[OperationalPermission] {
[
ViewTelemetry,
ViewDiagnostics,
ReadFiles,
WriteFiles,
SubmitCommands,
ConfirmCommands,
ManageStations,
ManageRoutes,
ManagePolicies,
PerformFailover,
]
}
///|
pub fn audit_action_examples() -> Array[AuditAction] {
[
AuthorizationChecked,
PolicyChanged,
MaintenanceChanged,
CommandAuthorized,
FileAuthorized,
StationConfigured,
FailoverAuthorized,
]
}