]>
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 | ||
55 | ||
56 | #[api( | |
57 | properties: { | |
522c0da0 | 58 | userid: { |
e7cb4dc5 | 59 | type: Userid, |
522c0da0 | 60 | }, |
579728c6 DM |
61 | comment: { |
62 | optional: true, | |
63 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
64 | }, | |
65 | enable: { | |
66 | optional: true, | |
67 | schema: ENABLE_USER_SCHEMA, | |
68 | }, | |
69 | expire: { | |
70 | optional: true, | |
71 | schema: EXPIRE_USER_SCHEMA, | |
72 | }, | |
73 | firstname: { | |
74 | optional: true, | |
75 | schema: FIRST_NAME_SCHEMA, | |
76 | }, | |
77 | lastname: { | |
78 | schema: LAST_NAME_SCHEMA, | |
79 | optional: true, | |
80 | }, | |
81 | email: { | |
82 | schema: EMAIL_SCHEMA, | |
83 | optional: true, | |
84 | }, | |
85 | } | |
86 | )] | |
87 | #[derive(Serialize,Deserialize)] | |
88 | /// User properties. | |
89 | pub struct User { | |
e7cb4dc5 | 90 | pub userid: Userid, |
579728c6 DM |
91 | #[serde(skip_serializing_if="Option::is_none")] |
92 | pub comment: Option<String>, | |
93 | #[serde(skip_serializing_if="Option::is_none")] | |
94 | pub enable: Option<bool>, | |
95 | #[serde(skip_serializing_if="Option::is_none")] | |
96 | pub expire: Option<i64>, | |
97 | #[serde(skip_serializing_if="Option::is_none")] | |
98 | pub firstname: Option<String>, | |
99 | #[serde(skip_serializing_if="Option::is_none")] | |
100 | pub lastname: Option<String>, | |
101 | #[serde(skip_serializing_if="Option::is_none")] | |
102 | pub email: Option<String>, | |
103 | } | |
104 | ||
105 | fn init() -> SectionConfig { | |
106 | let obj_schema = match User::API_SCHEMA { | |
107 | Schema::Object(ref obj_schema) => obj_schema, | |
108 | _ => unreachable!(), | |
109 | }; | |
110 | ||
522c0da0 | 111 | let plugin = SectionConfigPlugin::new("user".to_string(), Some("userid".to_string()), obj_schema); |
e7cb4dc5 | 112 | let mut config = SectionConfig::new(&Userid::API_SCHEMA); |
579728c6 DM |
113 | |
114 | config.register_plugin(plugin); | |
115 | ||
116 | config | |
117 | } | |
118 | ||
119 | pub const USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg"; | |
120 | pub const USER_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.user.lck"; | |
121 | ||
122 | pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { | |
3eeba687 DM |
123 | |
124 | let content = proxmox::tools::fs::file_read_optional_string(USER_CFG_FILENAME)?; | |
125 | let content = content.unwrap_or(String::from("")); | |
579728c6 DM |
126 | |
127 | let digest = openssl::sha::sha256(content.as_bytes()); | |
128 | let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?; | |
129 | ||
130 | if data.sections.get("root@pam").is_none() { | |
9c5c383b | 131 | let user: User = User { |
e7cb4dc5 | 132 | userid: Userid::root_userid().clone(), |
9c5c383b DC |
133 | comment: Some("Superuser".to_string()), |
134 | enable: None, | |
135 | expire: None, | |
136 | firstname: None, | |
137 | lastname: None, | |
138 | email: None, | |
139 | }; | |
579728c6 DM |
140 | data.set_data("root@pam", "user", &user).unwrap(); |
141 | } | |
142 | ||
143 | Ok((data, digest)) | |
144 | } | |
145 | ||
109d7817 | 146 | pub fn cached_config() -> Result<Arc<SectionConfigData>, Error> { |
68ccdf09 DM |
147 | |
148 | struct ConfigCache { | |
109d7817 | 149 | data: Option<Arc<SectionConfigData>>, |
68ccdf09 DM |
150 | last_mtime: i64, |
151 | last_mtime_nsec: i64, | |
152 | } | |
153 | ||
154 | lazy_static! { | |
155 | static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new( | |
156 | ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 }); | |
157 | } | |
158 | ||
b9f2f761 DM |
159 | let stat = match nix::sys::stat::stat(USER_CFG_FILENAME) { |
160 | Ok(stat) => Some(stat), | |
161 | Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None, | |
162 | Err(err) => bail!("unable to stat '{}' - {}", USER_CFG_FILENAME, err), | |
163 | }; | |
68ccdf09 | 164 | |
bd88dc41 | 165 | { // limit scope |
68ccdf09 | 166 | let cache = CACHED_CONFIG.read().unwrap(); |
bd88dc41 DM |
167 | if let Some(ref config) = cache.data { |
168 | if let Some(stat) = stat { | |
169 | if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec { | |
170 | return Ok(config.clone()); | |
171 | } | |
172 | } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 { | |
109d7817 | 173 | return Ok(config.clone()); |
68ccdf09 DM |
174 | } |
175 | } | |
176 | } | |
177 | ||
109d7817 | 178 | let (config, _digest) = config()?; |
68ccdf09 DM |
179 | let config = Arc::new(config); |
180 | ||
181 | let mut cache = CACHED_CONFIG.write().unwrap(); | |
b9f2f761 DM |
182 | if let Some(stat) = stat { |
183 | cache.last_mtime = stat.st_mtime; | |
184 | cache.last_mtime_nsec = stat.st_mtime_nsec; | |
185 | } | |
109d7817 | 186 | cache.data = Some(config.clone()); |
68ccdf09 | 187 | |
109d7817 | 188 | Ok(config) |
68ccdf09 DM |
189 | } |
190 | ||
579728c6 DM |
191 | pub fn save_config(config: &SectionConfigData) -> Result<(), Error> { |
192 | let raw = CONFIG.write(USER_CFG_FILENAME, &config)?; | |
193 | ||
194 | let backup_user = crate::backup::backup_user()?; | |
195 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); | |
196 | // set the correct owner/group/permissions while saving file | |
197 | // owner(rw) = root, group(r)= backup | |
198 | let options = CreateOptions::new() | |
199 | .perm(mode) | |
200 | .owner(nix::unistd::ROOT) | |
201 | .group(backup_user.gid); | |
202 | ||
203 | replace_file(USER_CFG_FILENAME, raw.as_bytes(), options)?; | |
204 | ||
205 | Ok(()) | |
206 | } | |
207 | ||
208 | // shell completion helper | |
209 | pub fn complete_user_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
210 | match config() { | |
211 | Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(), | |
212 | Err(_) => return vec![], | |
213 | } | |
214 | } |