use std::{collections::HashMap, fs, path::Path};
use serde::Deserialize;
use serde_json::json;
use crate::{
configuration::{BundleConfiguration, load_tui_configuration},
relay::{POLICIES_FILE, POLICIES_FORMAT_VERSION, RelayError, relay_error},
};
use super::context::{AuthorizationContext, PolicyControls, PolicyScope, UiSessionAuthorization};
use super::resolution::{normalize_policy_id, resolve_session_policy_controls};
const RELAY_FILE: &str = "relay.toml";
const DEFAULT_PERMISSION_MAX_PENDING: usize = 256;
const MIN_PERMISSION_MAX_PENDING: usize = 1;
const MAX_PERMISSION_MAX_PENDING: usize = 4096;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct RawPoliciesFile {
format_version: u32,
#[serde(default)]
default: Option<String>,
#[serde(default)]
policies: Vec<RawPolicyPreset>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct RawPolicyPreset {
id: String,
#[serde(default, rename = "description")]
_description: Option<String>,
controls: RawPolicyControls,
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct RawRelayFile {
#[serde(default)]
relay: Option<RawRelaySection>,
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct RawRelaySection {
#[serde(default)]
permission: Option<RawRelayPermissionSection>,
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct RawRelayPermissionSection {
#[serde(default)]
max_pending: Option<usize>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct RawPolicyControls {
find: String,
list: String,
look: String,
send: String,
#[serde(default = "default_raww_policy_scope")]
raww: String,
#[serde(default = "default_grant_policy_scope")]
grant: String,
#[serde(default = "default_updown_policy_scope")]
updown: String,
#[serde(default, rename = "do")]
do_controls: HashMap<String, String>,
#[serde(default, rename = "new")]
new_controls: HashMap<String, String>,
#[serde(default, rename = "change")]
change_controls: HashMap<String, String>,
}
fn default_raww_policy_scope() -> String {
"all:home".to_string()
}
fn default_grant_policy_scope() -> String {
"none".to_string()
}
fn default_updown_policy_scope() -> String {
"none".to_string()
}
pub(super) fn load_policy_presets(
configuration_root: &Path,
) -> Result<(HashMap<String, PolicyControls>, Option<String>), RelayError> {
let policies_path = configuration_root.join(POLICIES_FILE);
let policies_raw = fs::read_to_string(&policies_path).map_err(|source| {
relay_error(
"validation_invalid_arguments",
"failed to load authorization policy artifact",
Some(json!({
"path": policies_path.display().to_string(),
"cause": source.to_string(),
})),
)
})?;
let policies_file = toml::from_str::<RawPoliciesFile>(&policies_raw).map_err(|source| {
relay_error(
"validation_invalid_arguments",
"failed to parse authorization policy artifact",
Some(json!({
"path": policies_path.display().to_string(),
"cause": source.to_string(),
})),
)
})?;
if policies_file.format_version != POLICIES_FORMAT_VERSION {
return Err(relay_error(
"validation_invalid_arguments",
"authorization policy artifact has unsupported format-version",
Some(json!({
"path": policies_path.display().to_string(),
"format_version": policies_file.format_version,
})),
));
}
let mut presets = HashMap::<String, PolicyControls>::new();
for policy in policies_file.policies {
let policy_id = normalize_policy_id(policy.id.as_str()).ok_or_else(|| {
relay_error(
"validation_invalid_arguments",
"policy id must be non-empty",
Some(json!({
"path": policies_path.display().to_string(),
})),
)
})?;
if presets.contains_key(policy_id) {
return Err(relay_error(
"validation_invalid_arguments",
"authorization policy id must be unique",
Some(json!({
"path": policies_path.display().to_string(),
"policy_id": policy_id,
})),
));
}
let controls = parse_policy_controls(policy.controls, policies_path.as_path(), policy_id)?;
presets.insert(policy_id.to_string(), controls);
}
let default_policy_id = policies_file
.default
.as_deref()
.and_then(normalize_policy_id)
.map(ToString::to_string);
if let Some(default_policy_id) = default_policy_id.as_deref()
&& !presets.contains_key(default_policy_id)
{
return Err(relay_error(
"validation_invalid_arguments",
"authorization default policy references unknown policy id",
Some(json!({
"path": policies_path.display().to_string(),
"policy_id": default_policy_id,
})),
));
}
Ok((presets, default_policy_id))
}
pub(in crate::relay) fn load_authorization_context(
configuration_root: &Path,
bundle: Option<&BundleConfiguration>,
) -> Result<AuthorizationContext, RelayError> {
let policies_path = configuration_root.join(POLICIES_FILE);
let (presets, default_policy_id) = load_policy_presets(configuration_root)?;
let permission_max_pending = load_permission_max_pending(configuration_root)?;
let conservative_default = PolicyControls::conservative_default();
let mut controls_by_session =
HashMap::with_capacity(bundle.map_or(0, |bundle| bundle.members.len()));
if let Some(bundle) = bundle {
for member in &bundle.members {
let controls = resolve_session_policy_controls(
member,
&presets,
default_policy_id.as_deref(),
&conservative_default,
policies_path.as_path(),
)?;
controls_by_session.insert(member.id.clone(), controls.clone());
}
}
let mut ui_sessions = HashMap::<String, UiSessionAuthorization>::new();
if let Some(tui_configuration) =
load_tui_configuration(configuration_root).map_err(map_tui_configuration_error)?
{
for session in tui_configuration.sessions {
let session_id = session.id.clone();
let policy_id = normalize_policy_id(session.policy.as_str()).ok_or_else(|| {
relay_error(
"validation_unknown_policy",
"ui session policy reference is empty",
Some(json!({
"session_selector": session_id.as_str(),
"session_id": session_id.as_str(),
})),
)
})?;
let controls = presets.get(policy_id).ok_or_else(|| {
relay_error(
"validation_unknown_policy",
"ui session policy references unknown policy id",
Some(json!({
"session_selector": session_id.as_str(),
"session_id": session_id.as_str(),
"policy_id": policy_id,
})),
)
})?;
if let Some(existing_controls) = controls_by_session.get(session_id.as_str())
&& existing_controls != controls
{
return Err(relay_error(
"validation_invalid_arguments",
"session_id maps to conflicting authorization policies",
Some(json!({
"session_id": session_id.as_str(),
})),
));
}
controls_by_session.insert(session_id.clone(), controls.clone());
ui_sessions
.entry(session_id)
.and_modify(|existing| {
if existing.display_name.is_none() {
existing.display_name = session.name.clone();
}
})
.or_insert(UiSessionAuthorization {
display_name: session.name.clone(),
});
}
}
Ok(AuthorizationContext {
controls_by_session,
ui_sessions,
permission_max_pending,
})
}
fn load_permission_max_pending(configuration_root: &Path) -> Result<usize, RelayError> {
let path = configuration_root.join(RELAY_FILE);
if !path.exists() {
return Ok(DEFAULT_PERMISSION_MAX_PENDING);
}
let raw = fs::read_to_string(&path).map_err(|source| {
relay_error(
"validation_invalid_arguments",
"failed to load relay configuration",
Some(json!({
"path": path.display().to_string(),
"cause": source.to_string(),
})),
)
})?;
let parsed = toml::from_str::<RawRelayFile>(raw.as_str()).map_err(|source| {
relay_error(
"validation_invalid_arguments",
"failed to parse relay configuration",
Some(json!({
"path": path.display().to_string(),
"cause": source.to_string(),
})),
)
})?;
let configured = parsed
.relay
.and_then(|relay| relay.permission)
.and_then(|permission| permission.max_pending)
.unwrap_or(DEFAULT_PERMISSION_MAX_PENDING);
if (MIN_PERMISSION_MAX_PENDING..=MAX_PERMISSION_MAX_PENDING).contains(&configured) {
return Ok(configured);
}
Err(relay_error(
"validation_invalid_arguments",
"relay permission max-pending is out of supported range",
Some(json!({
"path": path.display().to_string(),
"field": "relay.permission.max-pending",
"value": configured,
"minimum": MIN_PERMISSION_MAX_PENDING,
"maximum": MAX_PERMISSION_MAX_PENDING,
})),
))
}
fn parse_policy_controls(
controls: RawPolicyControls,
policies_path: &Path,
policy_id: &str,
) -> Result<PolicyControls, RelayError> {
let find = parse_scope_for_control(
controls.find.as_str(),
policies_path,
policy_id,
"find",
&[
PolicyScope::None,
PolicyScope::SelfOnly,
PolicyScope::AllHome,
PolicyScope::AllAll,
],
"validation_invalid_arguments",
"authorization policy control uses unsupported scope value",
)?;
let list = parse_scope_for_control(
controls.list.as_str(),
policies_path,
policy_id,
"list",
&[PolicyScope::AllHome, PolicyScope::AllAll],
"validation_invalid_arguments",
"authorization policy list control uses unsupported scope value",
)?;
let look = parse_scope_for_control(
controls.look.as_str(),
policies_path,
policy_id,
"look",
&[
PolicyScope::None,
PolicyScope::SelfOnly,
PolicyScope::AllHome,
PolicyScope::AllAll,
],
"validation_invalid_arguments",
"authorization policy control uses unsupported scope value",
)?;
let send = parse_scope_for_control(
controls.send.as_str(),
policies_path,
policy_id,
"send",
&[PolicyScope::AllHome, PolicyScope::AllAll],
"validation_invalid_arguments",
"authorization policy send control uses unsupported scope value",
)?;
let raww = parse_scope_for_control(
controls.raww.as_str(),
policies_path,
policy_id,
"raww",
&[
PolicyScope::None,
PolicyScope::SelfOnly,
PolicyScope::AllHome,
PolicyScope::AllAll,
],
"validation_invalid_policy_scope",
"authorization policy raww control uses unsupported scope value",
)?;
let grant = parse_scope_for_control(
controls.grant.as_str(),
policies_path,
policy_id,
"grant",
&[PolicyScope::None, PolicyScope::AllHome],
"validation_invalid_policy_scope",
"authorization policy grant control uses unsupported scope value",
)?;
let updown = parse_scope_for_control(
controls.updown.as_str(),
policies_path,
policy_id,
"updown",
&[PolicyScope::None, PolicyScope::AllHome],
"validation_invalid_policy_scope",
"authorization policy updown control uses unsupported scope value",
)?;
let do_controls = parse_action_scope_map(
controls.do_controls,
"do",
policies_path,
policy_id,
&[
PolicyScope::None,
PolicyScope::SelfOnly,
PolicyScope::AllHome,
PolicyScope::AllAll,
],
)?;
let new_controls = parse_action_scope_map(
controls.new_controls,
"new",
policies_path,
policy_id,
&[PolicyScope::None, PolicyScope::AllHome, PolicyScope::AllAll],
)?;
let change_controls = parse_action_scope_map(
controls.change_controls,
"change",
policies_path,
policy_id,
&[PolicyScope::None, PolicyScope::AllHome, PolicyScope::AllAll],
)?;
Ok(PolicyControls {
find,
list,
look,
send,
raww,
grant,
updown,
do_controls,
new_controls,
change_controls,
})
}
fn parse_action_scope_map(
raw_map: HashMap<String, String>,
namespace: &str,
policies_path: &Path,
policy_id: &str,
allowed_scopes: &[PolicyScope],
) -> Result<HashMap<String, PolicyScope>, RelayError> {
let mut result = HashMap::with_capacity(raw_map.len());
for (action_id, scope_value) in raw_map {
let action_id = action_id.trim();
if action_id.is_empty() {
return Err(relay_error(
"validation_invalid_arguments",
"policy action id must be non-empty",
Some(json!({
"path": policies_path.display().to_string(),
"policy_id": policy_id,
"namespace": namespace,
})),
));
}
let scope = parse_scope_for_control(
scope_value.as_str(),
policies_path,
policy_id,
format!("{namespace}.{action_id}").as_str(),
allowed_scopes,
"validation_invalid_arguments",
"authorization policy control uses unsupported scope value",
)?;
result.insert(action_id.to_string(), scope);
}
Ok(result)
}
fn parse_scope_for_control(
raw: &str,
policies_path: &Path,
policy_id: &str,
control: &str,
allowed: &[PolicyScope],
error_code: &str,
unsupported_message: &str,
) -> Result<PolicyScope, RelayError> {
let value = raw.trim();
let parsed = match value {
"none" => PolicyScope::None,
"self" => PolicyScope::SelfOnly,
"all:home" => PolicyScope::AllHome,
"all:all" => PolicyScope::AllAll,
_ => {
return Err(relay_error(
error_code,
unsupported_message,
Some(json!({
"path": policies_path.display().to_string(),
"policy_id": policy_id,
"control": control,
"value": value,
})),
));
}
};
if allowed.contains(&parsed) {
return Ok(parsed);
}
Err(relay_error(
error_code,
unsupported_message,
Some(json!({
"path": policies_path.display().to_string(),
"policy_id": policy_id,
"control": control,
"value": value,
})),
))
}
pub(super) fn map_tui_configuration_error(
source: crate::configuration::ConfigurationError,
) -> RelayError {
match source {
crate::configuration::ConfigurationError::InvalidConfiguration { path, message } => {
relay_error(
"validation_invalid_arguments",
"tui configuration is invalid",
Some(json!({
"path": path.display().to_string(),
"cause": message,
})),
)
}
crate::configuration::ConfigurationError::Io { context, source } => relay_error(
"validation_invalid_arguments",
"failed to load tui configuration",
Some(json!({
"context": context,
"cause": source.to_string(),
})),
),
other => relay_error(
"validation_invalid_arguments",
"failed to load tui configuration",
Some(json!({
"cause": other.to_string(),
})),
),
}
}