Skip to main content

isoloom_spec/syntax/step/
accounts.rs

1//! The steps that make accounts: `user` and `group`. A user's `password` in a step is a literal,
2//! lab content that lands in generated files: steps have no secrets.
3
4use std::borrow::Cow;
5use std::fmt;
6
7use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
8use serde::de::{self, IgnoredAny, MapAccess, Visitor};
9use serde::{Deserialize, Deserializer};
10
11use super::super::mistake::Mistake;
12use super::super::problem::Code;
13use super::words::{AbsPath, GroupName, UserName};
14
15/// A `user` step: a local user, created or brought in line, never deleted.
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
17#[serde(deny_unknown_fields)]
18pub struct UserStep {
19    /// Its name.
20    pub name: UserName,
21    /// Its user id (default: the next free one).
22    #[serde(default)]
23    pub uid: Option<u32>,
24    /// Its primary group (default: one of its own name, or the system's default).
25    #[serde(default)]
26    pub group: Option<GroupName>,
27    /// The other groups it is a member of (`[sudo]`, `[Remote Desktop Users]`).
28    #[serde(default)]
29    pub groups: Vec<GroupName>,
30    /// Its home folder (absolute; default the system's).
31    #[serde(default)]
32    pub home: Option<AbsPath>,
33    /// Its login shell (absolute; `/usr/sbin/nologin` for a service account).
34    #[serde(default)]
35    pub shell: Option<AbsPath>,
36    /// A system account (a low id, no ageing): a service's.
37    #[serde(default)]
38    pub system: bool,
39    /// Its password, a literal: lab content, written in the generated files (hashed on Linux).
40    /// A secret or an input is refused here: a catalogue entry's `users` setting takes those.
41    #[serde(default)]
42    pub password: Option<Password>,
43}
44
45/// A `group` step: a local group, created or brought in line, never deleted.
46#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct GroupStep {
49    /// Its name.
50    pub name: GroupName,
51    /// Its group id (default: the next free one).
52    #[serde(default)]
53    pub gid: Option<u32>,
54    /// A system group (a low id).
55    #[serde(default)]
56    pub system: bool,
57}
58
59/// A user's password in a step: a literal, which is lab content (it lands in generated files).
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub struct Password(String);
62
63impl Password {
64    /// The password as written.
65    #[must_use]
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69}
70
71impl<'de> Deserialize<'de> for Password {
72    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Password, D::Error> {
73        d.deserialize_any(PasswordVisitor)
74    }
75}
76
77/// A password as written: a text or a number, or a value source, which steps refuse.
78struct PasswordVisitor;
79
80impl<'de> Visitor<'de> for PasswordVisitor {
81    type Value = Password;
82
83    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
84        f.write_str("a password, as text")
85    }
86
87    fn visit_str<E: de::Error>(self, v: &str) -> Result<Password, E> {
88        if v.is_empty() {
89            return Err(E::custom("an empty password: leave `password` out for an account without one"));
90        }
91        Ok(Password(v.to_owned()))
92    }
93
94    fn visit_u64<E: de::Error>(self, v: u64) -> Result<Password, E> {
95        Ok(Password(v.to_string()))
96    }
97
98    fn visit_i64<E: de::Error>(self, v: i64) -> Result<Password, E> {
99        Ok(Password(v.to_string()))
100    }
101
102    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Password, A::Error> {
103        while map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {}
104        Err(de::Error::custom(
105            Mistake::new(
106                Code::SensitiveValue,
107                "steps have no secrets: a step's password is lab content, written in the generated files",
108            )
109            .fix("a literal (lab content), or a catalogue entry's `users` setting for a secret or an input"),
110        ))
111    }
112}
113
114impl JsonSchema for Password {
115    fn schema_name() -> Cow<'static, str> {
116        "Password".into()
117    }
118
119    fn inline_schema() -> bool {
120        true
121    }
122
123    fn json_schema(_: &mut SchemaGenerator) -> Schema {
124        json_schema!({ "type": ["string", "integer"], "minLength": 1 })
125    }
126}