use bon::{Builder, bon};
use capctl::{Cap, CapSet};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
use std::{
cell::RefCell,
error::Error,
fmt::{self, Display},
ops::{Index, Not},
rc::{Rc, Weak},
str::FromStr,
};
use crate::{
rc_refcell,
util::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1},
};
use super::{
actor::{SActor, SGroupType, SGroups, SUserType},
options::{Level, Opt, OptBuilder},
};
#[derive(Deserialize, PartialEq, Eq, Debug, Default, Clone)]
pub struct SPolicy {
#[serde(default, deserialize_with = "sconfig_opt", alias = "o")]
pub options: Option<Rc<RefCell<Opt>>>,
#[serde(default, alias = "r")]
pub roles: Vec<Rc<RefCell<SRole>>>,
#[serde(default, flatten)]
pub extra_fields: Map<String, Value>,
}
fn sconfig_opt<'de, D>(deserializer: D) -> Result<Option<Rc<RefCell<Opt>>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<Rc<RefCell<Opt>>> = Option::deserialize(deserializer)?;
opt.map_or_else(
|| Ok(None),
|opt| {
opt.as_ref().borrow_mut().level = Level::Global;
Ok(Some(opt))
},
)
}
#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "kebab-case")]
pub struct SRole {
#[serde(alias = "n", default, skip_serializing_if = "String::is_empty")]
pub name: String,
#[serde(alias = "a", default, skip_serializing_if = "Vec::is_empty")]
pub actors: Vec<SActor>,
#[serde(alias = "t", default, skip_serializing_if = "Vec::is_empty")]
pub tasks: Vec<Rc<RefCell<STask>>>,
#[serde(
alias = "o",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "srole_opt"
)]
pub options: Option<Rc<RefCell<Opt>>>,
#[serde(default, flatten, skip_serializing_if = "Map::is_empty")]
pub extra_fields: Map<String, Value>,
#[serde(skip)]
pub config: Option<Weak<RefCell<SPolicy>>>,
}
impl PartialEq for SRole {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.actors == other.actors
&& self.tasks == other.tasks
&& self.options == other.options
&& self.extra_fields == other.extra_fields
}
}
impl Eq for SRole {}
fn srole_opt<'de, D>(deserializer: D) -> Result<Option<Rc<RefCell<Opt>>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<Rc<RefCell<Opt>>> = Option::deserialize(deserializer)?;
opt.map_or_else(
|| Ok(None),
|opt| {
opt.as_ref().borrow_mut().level = Level::Role;
Ok(Some(opt))
},
)
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(untagged)]
pub enum IdTask {
Name(String),
Number(usize),
}
impl Default for IdTask {
fn default() -> Self {
Self::Number(0)
}
}
impl std::fmt::Display for IdTask {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Name(name) => write!(f, "{name}"),
Self::Number(id) => write!(f, "{id}"),
}
}
}
pub(super) fn cmds_is_default(cmds: &SCommands) -> bool {
cmds.default
.as_ref()
.is_none_or(|b| *b == SetBehavior::default())
&& cmds.add.is_empty()
&& cmds.sub.is_empty()
&& cmds.extra_fields.is_empty()
}
#[derive(Deserialize, Debug, Default)]
pub struct STask {
#[serde(alias = "n", default, skip_serializing_if = "IdTask::is_number")]
pub name: IdTask,
#[serde(alias = "p", skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
#[serde(
alias = "i",
alias = "credentials",
default,
skip_serializing_if = "is_default"
)]
pub cred: SCredentials,
#[serde(
alias = "c",
alias = "cmds",
default,
skip_serializing_if = "cmds_is_default"
)]
pub commands: SCommands,
#[serde(
alias = "o",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "stask_opt"
)]
pub options: Option<Rc<RefCell<Opt>>>,
#[serde(default, flatten, skip_serializing_if = "Map::is_empty")]
pub extra_fields: Map<String, Value>,
#[serde(skip)]
pub role: Option<Weak<RefCell<SRole>>>,
}
impl PartialEq for STask {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.purpose == other.purpose
&& self.cred == other.cred
&& self.commands == other.commands
&& self.options == other.options
&& self.extra_fields == other.extra_fields
}
}
impl Eq for STask {}
fn stask_opt<'de, D>(deserializer: D) -> Result<Option<Rc<RefCell<Opt>>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<Rc<RefCell<Opt>>> = Option::deserialize(deserializer)?;
opt.map_or_else(
|| Ok(None),
|opt| {
opt.as_ref().borrow_mut().level = Level::Task;
Ok(Some(opt))
},
)
}
#[cfg_attr(test, derive(Clone))]
#[derive(Deserialize, Debug, Builder, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub struct SCredentials {
#[serde(alias = "u", skip_serializing_if = "Option::is_none")]
#[builder(into)]
pub setuid: Option<SUserEither>,
#[serde(alias = "g", skip_serializing_if = "Option::is_none")]
#[builder(into)]
pub setgid: Option<SGroupsEither>,
#[serde(default, alias = "c", skip_serializing_if = "Option::is_none")]
pub capabilities: Option<SCapabilities>,
#[serde(default, flatten, skip_serializing_if = "Map::is_empty")]
#[builder(default)]
pub extra_fields: Map<String, Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum SUserEither {
MandatoryUser(SUserType),
UserSelector(SSetuidSet),
}
impl From<SUserType> for SUserEither {
fn from(actor: SUserType) -> Self {
Self::MandatoryUser(actor)
}
}
impl From<SSetuidSet> for SUserEither {
fn from(set: SSetuidSet) -> Self {
Self::UserSelector(set)
}
}
impl From<&str> for SUserEither {
fn from(name: &str) -> Self {
Self::MandatoryUser(name.into())
}
}
impl From<u32> for SUserEither {
fn from(id: u32) -> Self {
Self::MandatoryUser(id.into())
}
}
#[derive(Deserialize, Debug, Clone, Builder, PartialEq, Eq)]
pub struct SSetuidSet {
#[serde(
alias = "d",
rename = "default",
default,
skip_serializing_if = "is_default"
)]
#[builder(default)]
pub default: SetBehavior,
#[builder(into)]
#[serde(alias = "f", skip_serializing_if = "Option::is_none")]
pub fallback: Option<SUserType>,
#[serde(default, alias = "a", skip_serializing_if = "Vec::is_empty")]
#[builder(default, with = FromIterator::from_iter)]
pub add: Vec<SUserType>,
#[serde(
default,
alias = "del",
alias = "s",
skip_serializing_if = "Vec::is_empty"
)]
#[builder(default, with = FromIterator::from_iter)]
pub sub: Vec<SUserType>,
}
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
#[repr(u32)]
pub enum SetBehavior {
#[default]
None = HARDENED_ENUM_VALUE_0,
All = HARDENED_ENUM_VALUE_1,
}
impl SetBehavior {
#[must_use]
pub const fn is_none(&self) -> bool {
matches!(self, Self::None)
}
#[must_use]
pub const fn is_all(&self) -> bool {
matches!(self, Self::All)
}
#[must_use]
pub const fn from_repr(value: u32) -> Option<Self> {
match value {
HARDENED_ENUM_VALUE_0 => Some(Self::None),
HARDENED_ENUM_VALUE_1 => Some(Self::All),
_ => None,
}
}
}
impl Display for SetBehavior {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::All => write!(f, "all"),
}
}
}
impl FromStr for SetBehavior {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"none" => Ok(Self::None),
"all" => Ok(Self::All),
_ => Err(format!("Invalid SetBehavior value: {s}")),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum SGroupsEither {
MandatoryGroup(SGroupType),
MandatoryGroups(SGroups),
GroupSelector(SSetgidSet),
}
impl From<SGroups> for SGroupsEither {
fn from(group: SGroups) -> Self {
Self::MandatoryGroups(group)
}
}
impl From<SSetgidSet> for SGroupsEither {
fn from(set: SSetgidSet) -> Self {
Self::GroupSelector(set)
}
}
impl From<&str> for SGroupsEither {
fn from(name: &str) -> Self {
Self::MandatoryGroup(name.into())
}
}
impl From<u32> for SGroupsEither {
fn from(id: u32) -> Self {
Self::MandatoryGroup(id.into())
}
}
#[derive(Debug, Clone, Builder, PartialEq, Eq)]
pub struct SSetgidSet {
#[builder(start_fn)]
pub default_behavior: SetBehavior,
#[builder(start_fn, into)]
pub fallback: SGroups,
#[builder(default, with = FromIterator::from_iter)]
pub add: Vec<SGroups>,
#[builder(default, with = FromIterator::from_iter)]
pub sub: Vec<SGroups>,
}
#[derive(PartialEq, Eq, Debug, Builder, Default)]
#[cfg_attr(test, derive(Clone))]
pub struct SCapabilities {
#[builder(start_fn)]
pub default_behavior: SetBehavior,
#[builder(field)]
pub add: CapSet,
#[builder(field)]
pub sub: CapSet,
}
impl<S: s_capabilities_builder::State> SCapabilitiesBuilder<S> {
pub fn add_cap(mut self, cap: Cap) -> Self {
self.add.add(cap);
self
}
pub const fn add_all(mut self, set: CapSet) -> Self {
self.add = set;
self
}
pub fn sub_cap(mut self, cap: Cap) -> Self {
self.sub.add(cap);
self
}
pub const fn sub_all(mut self, set: CapSet) -> Self {
self.sub = set;
self
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(untagged)]
pub enum SCommand {
Simple(String),
Complex(Value),
}
#[cfg_attr(test, derive(Clone))]
#[derive(PartialEq, Eq, Debug, Default)]
pub struct SCommands {
pub default: Option<SetBehavior>,
pub add: Vec<SCommand>,
pub sub: Vec<SCommand>,
pub extra_fields: Map<String, Value>,
}
impl From<usize> for IdTask {
fn from(id: usize) -> Self {
Self::Number(id)
}
}
impl From<String> for IdTask {
fn from(name: String) -> Self {
Self::Name(name)
}
}
impl From<&str> for IdTask {
fn from(name: &str) -> Self {
Self::Name(name.to_string())
}
}
impl From<&str> for SCommand {
fn from(name: &str) -> Self {
Self::Simple(name.to_string())
}
}
impl From<CapSet> for SCapabilities {
fn from(capset: CapSet) -> Self {
Self {
add: capset,
..Default::default()
}
}
}
#[bon]
impl SPolicy {
#[builder]
pub fn new(
#[builder(field)] roles: Vec<Rc<RefCell<SRole>>>,
#[builder(with = |f : impl Fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Global))))]
options: Option<Rc<RefCell<Opt>>>,
extra_fields: Option<Map<String, Value>>,
) -> Rc<RefCell<Self>> {
let c = Rc::new(RefCell::new(Self {
roles: roles.clone(),
options,
extra_fields: extra_fields.unwrap_or_default(),
}));
for role in roles {
role.borrow_mut().config = Some(Rc::downgrade(&c));
}
c
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.roles.is_empty() && self.options.is_none() && self.extra_fields.is_empty()
}
}
pub trait RoleGetter {
fn role(&self, name: &str) -> Option<Rc<RefCell<SRole>>>;
fn task<T: Into<IdTask>>(
&self,
role: &str,
name: T,
) -> Result<Rc<RefCell<STask>>, Box<dyn Error>>;
}
pub trait TaskGetter {
fn task(&self, name: &IdTask) -> Option<Rc<RefCell<STask>>>;
}
impl RoleGetter for Rc<RefCell<SPolicy>> {
fn role(&self, name: &str) -> Option<Rc<RefCell<SRole>>> {
self.as_ref()
.borrow()
.roles
.iter()
.find(|role| role.borrow().name == name)
.cloned()
}
fn task<T: Into<IdTask>>(
&self,
role: &str,
name: T,
) -> Result<Rc<RefCell<STask>>, Box<dyn Error>> {
let name = name.into();
self.role(role)
.and_then(|role| role.as_ref().borrow().task(&name).cloned())
.ok_or_else(|| format!("Task {name} not found in role {role}").into())
}
}
impl TaskGetter for Rc<RefCell<SRole>> {
fn task(&self, name: &IdTask) -> Option<Rc<RefCell<STask>>> {
self.as_ref()
.borrow()
.tasks
.iter()
.find(|task| task.borrow().name == *name)
.cloned()
}
}
impl<S: s_policy_builder::State> SPolicyBuilder<S> {
pub fn role(mut self, role: Rc<RefCell<SRole>>) -> Self {
self.roles.push(role);
self
}
pub fn roles(mut self, roles: impl IntoIterator<Item = Rc<RefCell<SRole>>>) -> Self {
self.roles.extend(roles);
self
}
}
impl<S: s_role_builder::State> SRoleBuilder<S> {
pub fn task(mut self, task: Rc<RefCell<STask>>) -> Self {
self.tasks.push(task);
self
}
pub fn actor(mut self, actor: SActor) -> Self {
self.actors.push(actor);
self
}
pub fn actors(mut self, actors: impl IntoIterator<Item = SActor>) -> Self {
self.actors.extend(actors);
self
}
pub fn tasks(mut self, tasks: impl IntoIterator<Item = Rc<RefCell<STask>>>) -> Self {
self.tasks.extend(tasks);
self
}
}
#[bon]
impl SRole {
#[builder]
pub fn new(
#[builder(start_fn, into)] name: String,
#[builder(field)] tasks: Vec<Rc<RefCell<STask>>>,
#[builder(field)] actors: Vec<SActor>,
#[builder(with = |f : impl Fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Role))))]
options: Option<Rc<RefCell<Opt>>>,
#[builder(default)] extra_fields: Map<String, Value>,
) -> Rc<RefCell<Self>> {
let s = Rc::new(RefCell::new(Self {
name,
actors,
tasks,
options,
extra_fields,
config: None,
}));
for task in &s.as_ref().borrow_mut().tasks {
task.borrow_mut().role = Some(Rc::downgrade(&s));
}
s
}
#[must_use]
pub fn config(&self) -> Option<Rc<RefCell<SPolicy>>> {
self.config.as_ref()?.upgrade()
}
#[must_use]
pub fn task(&self, name: &IdTask) -> Option<&Rc<RefCell<STask>>> {
self.tasks
.iter()
.find(|task| task.as_ref().borrow().name == *name)
}
}
#[bon]
impl STask {
#[builder]
pub fn new(
#[builder(start_fn, into)] name: IdTask,
purpose: Option<String>,
#[builder(default)] cred: SCredentials,
#[builder(default)] commands: SCommands,
#[builder(with = |f : impl Fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Task))))]
options: Option<Rc<RefCell<Opt>>>,
#[builder(default)] extra_fields: Map<String, Value>,
role: Option<Weak<RefCell<SRole>>>,
) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
name,
purpose,
cred,
commands,
options,
extra_fields,
role,
}))
}
#[must_use]
pub fn role(&self) -> Option<Rc<RefCell<SRole>>> {
self.role.as_ref()?.upgrade()
}
}
impl Index<usize> for SPolicy {
type Output = Rc<RefCell<SRole>>;
fn index(&self, index: usize) -> &Self::Output {
&self.roles[index]
}
}
impl Index<usize> for SRole {
type Output = Rc<RefCell<STask>>;
fn index(&self, index: usize) -> &Self::Output {
&self.tasks[index]
}
}
#[bon]
impl SCommands {
#[builder]
pub const fn new(
#[builder(start_fn)] default: SetBehavior,
#[builder(default, with = FromIterator::from_iter)] add: Vec<SCommand>,
#[builder(default, with = FromIterator::from_iter)] sub: Vec<SCommand>,
#[builder(default, with = <_>::from_iter)] extra_fields: Map<String, Value>,
) -> Self {
Self {
default: Some(default),
add,
sub,
extra_fields,
}
}
}
impl SCapabilities {
#[must_use]
pub fn to_capset(&self) -> CapSet {
let mut capset = match self.default_behavior {
SetBehavior::All => capctl::bounding::probe() & CapSet::not(CapSet::empty()),
SetBehavior::None => CapSet::empty(),
};
capset = capset.union(self.add);
capset.drop_all(self.sub);
capset
}
}
#[cfg(test)]
mod tests {
use capctl::Cap;
use chrono::Duration;
use indexmap::IndexSet;
use crate::{
as_borrow,
database::{
actor::SGroupType,
options::{
EnvBehavior, PathBehavior, SAuthentication, SBounding, SEnvOptions, SPathOptions,
SPrivileged, STimeout, TimestampType,
},
},
};
use super::*;
#[allow(clippy::too_many_lines)]
#[test]
fn test_deserialize() {
let config = r#"
{
"options": {
"path": {
"default": "delete",
"add": ["path_add"],
"sub": ["path_sub"]
},
"env": {
"default": "delete",
"override_behavior": true,
"keep": ["keep_env"],
"check": ["check_env"]
},
"root": "privileged",
"bounding": "ignore",
"authentication": "skip",
"timeout": {
"type": "ppid",
"duration": "00:05:00"
}
},
"roles": [
{
"name": "role1",
"actors": [
{
"type": "user",
"name": "user1"
},
{
"type":"group",
"groups": ["group1","1000"]
}
],
"tasks": [
{
"name": "task1",
"purpose": "purpose1",
"cred": {
"setuid": {
"fallback": "user1",
"default": "all",
"add": ["user2"],
"sub": ["user3"]
},
"setgid": "setgid1",
"capabilities": {
"default": "all",
"add": ["cap_net_bind_service"],
"sub": ["cap_sys_admin"]
}
},
"commands": {
"default": "all",
"add": ["cmd1"],
"sub": ["cmd2"]
}
}
]
}
]
}
"#;
let config: SPolicy = serde_json::from_str(config).unwrap();
let options = config.options.as_ref().unwrap().as_ref().borrow();
let path = options.path.as_ref().unwrap();
assert_eq!(path.default_behavior, PathBehavior::Delete);
let default = IndexSet::new();
assert!(
path.add
.as_ref()
.unwrap_or(&default)
.first()
.is_some_and(|s| s == "path_add")
);
let env = options.env.as_ref().unwrap();
assert_eq!(env.default_behavior, EnvBehavior::Delete);
assert!(env.override_behavior.is_some_and(|b| b));
assert!(
env.keep
.as_ref()
.into_iter()
.flatten()
.next()
.is_some_and(|s| s == "keep_env")
);
assert!(
env.check
.as_ref()
.into_iter()
.flatten()
.next()
.is_some_and(|s| s == "check_env")
);
assert!(options.root.as_ref().unwrap().is_privileged());
assert!(options.bounding.as_ref().unwrap().is_ignore());
assert_eq!(options.authentication, Some(SAuthentication::Skip));
let timeout = options.timeout.as_ref().unwrap();
assert_eq!(timeout.type_field, Some(TimestampType::PPID));
assert_eq!(timeout.duration, Some(Duration::minutes(5)));
assert_eq!(config.roles[0].as_ref().borrow().name, "role1");
let actor0 = &config.roles[0].as_ref().borrow().actors[0];
assert_eq!(
actor0,
&SActor::User {
id: Some("user1".into()),
extra_fields: Map::default()
}
);
let actor1 = &config.roles[0].as_ref().borrow().actors[1];
match actor1 {
SActor::Group { groups, .. } => match groups.as_ref().unwrap() {
SGroups::Multiple(groups) => {
assert_eq!(&groups[0], "group1");
assert_eq!(groups[1], 1000);
}
SGroups::Single(_) => panic!("unexpected actor group type"),
},
_ => panic!("unexpected actor {actor1:?}"),
}
let role = config.roles[0].as_ref().borrow();
assert_eq!(as_borrow!(role[0]).purpose.as_ref().unwrap(), "purpose1");
let cred = &as_borrow!(&role[0]).cred;
let setuidstruct = SSetuidSet::builder()
.fallback("user1")
.default(SetBehavior::All)
.add(["user2".into()])
.sub(["user3".into()])
.build();
assert!(
matches!(cred.setuid.as_ref().unwrap(), SUserEither::UserSelector(set) if set == &setuidstruct)
);
assert_eq!(
*cred.setgid.as_ref().unwrap(),
SGroupsEither::MandatoryGroup(SGroupType::from("setgid1"))
);
let capabilities = cred.capabilities.as_ref().unwrap();
assert_eq!(capabilities.default_behavior, SetBehavior::All);
assert!(capabilities.add.has(Cap::NET_BIND_SERVICE));
assert!(capabilities.sub.has(Cap::SYS_ADMIN));
let commands = &as_borrow!(&role[0]).commands;
assert_eq!(*commands.default.as_ref().unwrap(), SetBehavior::All);
assert_eq!(commands.add[0], SCommand::Simple("cmd1".into()));
assert_eq!(commands.sub[0], SCommand::Simple("cmd2".into()));
}
#[allow(clippy::too_many_lines)]
#[test]
fn test_unknown_fields() {
let config = r#"
{
"options": {
"path": {
"default": "delete",
"add": ["path_add"],
"sub": ["path_sub"],
"unknown": "unknown"
},
"env": {
"default": "delete",
"keep": ["keep_env"],
"check": ["check_env"],
"unknown": "unknown"
},
"allow-root": false,
"allow-bounding": false,
"timeout": {
"type": "ppid",
"duration": "00:05:00",
"unknown": "unknown"
},
"unknown": "unknown"
},
"roles": [
{
"name": "role1",
"actors": [
{
"type": "user",
"name": "user1",
"unknown": "unknown"
},
{
"type":"bla",
"unknown": "unknown"
}
],
"tasks": [
{
"name": "task1",
"purpose": "purpose1",
"cred": {
"setuid": "setuid1",
"setgid": "setgid1",
"capabilities": {
"default": "all",
"add": ["cap_dac_override"],
"sub": ["cap_dac_override"]
},
"unknown": "unknown"
},
"commands": {
"default": "all",
"add": ["cmd1"],
"sub": ["cmd2"],
"unknown": "unknown"
},
"unknown": "unknown"
}
],
"unknown": "unknown"
}
],
"unknown": "unknown"
}
"#;
let config: SPolicy = serde_json::from_str(config).unwrap();
assert_eq!(config.extra_fields.get("unknown").unwrap(), "unknown");
let binding = config.options.unwrap();
let options = binding.as_ref().borrow();
let env = &options.env.as_ref().unwrap();
assert_eq!(env.extra_fields.get("unknown").unwrap(), "unknown");
assert_eq!(options.extra_fields.get("unknown").unwrap(), "unknown");
let timeout = options.timeout.as_ref().unwrap();
assert_eq!(timeout.extra_fields.get("unknown").unwrap(), "unknown");
assert_eq!(config.extra_fields.get("unknown").unwrap(), "unknown");
let actor0 = &as_borrow!(config.roles[0]).actors[0];
match actor0 {
SActor::User { id, extra_fields } => {
assert_eq!(id.as_ref().unwrap(), "user1");
assert_eq!(extra_fields.get("unknown").unwrap(), "unknown");
}
_ => panic!("unexpected actor type"),
}
let actor1 = &as_borrow!(config.roles[0]).actors[1];
match actor1 {
SActor::Unknown(unknown) => {
let obj = unknown.as_object().unwrap();
assert_eq!(obj.get("type").unwrap().as_str().unwrap(), "bla");
assert_eq!(obj.get("unknown").unwrap().as_str().unwrap(), "unknown");
}
_ => panic!("unexpected actor type"),
}
assert_eq!(
config.roles[0].as_ref().borrow()[0]
.as_ref()
.borrow()
.extra_fields
.get("unknown")
.as_ref()
.unwrap()
.as_str()
.unwrap(),
"unknown"
);
let role = config.roles[0].as_ref().borrow();
let cred = &role[0].as_ref().borrow().cred;
assert_eq!(cred.extra_fields.get("unknown").unwrap(), "unknown");
let commands = &as_borrow!(role[0]).commands;
assert_eq!(commands.extra_fields.get("unknown").unwrap(), "unknown");
}
#[allow(clippy::too_many_lines)]
#[test]
fn test_deserialize_alias() {
let config = r#"
{
"options": {
"path": {
"default": "delete",
"add": ["path_add"],
"del": ["path_sub"]
},
"env": {
"default": "delete",
"keep": ["keep_env"],
"check": ["check_env"]
},
"root": "privileged",
"bounding": "ignore",
"authentication": "skip",
"timeout": {
"type": "ppid",
"duration": "00:05:00"
}
},
"roles": [
{
"name": "role1",
"actors": [
{
"type": "user",
"name": "user1"
},
{
"type":"group",
"groups": ["group1","1000"]
}
],
"tasks": [
{
"name": "task1",
"purpose": "purpose1",
"cred": {
"setuid": "setuid1",
"setgid": "setgid1",
"capabilities": ["cap_net_bind_service"]
},
"commands": {
"default": "all",
"add": ["cmd1"],
"del": ["cmd2"]
}
}
]
}
]
}
"#;
let config: SPolicy = serde_json::from_str(config).unwrap();
let options = config.options.as_ref().unwrap().as_ref().borrow();
let path = options.path.as_ref().unwrap();
assert_eq!(path.default_behavior, PathBehavior::Delete);
let default = IndexSet::new();
assert!(
path.add
.as_ref()
.unwrap_or(&default)
.first()
.is_some_and(|s| s == "path_add")
);
let env = options.env.as_ref().unwrap();
assert_eq!(env.default_behavior, EnvBehavior::Delete);
assert!(
env.keep
.as_ref()
.unwrap()
.first()
.is_some_and(|s| s == "keep_env")
);
assert!(
env.check
.as_ref()
.unwrap()
.first()
.is_some_and(|s| s == "check_env")
);
assert!(options.root.as_ref().unwrap().is_privileged());
assert!(options.bounding.as_ref().unwrap().is_ignore());
assert_eq!(options.authentication, Some(SAuthentication::Skip));
let timeout = options.timeout.as_ref().unwrap();
assert_eq!(timeout.type_field, Some(TimestampType::PPID));
assert_eq!(timeout.duration, Some(Duration::minutes(5)));
assert_eq!(config.roles[0].as_ref().borrow().name, "role1");
let actor0 = &config.roles[0].as_ref().borrow().actors[0];
match actor0 {
SActor::User { id, .. } => {
assert_eq!(id.as_ref().unwrap(), "user1");
}
_ => panic!("unexpected actor type"),
}
let actor1 = &config.roles[0].as_ref().borrow().actors[1];
match actor1 {
SActor::Group { groups, .. } => match groups.as_ref().unwrap() {
SGroups::Multiple(groups) => {
assert_eq!(groups[0], SGroupType::from("group1"));
assert_eq!(groups[1], SGroupType::from(1000));
}
SGroups::Single(_) => panic!("unexpected actor group type"),
},
_ => panic!("unexpected actor {actor1:?}"),
}
let role = config.roles[0].as_ref().borrow();
assert_eq!(as_borrow!(role[0]).purpose.as_ref().unwrap(), "purpose1");
let cred = &as_borrow!(&role[0]).cred;
assert_eq!(
cred.setuid.as_ref().unwrap(),
&SUserEither::from(SUserType::from("setuid1"))
);
assert_eq!(
*cred.setgid.as_ref().unwrap(),
SGroupsEither::MandatoryGroup(SGroupType::from("setgid1"))
);
let capabilities = cred.capabilities.as_ref().unwrap();
assert_eq!(capabilities.default_behavior, SetBehavior::None);
assert!(capabilities.add.has(Cap::NET_BIND_SERVICE));
assert!(capabilities.sub.is_empty());
let commands = &as_borrow!(&role[0]).commands;
assert_eq!(*commands.default.as_ref().unwrap(), SetBehavior::All);
assert_eq!(commands.add[0], SCommand::Simple("cmd1".into()));
assert_eq!(commands.sub[0], SCommand::Simple("cmd2".into()));
}
#[test]
fn test_serialize() {
let config = SPolicy::builder()
.role(
SRole::builder("role1")
.actor(SActor::user("user1").build())
.actor(
SActor::group([SGroupType::from("group1"), SGroupType::from(1000)]).build(),
)
.task(
STask::builder("task1")
.purpose("purpose1".into())
.cred(
SCredentials::builder()
.setuid(SUserEither::UserSelector(
SSetuidSet::builder()
.fallback("user1")
.default(SetBehavior::All)
.add(["user2".into()])
.sub(["user3".into()])
.build(),
))
.setgid(SGroupsEither::MandatoryGroup(SGroupType::from(
"setgid1",
)))
.capabilities(
SCapabilities::builder(SetBehavior::All)
.add_cap(Cap::NET_BIND_SERVICE)
.sub_cap(Cap::SYS_ADMIN)
.build(),
)
.build(),
)
.commands(
SCommands::builder(SetBehavior::All)
.add(["cmd1".into()])
.sub(["cmd2".into()])
.build(),
)
.build(),
)
.build(),
)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Delete)
.add(["path_add"])
.sub(["path_sub"])
.build(),
)
.env(
SEnvOptions::builder(EnvBehavior::Delete)
.override_behavior(true)
.keep(["keep_env"])
.unwrap()
.check(["check_env"])
.unwrap()
.build(),
)
.root(SPrivileged::Privileged)
.bounding(SBounding::Ignore)
.authentication(SAuthentication::Skip)
.timeout(
STimeout::builder()
.type_field(TimestampType::PPID)
.duration(Duration::minutes(5))
.build(),
)
.build()
})
.build();
serde_json::to_string_pretty(&config).unwrap();
}
#[test]
fn test_serialize_operride_behavior_option() {
let config = SPolicy::builder()
.options(|opt| {
opt.env(
SEnvOptions::builder(EnvBehavior::Inherit)
.override_behavior(true)
.build(),
)
.build()
})
.build();
let config = serde_json::to_string(&config).unwrap();
assert_eq!(
config,
"{\"options\":{\"env\":{\"override_behavior\":true}}}"
);
}
}