Skip to content

Commit

Permalink
Merge pull request #92 from lucab/ups/sysusers-initial
Browse files Browse the repository at this point in the history
sysusers: introduce new module for 'sysusers.d' configuration
  • Loading branch information
lucab committed Nov 19, 2021
2 parents b063ec5 + 94a13c6 commit 869a1e3
Show file tree
Hide file tree
Showing 3 changed files with 356 additions and 8 deletions.
12 changes: 4 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,14 @@

/// Interfaces for socket-activated services.
pub mod activation;

/// Interfaces for systemd-aware daemons.
pub mod daemon;

/// Error handling.
pub mod errors;

/// Helpers for logging to systemd-journald.
/// APIs for processing 128-bits IDs.
pub mod id128;
/// Helpers for logging to `systemd-journald`.
pub mod logging;

pub mod sysusers;
/// Helpers for working with systemd units.
pub mod unit;

/// APIs for processing 128-bits IDs.
pub mod id128;
104 changes: 104 additions & 0 deletions src/sysusers/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use super::*;
use std::fmt::{self, Display};

impl Display for SysusersEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SysusersEntry::AddRange(v) => write!(f, "{}", v),
SysusersEntry::AddUserToGroup(v) => write!(f, "{}", v),
SysusersEntry::CreateGroup(v) => write!(f, "{}", v),
SysusersEntry::CreateUserAndGroup(v) => write!(f, "{}", v),
}
}
}

impl Display for AddRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "r - {}-{} - - -", self.from, self.to)
}
}

impl Display for AddUserToGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "m {} {} - - -", self.username, self.groupname,)
}
}

impl Display for CreateGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "g {} {} - - -", self.groupname, self.gid,)
}
}

impl Display for CreateUserAndGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"u {} {} \"{}\" {} {}",
self.name,
self.id,
self.gecos,
self.home_dir
.as_deref()
.map(|p| p.to_string_lossy())
.unwrap_or(Cow::Borrowed("-")),
self.shell
.as_deref()
.map(|p| p.to_string_lossy())
.unwrap_or(Cow::Borrowed("-")),
)
}
}

impl Display for IdOrPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IdOrPath::Id(i) => write!(f, "{}", i),
IdOrPath::UidGid((u, g)) => write!(f, "{}:{}", u, g),
IdOrPath::UidGroupname((u, g)) => write!(f, "{}:{}", u, g),
IdOrPath::Path(p) => write!(f, "{}", p.display()),
IdOrPath::Automatic => write!(f, "-",),
}
}
}

impl Display for GidOrPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GidOrPath::Gid(g) => write!(f, "{}", g),
GidOrPath::Path(p) => write!(f, "{}", p.display()),
GidOrPath::Automatic => write!(f, "-",),
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_formatters() {
{
let type_u =
CreateUserAndGroup::new("foo0".to_string(), "test".to_string(), None, None)
.unwrap();
let expected = r#"u foo0 - "test" - -"#;
assert_eq!(type_u.to_string(), expected);
}
{
let type_g = CreateGroup::new("foo1".to_string()).unwrap();
let expected = r#"g foo1 - - - -"#;
assert_eq!(type_g.to_string(), expected);
}
{
let type_r = AddRange::new(10, 20).unwrap();
let expected = r#"r - 10-20 - - -"#;
assert_eq!(type_r.to_string(), expected);
}
{
let type_m = AddUserToGroup::new("foo3".to_string(), "bar".to_string()).unwrap();
let expected = r#"m foo3 bar - - -"#;
assert_eq!(type_m.to_string(), expected);
}
}
}
248 changes: 248 additions & 0 deletions src/sysusers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
//! Helpers for working with `sysusers.d` configuration files.
//!
//! For the complete documentation see
//! <https://www.freedesktop.org/software/systemd/man/sysusers.d.html>.

use crate::errors::SdError;
use std::borrow::Cow;
use std::path::PathBuf;

mod format;

/// Single entry in `sysusers.d` configuration format.
#[derive(Clone, Debug, PartialEq)]
pub enum SysusersEntry {
AddRange(AddRange),
AddUserToGroup(AddUserToGroup),
CreateGroup(CreateGroup),
CreateUserAndGroup(CreateUserAndGroup),
}

/// Sysusers entry of type `r`.
#[derive(Clone, Debug, PartialEq)]
pub struct AddRange {
from: u32,
to: u32,
}

impl AddRange {
/// Create a new `AddRange` entry.
pub fn new(from: u32, to: u32) -> Result<Self, SdError> {
Ok(Self { from, to })
}
}

