]>
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 | 6 | use serde::{Serialize, Deserialize}; |
579728c6 DM |
7 | |
8 | use proxmox::api::{ | |
9 | api, | |
10 | schema::*, | |
11 | section_config::{ | |
12 | SectionConfig, | |
13 | SectionConfigData, | |
14 | SectionConfigPlugin, | |
15 | } | |
16 | }; | |
17 | ||
18 | use proxmox::tools::{fs::replace_file, fs::CreateOptions}; | |
19 | ||
20 | use crate::api2::types::*; | |
21 | ||
22 | lazy_static! { | |
23 | static ref CONFIG: SectionConfig = init(); | |
24 | } | |
25 | ||
26 | pub const ENABLE_USER_SCHEMA: Schema = BooleanSchema::new( | |
27 | "Enable the account (default). You can set this to '0' to disable the account.") | |
28 | .default(true) | |
29 | .schema(); | |
30 | ||
31 | pub const EXPIRE_USER_SCHEMA: Schema = IntegerSchema::new( | |
32 | "Account expiration date (seconds since epoch). '0' means no expiration date.") | |
33 | .default(0) | |
34 | .minimum(0) | |
35 | .schema(); | |
36 | ||
37 | pub const FIRST_NAME_SCHEMA: Schema = StringSchema::new("First name.") | |
38 | .format(&SINGLE_LINE_COMMENT_FORMAT) | |
39 | .min_length(2) | |
40 | .max_length(64) | |
41 | .schema(); | |
42 | ||
43 | pub const LAST_NAME_SCHEMA: Schema = StringSchema::new("Last name.") | |
44 | .format(&SINGLE_LINE_COMMENT_FORMAT) | |
45 | .min_length(2) | |
46 | .max_length(64) | |
47 | .schema(); | |
48 | ||
49 | pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.") | |
50 | .format(&SINGLE_LINE_COMMENT_FORMAT) | |
51 | .min_length(2) | |
52 | .max_length(64) | |
53 | .schema(); | |
54 | ||
e6dc35ac FG |
55 | #[api( |
56 | properties: { | |
57 | tokenid: { | |
58 | schema: PROXMOX_TOKEN_ID_SCHEMA, | |
59 | }, | |
60 | comment: { | |
61 | optional: true, | |
62 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
63 | }, | |
64 | enable: { | |
65 | optional: true, | |
66 | schema: ENABLE_USER_SCHEMA, | |
67 | }, | |
68 | expire: { | |
69 | optional: true, | |
70 | schema: EXPIRE_USER_SCHEMA, | |
71 | }, | |
72 | } | |
73 | )] | |
74 | #[derive(Serialize,Deserialize)] | |
75 | /// ApiToken properties. | |
76 | pub struct ApiToken { | |
77 | pub tokenid: Authid, | |
78 | #[serde(skip_serializing_if="Option::is_none")] | |
79 | pub comment: Option<String>, | |
80 | #[serde(skip_serializing_if="Option::is_none")] | |
81 | pub enable: Option<bool>, | |
82 | #[serde(skip_serializing_if="Option::is_none")] | |
83 | pub expire: Option<i64>, | |
84 | } | |
579728c6 DM |
85 | |
86 | #[api( | |
87 | properties: { | |
522c0da0 | 88 | userid: { |
e7cb4dc5 | 89 | type: Userid, |
522c0da0 | 90 | }, |
579728c6 DM |
91 | comment: { |
92 | optional: true, | |
93 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
94 | }, | |
95 | enable: { | |
96 | optional: true, | |
97 | schema: ENABLE_USER_SCHEMA, | |
98 | }, | |
99 | expire: { | |
100 | optional: true, | |
101 | schema: EXPIRE_USER_SCHEMA, | |
102 | }, | |
103 | firstname: { | |
104 | optional: true, | |
105 | schema: FIRST_NAME_SCHEMA, | |
106 | }, | |
107 | lastname: { | |
108 | schema: LAST_NAME_SCHEMA, | |
109 | optional: true, | |
110 | }, | |
111 | email: { | |
112 | schema: EMAIL_SCHEMA, | |
113 | optional: true, | |
114 | }, | |
115 | } | |
116 | )] | |
117 | #[derive(Serialize,Deserialize)] | |
118 | /// User properties. | |
119 | pub struct User { | |
e7cb4dc5 | 120 | pub userid: Userid, |
579728c6 DM |
121 | #[serde(skip_serializing_if="Option::is_none")] |
122 | pub comment: Option<String>, | |
123 | #[serde(skip_serializing_if="Option::is_none")] | |
124 | pub enable: Option<bool>, | |
125 | #[serde(skip_serializing_if="Option::is_none")] | |
126 | pub expire: Option<i64>, | |
127 | #[serde(skip_serializing_if="Option::is_none")] | |
128 | pub firstname: Option<String>, | |
129 | #[serde(skip_serializing_if="Option::is_none")] | |
130 | pub lastname: Option<String>, | |
131 | #[serde(skip_serializing_if="Option::is_none")] | |
132 | pub email: Option<String>, | |
133 | } | |
134 | ||
135 | fn init() -> SectionConfig { | |
e6dc35ac FG |
136 | let mut config = SectionConfig::new(&Authid::API_SCHEMA); |
137 | ||
138 | let user_schema = match User::API_SCHEMA { | |
139 | Schema::Object(ref user_schema) => user_schema, | |
579728c6 DM |
140 | _ => unreachable!(), |
141 | }; | |
e6dc35ac FG |
142 | let user_plugin = SectionConfigPlugin::new("user".to_string(), Some("userid".to_string()), user_schema); |
143 | config.register_plugin(user_plugin); | |
579728c6 | 144 | |
e6dc35ac FG |
145 | let token_schema = match ApiToken::API_SCHEMA { |
146 | Schema::Object(ref token_schema) => token_schema, | |
147 | _ => unreachable!(), | |
148 | }; | |
149 | let token_plugin = SectionConfigPlugin::new("token".to_string(), Some("tokenid".to_string()), token_schema); | |
150 | config.register_plugin(token_plugin); | |
579728c6 DM |
151 | |
152 | config | |
153 | } | |
154 | ||
155 | pub const USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg"; | |
156 | pub const USER_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.user.lck"; | |
157 | ||
158 | pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { | |
3eeba687 DM |
159 | |
160 | let content = proxmox::tools::fs::file_read_optional_string(USER_CFG_FILENAME)?; | |
161 | let content = content.unwrap_or(String::from("")); | |
579728c6 DM |
162 | |
163 | let digest = openssl::sha::sha256(content.as_bytes()); | |
164 | let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?; | |
165 | ||
166 | if data.sections.get("root@pam").is_none() { | |
9c5c383b | 167 | let user: User = User { |
e7cb4dc5 | 168 | userid: Userid::root_userid().clone(), |
9c5c383b DC |
169 | comment: Some("Superuser".to_string()), |
170 | enable: None, | |
171 | expire: None, | |
172 | firstname: None, | |
173 | lastname: None, | |
174 | email: None, | |
175 | }; | |
579728c6 DM |
176 | data.set_data("root@pam", "user", &user).unwrap(); |
177 | } | |
178 | ||
179 | Ok((data, digest)) | |
180 | } | |
181 | ||
109d7817 | 182 | pub fn cached_config() -> Result<Arc<SectionConfigData>, Error> { |
68ccdf09 DM |
183 | |
184 | struct ConfigCache { | |
109d7817 | 185 | data: Option<Arc<SectionConfigData>>, |
68ccdf09 DM |
186 | last_mtime: i64, |
187 | last_mtime_nsec: i64, | |
188 | } | |
189 | ||
190 | lazy_static! { | |
191 | static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new( | |
192 | ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 }); | |
193 | } | |
194 | ||
b9f2f761 DM |
195 | let stat = match nix::sys::stat::stat(USER_CFG_FILENAME) { |
196 | Ok(stat) => Some(stat), | |
197 | Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None, | |
198 | Err(err) => bail!("unable to stat '{}' - {}", USER_CFG_FILENAME, err), | |
199 | }; | |
68ccdf09 | 200 | |
bd88dc41 | 201 | { // limit scope |
68ccdf09 | 202 | let cache = CACHED_CONFIG.read().unwrap(); |
bd88dc41 DM |
203 | if let Some(ref config) = cache.data { |
204 | if let Some(stat) = stat { | |
205 | if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec { | |
206 | return Ok(config.clone()); | |
207 | } | |
208 | } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 { | |
109d7817 | 209 | return Ok(config.clone()); |
68ccdf09 DM |
210 | } |
211 | } | |
212 | } | |
213 | ||
109d7817 | 214 | let (config, _digest) = config()?; |
68ccdf09 DM |
215 | let config = Arc::new(config); |
216 | ||
217 | let mut cache = CACHED_CONFIG.write().unwrap(); | |
b9f2f761 DM |
218 | if let Some(stat) = stat { |
219 | cache.last_mtime = stat.st_mtime; | |
220 | cache.last_mtime_nsec = stat.st_mtime_nsec; | |
221 | } | |
109d7817 | 222 | cache.data = Some(config.clone()); |
68ccdf09 | 223 | |
109d7817 | 224 | Ok(config) |
68ccdf09 DM |
225 | } |
226 | ||
579728c6 DM |
227 | pub fn save_config(config: &SectionConfigData) -> Result<(), Error> { |
228 | let raw = CONFIG.write(USER_CFG_FILENAME, &config)?; | |
229 | ||
230 | let backup_user = crate::backup::backup_user()?; | |
231 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); | |
232 | // set the correct owner/group/permissions while saving file | |
233 | // owner(rw) = root, group(r)= backup | |
234 | let options = CreateOptions::new() | |
235 | .perm(mode) | |
236 | .owner(nix::unistd::ROOT) | |
237 | .group(backup_user.gid); | |
238 | ||
239 | replace_file(USER_CFG_FILENAME, raw.as_bytes(), options)?; | |
240 | ||
241 | Ok(()) | |
242 | } | |
243 | ||
244 | // shell completion helper | |
e6dc35ac | 245 | pub fn complete_userid(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { |
579728c6 | 246 | match config() { |
e6dc35ac FG |
247 | Ok((data, _digest)) => { |
248 | data.sections.iter() | |
249 | .filter_map(|(id, (section_type, _))| { | |
250 | if section_type == "user" { | |
251 | Some(id.to_string()) | |
252 | } else { | |
253 | None | |
254 | } | |
255 | }).collect() | |
256 | }, | |
579728c6 DM |
257 | Err(_) => return vec![], |
258 | } | |
259 | } | |
e6dc35ac FG |
260 | |
261 | // shell completion helper | |
262 | pub fn complete_authid(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
263 | match config() { | |
264 | Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(), | |
265 | Err(_) => vec![], | |
266 | } | |
267 | } | |
2156dec5 FG |
268 | |
269 | // shell completion helper | |
270 | pub fn complete_token_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> { | |
271 | let data = match config() { | |
272 | Ok((data, _digest)) => data, | |
273 | Err(_) => return Vec::new(), | |
274 | }; | |
275 | ||
276 | match param.get("userid") { | |
277 | Some(userid) => { | |
278 | let user = data.lookup::<User>("user", userid); | |
279 | let tokens = data.convert_to_typed_array("token"); | |
280 | match (user, tokens) { | |
281 | (Ok(_), Ok(tokens)) => { | |
282 | tokens | |
283 | .into_iter() | |
284 | .filter_map(|token: ApiToken| { | |
285 | let tokenid = token.tokenid; | |
286 | if tokenid.is_token() && tokenid.user() == userid { | |
287 | Some(tokenid.tokenname().unwrap().as_str().to_string()) | |
288 | } else { | |
289 | None | |
290 | } | |
291 | }).collect() | |
292 | }, | |
293 | _ => vec![], | |
294 | } | |
295 | }, | |
296 | None => vec![], | |
297 | } | |
298 | } |