]>
Commit | Line | Data |
---|---|---|
68ccdf09 DM |
1 | use std::collections::HashMap; |
2 | use std::sync::{Arc, RwLock}; | |
3 | ||
f7d4e4b5 | 4 | use anyhow::{bail, Error}; |
579728c6 | 5 | use lazy_static::lazy_static; |
579728c6 DM |
6 | use serde::{Serialize, Deserialize}; |
7 | use serde_json::json; | |
8 | ||
9 | use proxmox::api::{ | |
10 | api, | |
11 | schema::*, | |
12 | section_config::{ | |
13 | SectionConfig, | |
14 | SectionConfigData, | |
15 | SectionConfigPlugin, | |
16 | } | |
17 | }; | |
18 | ||
19 | use proxmox::tools::{fs::replace_file, fs::CreateOptions}; | |
20 | ||
21 | use crate::api2::types::*; | |
22 | ||
23 | lazy_static! { | |
24 | static ref CONFIG: SectionConfig = init(); | |
25 | } | |
26 | ||
27 | pub const ENABLE_USER_SCHEMA: Schema = BooleanSchema::new( | |
28 | "Enable the account (default). You can set this to '0' to disable the account.") | |
29 | .default(true) | |
30 | .schema(); | |
31 | ||
32 | pub const EXPIRE_USER_SCHEMA: Schema = IntegerSchema::new( | |
33 | "Account expiration date (seconds since epoch). '0' means no expiration date.") | |
34 | .default(0) | |
35 | .minimum(0) | |
36 | .schema(); | |
37 | ||
38 | pub const FIRST_NAME_SCHEMA: Schema = StringSchema::new("First name.") | |
39 | .format(&SINGLE_LINE_COMMENT_FORMAT) | |
40 | .min_length(2) | |
41 | .max_length(64) | |
42 | .schema(); | |
43 | ||
44 | pub const LAST_NAME_SCHEMA: Schema = StringSchema::new("Last name.") | |
45 | .format(&SINGLE_LINE_COMMENT_FORMAT) | |
46 | .min_length(2) | |
47 | .max_length(64) | |
48 | .schema(); | |
49 | ||
50 | pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.") | |
51 | .format(&SINGLE_LINE_COMMENT_FORMAT) | |
52 | .min_length(2) | |
53 | .max_length(64) | |
54 | .schema(); | |
55 | ||
56 | ||
57 | #[api( | |
58 | properties: { | |
59 | comment: { | |
60 | optional: true, | |
61 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
62 | }, | |
63 | enable: { | |
64 | optional: true, | |
65 | schema: ENABLE_USER_SCHEMA, | |
66 | }, | |
67 | expire: { | |
68 | optional: true, | |
69 | schema: EXPIRE_USER_SCHEMA, | |
70 | }, | |
71 | firstname: { | |
72 | optional: true, | |
73 | schema: FIRST_NAME_SCHEMA, | |
74 | }, | |
75 | lastname: { | |
76 | schema: LAST_NAME_SCHEMA, | |
77 | optional: true, | |
78 | }, | |
79 | email: { | |
80 | schema: EMAIL_SCHEMA, | |
81 | optional: true, | |
82 | }, | |
83 | } | |
84 | )] | |
85 | #[derive(Serialize,Deserialize)] | |
86 | /// User properties. | |
87 | pub struct User { | |
88 | #[serde(skip_serializing_if="Option::is_none")] | |
89 | pub comment: Option<String>, | |
90 | #[serde(skip_serializing_if="Option::is_none")] | |
91 | pub enable: Option<bool>, | |
92 | #[serde(skip_serializing_if="Option::is_none")] | |
93 | pub expire: Option<i64>, | |
94 | #[serde(skip_serializing_if="Option::is_none")] | |
95 | pub firstname: Option<String>, | |
96 | #[serde(skip_serializing_if="Option::is_none")] | |
97 | pub lastname: Option<String>, | |
98 | #[serde(skip_serializing_if="Option::is_none")] | |
99 | pub email: Option<String>, | |
100 | } | |
101 | ||
102 | fn init() -> SectionConfig { | |
103 | let obj_schema = match User::API_SCHEMA { | |
104 | Schema::Object(ref obj_schema) => obj_schema, | |
105 | _ => unreachable!(), | |
106 | }; | |
107 | ||
108 | let plugin = SectionConfigPlugin::new("user".to_string(), obj_schema); | |
109 | let mut config = SectionConfig::new(&PROXMOX_USER_ID_SCHEMA); | |
110 | ||
111 | config.register_plugin(plugin); | |
112 | ||
113 | config | |
114 | } | |
115 | ||
116 | pub const USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg"; | |
117 | pub const USER_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.user.lck"; | |
118 | ||
119 | pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { | |
120 | let content = match std::fs::read_to_string(USER_CFG_FILENAME) { | |
121 | Ok(c) => c, | |
122 | Err(err) => { | |
123 | if err.kind() == std::io::ErrorKind::NotFound { | |
124 | String::from("") | |
125 | } else { | |
126 | bail!("unable to read '{}' - {}", USER_CFG_FILENAME, err); | |
127 | } | |
128 | } | |
129 | }; | |
130 | ||
131 | let digest = openssl::sha::sha256(content.as_bytes()); | |
132 | let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?; | |
133 | ||
134 | if data.sections.get("root@pam").is_none() { | |
135 | let user: User = serde_json::from_value(json!({ | |
136 | "comment": "Superuser", | |
137 | })).unwrap(); | |
138 | data.set_data("root@pam", "user", &user).unwrap(); | |
139 | } | |
140 | ||
141 | Ok((data, digest)) | |
142 | } | |
143 | ||
109d7817 | 144 | pub fn cached_config() -> Result<Arc<SectionConfigData>, Error> { |
68ccdf09 DM |
145 | |
146 | struct ConfigCache { | |
109d7817 | 147 | data: Option<Arc<SectionConfigData>>, |
68ccdf09 DM |
148 | last_mtime: i64, |
149 | last_mtime_nsec: i64, | |
150 | } | |
151 | ||
152 | lazy_static! { | |
153 | static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new( | |
154 | ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 }); | |
155 | } | |
156 | ||
b9f2f761 DM |
157 | let stat = match nix::sys::stat::stat(USER_CFG_FILENAME) { |
158 | Ok(stat) => Some(stat), | |
159 | Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None, | |
160 | Err(err) => bail!("unable to stat '{}' - {}", USER_CFG_FILENAME, err), | |
161 | }; | |
68ccdf09 | 162 | |
b9f2f761 | 163 | if let Some(stat) = stat { |
68ccdf09 DM |
164 | let cache = CACHED_CONFIG.read().unwrap(); |
165 | if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec { | |
109d7817 DM |
166 | if let Some(ref config) = cache.data { |
167 | return Ok(config.clone()); | |
68ccdf09 DM |
168 | } |
169 | } | |
170 | } | |
171 | ||
109d7817 | 172 | let (config, _digest) = config()?; |
68ccdf09 DM |
173 | let config = Arc::new(config); |
174 | ||
175 | let mut cache = CACHED_CONFIG.write().unwrap(); | |
b9f2f761 DM |
176 | if let Some(stat) = stat { |
177 | cache.last_mtime = stat.st_mtime; | |
178 | cache.last_mtime_nsec = stat.st_mtime_nsec; | |
179 | } | |
109d7817 | 180 | cache.data = Some(config.clone()); |
68ccdf09 | 181 | |
109d7817 | 182 | Ok(config) |
68ccdf09 DM |
183 | } |
184 | ||
579728c6 DM |
185 | pub fn save_config(config: &SectionConfigData) -> Result<(), Error> { |
186 | let raw = CONFIG.write(USER_CFG_FILENAME, &config)?; | |
187 | ||
188 | let backup_user = crate::backup::backup_user()?; | |
189 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); | |
190 | // set the correct owner/group/permissions while saving file | |
191 | // owner(rw) = root, group(r)= backup | |
192 | let options = CreateOptions::new() | |
193 | .perm(mode) | |
194 | .owner(nix::unistd::ROOT) | |
195 | .group(backup_user.gid); | |
196 | ||
197 | replace_file(USER_CFG_FILENAME, raw.as_bytes(), options)?; | |
198 | ||
199 | Ok(()) | |
200 | } | |
201 | ||
202 | // shell completion helper | |
203 | pub fn complete_user_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
204 | match config() { | |
205 | Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(), | |
206 | Err(_) => return vec![], | |
207 | } | |
208 | } |