/// Sysusers entry of type `m`.
#[derive(Clone, Debug, PartialEq)]
pub struct AddUserToGroup {
username: String,
groupname: String,
}

impl AddUserToGroup {
/// Create a new `AddUserToGroup` entry.
pub fn new(username: String, groupname: String) -> Result<Self, SdError> {
validate_name_strict(&username)?;
validate_name_strict(&groupname)?;
Ok(Self {
username,
groupname,
})
}
}

/// Sysusers entry of type `g`.
#[derive(Clone, Debug, PartialEq)]
pub struct CreateGroup {
groupname: String,
gid: GidOrPath,
}

impl CreateGroup {
/// Create a new `CreateGroup` entry.
pub fn new(groupname: String) -> Result<Self, SdError> {
validate_name_strict(&groupname)?;
Ok(Self {
groupname,
gid: GidOrPath::Automatic,
})
}

/// Create a new `CreateGroup` entry, using a numeric ID.
pub fn new_with_gid(groupname: String, gid: u32) -> Result<Self, SdError> {
validate_name_strict(&groupname)?;
Ok(Self {
groupname,
gid: GidOrPath::Gid(gid),
})
}

/// Create a new `CreateGroup` entry, using a filepath reference.
pub fn new_with_path(groupname: String, path: PathBuf) -> Result<Self, SdError> {
validate_name_strict(&groupname)?;
Ok(Self {
groupname,
gid: GidOrPath::Path(path),
})
}
}

/// Sysusers entry of type `u`.
#[derive(Clone, Debug, PartialEq)]
pub struct CreateUserAndGroup {
name: String,
id: IdOrPath,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
}

impl CreateUserAndGroup {
/// Create a new `CreateUserAndGroup` entry, using a filepath reference.
pub fn new(
name: String,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Automatic)
}

/// Create a new `CreateUserAndrGroup` entry, using a numeric ID.
pub fn new_with_id(
name: String,
id: u32,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Id(id))
}

/// Create a new `CreateUserAndGroup` entry, using a UID and a GID.
pub fn new_with_uid_gid(
name: String,
uid: u32,
gid: u32,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::UidGid((uid, gid)))
}

/// Create a new `CreateUserAndGroup` entry, using a UID and a groupname.
pub fn new_with_uid_groupname(
name: String,
uid: u32,
groupname: String,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
validate_name_strict(&groupname)?;
Self::impl_new(
name,
gecos,
home_dir,
shell,
IdOrPath::UidGroupname((uid, groupname)),
)
}

/// Create a new `CreateUserAndGroup` entry, using a filepath reference.
pub fn new_with_path(
name: String,
path: PathBuf,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Path(path))
}

fn impl_new(
name: String,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
id: IdOrPath,
) -> Result<Self, SdError> {
validate_name_strict(&name)?;
Ok(Self {
name,
id,
gecos,
home_dir,
shell,
})
}
}

/// ID entity for `CreateUserAndGroup`.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum IdOrPath {
Id(u32),
UidGid((u32, u32)),
UidGroupname((u32, String)),
Path(PathBuf),
Automatic,
}

/// ID entity for `CreateGroup`.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum GidOrPath {
Gid(u32),
Path(PathBuf),
Automatic,
}

/// Validate a sysusers name in strict mode.
pub fn validate_name_strict(input: &str) -> Result<(), SdError> {
if input.is_empty() {
return Err(SdError::from("empty name"));
}

if input.len() > 31 {
let err_msg = format!(
"overlong sysusers name '{}' (more than 31 characters)",
input
);
return Err(SdError::from(err_msg));
}

for (index, ch) in input.char_indices() {
if index == 0 {
if !(ch.is_ascii_alphabetic() || ch == '_') {
let err_msg = format!(
"invalid starting character '{}' in sysusers name '{}'",
ch, input
);
return Err(SdError::from(err_msg));
}
} else if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') {
let err_msg = format!("invalid character '{}' in sysusers name '{}'", ch, input);
return Err(SdError::from(err_msg));
}
}

Ok(())
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_validate_name_strict() {
let err_cases = vec!["-foo", "10bar", "42"];
for entry in err_cases {
validate_name_strict(entry).unwrap_err();
}

let ok_cases = vec!["_authd", "httpd"];
for entry in ok_cases {
validate_name_strict(entry).unwrap();
}
}
}

0 comments on commit 869a1e3

Please sign in to comment.