evergreen/common/
auth.rs

1use crate as eg;
2use eg::common::settings::Settings;
3use eg::constants as C;
4use eg::date;
5use eg::osrf::cache::Cache;
6use eg::osrf::sclient::HostSettings;
7use eg::util;
8use eg::{Client, Editor, EgError, EgEvent, EgResult, EgValue};
9use md5;
10use std::fmt;
11
12const LOGIN_TIMEOUT: u64 = 30;
13
14// Default time for extending a persistent session: ten minutes
15const DEFAULT_RESET_INTERVAL: i32 = 10 * 60;
16
17fn cache_key(token: &str) -> String {
18    format!("{}{}", C::OILS_AUTH_CACHE_PRFX, token)
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum LoginType {
23    Temp,
24    Opac,
25    Staff,
26    Persist,
27}
28
29impl TryFrom<&str> for LoginType {
30    type Error = EgError;
31    fn try_from(s: &str) -> EgResult<LoginType> {
32        match s {
33            "opac" => Ok(Self::Opac),
34            "staff" => Ok(Self::Staff),
35            "persist" => Ok(Self::Persist),
36            "temp" => Ok(Self::Temp),
37            _ => Err(format!("Invalid login type: {s}. Using temp instead").into()),
38        }
39    }
40}
41
42impl From<&LoginType> for &str {
43    fn from(lt: &LoginType) -> &'static str {
44        match *lt {
45            LoginType::Temp => "temp",
46            LoginType::Opac => "opac",
47            LoginType::Staff => "staff",
48            LoginType::Persist => "persist",
49        }
50    }
51}
52
53impl fmt::Display for LoginType {
54    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
55        let s: &str = (self).into();
56        write!(f, "{}", s)
57    }
58}
59
60pub struct LoginArgs {
61    pub username: String,
62    pub password: String,
63    pub login_type: LoginType,
64    pub workstation: Option<String>,
65}
66
67impl LoginArgs {
68    /// # Examples
69    /// ```
70    /// use evergreen::common::auth;
71    /// let args = auth::LoginArgs::new(
72    ///   "my-staff-username",
73    ///   "my-staff-password",
74    ///   auth::LoginType::Staff,
75    ///   Some("branch1-circ-workstation")
76    /// );
77    /// assert_eq!(args.username(), "my-staff-username");
78    /// ```
79    pub fn new(
80        username: &str,
81        password: &str,
82        login_type: impl Into<LoginType>,
83        workstation: Option<&str>,
84    ) -> Self {
85        LoginArgs {
86            username: username.to_string(),
87            password: password.to_string(),
88            login_type: login_type.into(),
89            workstation: workstation.map(|w| w.to_string()),
90        }
91    }
92
93    pub fn username(&self) -> &str {
94        &self.username
95    }
96
97    pub fn password(&self) -> &str {
98        &self.password
99    }
100
101    pub fn login_type(&self) -> &LoginType {
102        &self.login_type
103    }
104
105    pub fn workstation(&self) -> Option<&str> {
106        self.workstation.as_deref()
107    }
108
109    pub fn to_eg_value(&self) -> EgValue {
110        let lt: &str = self.login_type().into();
111
112        let mut jv = eg::hash! {
113            username: self.username(),
114            password: self.password(),
115            "type": lt,
116        };
117
118        if let Some(w) = &self.workstation {
119            jv["workstation"] = EgValue::from(w.as_str());
120        }
121
122        jv
123    }
124}
125
126#[derive(Debug)]
127pub struct InternalLoginArgs {
128    pub user_id: i64,
129    pub org_unit: Option<i64>,
130    pub login_type: LoginType,
131    pub workstation: Option<String>,
132}
133
134impl InternalLoginArgs {
135    pub fn new(user_id: i64, login_type: impl Into<LoginType>) -> Self {
136        InternalLoginArgs {
137            user_id,
138            login_type: login_type.into(),
139            org_unit: None,
140            workstation: None,
141        }
142    }
143    pub fn set_org_unit(&mut self, org_unit: i64) {
144        self.org_unit = Some(org_unit);
145    }
146    pub fn set_workstation(&mut self, workstation: &str) {
147        self.workstation = Some(workstation.to_string());
148    }
149
150    pub fn to_eg_value(&self) -> EgValue {
151        let lt: &str = (&self.login_type).into();
152
153        let mut jv = eg::hash! {
154            "login_type": lt,
155            "user_id": self.user_id,
156        };
157
158        if let Some(w) = &self.workstation {
159            jv["workstation"] = EgValue::from(w.as_str());
160        }
161
162        if let Some(w) = self.org_unit {
163            jv["org_unit"] = EgValue::from(w);
164        }
165
166        jv
167    }
168}
169
170pub struct Session {
171    user: EgValue,
172
173    token: String,
174
175    /// Duration of the authentication session
176    authtime: u32,
177
178    /// Workstation name if applied
179    /// Sessions pulled from the cache will not have a Workstation
180    /// value, only "wsid" values.
181    workstation: Option<String>,
182
183    /// Epoch seconds where this session expires from cache.
184    /// Only applied to Persist sessions.
185    endtime: Option<i64>,
186
187    /// Amount to exist Persist sessions.
188    reset_interval: Option<i64>,
189}
190
191impl Session {
192    /// Get the auth session matching the provided auth token.
193    ///
194    /// Uses our internal cache, not the API.
195    pub fn from_cache(token: &str) -> EgResult<Option<Session>> {
196        let mut cache_val = match Cache::get_global(&cache_key(token))? {
197            Some(v) => v,
198            None => return Ok(None),
199        };
200
201        let authtime = cache_val["authtime"].int()? as u32;
202        let user = cache_val["userobj"].take();
203        let endtime = cache_val["endtime"].as_i64();
204        let reset_interval = cache_val["reset_interval"].as_i64();
205
206        let ses = Session {
207            user,
208            authtime,
209            endtime,
210            reset_interval,
211            workstation: None,
212            token: token.to_string(),
213        };
214
215        Ok(Some(ses))
216    }
217
218    pub fn remove(&self) -> EgResult<()> {
219        Cache::del_global(&cache_key(self.token()))
220    }
221
222    /// Logout and remove the cached auth session.
223    pub fn logout(client: &Client, token: &str) -> EgResult<()> {
224        let mut ses = client.session("open-ils.auth");
225        let mut req = ses.request("open-ils.auth.session.delete", token)?;
226        // We don't care so much about the response from logout,
227        // only that the call completed OK.
228        req.recv_with_timeout(LOGIN_TIMEOUT)?;
229        Ok(())
230    }
231
232    /// Login and acquire an authtoken.
233    ///
234    /// Returns None on login failure, Err on error.
235    pub fn login(client: &Client, args: &LoginArgs) -> EgResult<Option<Session>> {
236        let params = vec![args.to_eg_value()];
237        let mut ses = client.session("open-ils.auth");
238        let mut req = ses.request("open-ils.auth.login", params)?;
239
240        let eg_val = match req.recv_with_timeout(LOGIN_TIMEOUT)? {
241            Some(v) => v,
242            None => return Err("Login Timed Out".into()),
243        };
244
245        Session::handle_auth_response(&args.workstation, &eg_val)
246    }
247
248    /// Create an authtoken for an internal auth session via the API.
249    ///
250    /// Returns None on login failure, Err on error.
251    pub fn internal_session_api(
252        client: &Client,
253        args: &InternalLoginArgs,
254    ) -> EgResult<Option<Session>> {
255        let params = vec![args.to_eg_value()];
256        let mut ses = client.session("open-ils.auth_internal");
257        let mut req = ses.request("open-ils.auth_internal.session.create", params)?;
258
259        let eg_val = match req.recv_with_timeout(LOGIN_TIMEOUT)? {
260            Some(v) => v,
261            None => return Err("Login Timed Out".into()),
262        };
263
264        Session::handle_auth_response(&args.workstation, &eg_val)
265    }
266
267    fn handle_auth_response(
268        workstation: &Option<String>,
269        response: &EgValue,
270    ) -> EgResult<Option<Session>> {
271        let mut evt = match EgEvent::parse(response) {
272            Some(e) => e,
273            None => return Err(format!("Unexpected response: {:?}", response).into()),
274        };
275
276        if !evt.is_success() {
277            log::warn!("Login failed: {evt:?}");
278            return Ok(None);
279        }
280
281        if !evt.payload().is_object() {
282            return Err(format!("Unexpected response: {}", evt).into());
283        }
284
285        let token = evt.payload_mut()["authtoken"]
286            .take_string()
287            .ok_or_else(|| "Auth cache value has invalid authtoken".to_string())?;
288
289        let authtime = evt.payload()["authtime"].int()? as u32;
290        let user = evt.payload_mut()["userobj"].take();
291
292        let mut auth_ses = Session {
293            user,
294            token,
295            authtime,
296            workstation: None,
297            endtime: None,
298            reset_interval: None,
299        };
300
301        if let Some(w) = workstation {
302            auth_ses.workstation = Some(String::from(w));
303        }
304
305        Ok(Some(auth_ses))
306    }
307
308    /// Create an internal auth session and store it directly in the cache.
309    pub fn internal_session(editor: &mut Editor, args: &InternalLoginArgs) -> EgResult<Session> {
310        let mut user = editor
311            .retrieve("au", args.user_id)?
312            .ok_or_else(|| editor.die_event())?;
313
314        // Clear the (mostly dummy) password values.
315        user["passwd"].take();
316
317        if let Some(workstation) = args.workstation.as_deref() {
318            let mut ws = editor
319                .search("aws", eg::hash! {"name": workstation})?
320                .pop()
321                .ok_or_else(|| editor.die_event())?;
322
323            user["wsid"] = ws["id"].take();
324            user["ws_ou"] = ws["owning_lib"].take();
325        } else {
326            user["ws_ou"] = user["home_ou"].clone();
327        }
328
329        let org_id = match args.org_unit {
330            Some(id) => id,
331            None => user["ws_ou"].int()?,
332        };
333
334        let duration = get_auth_duration(editor, org_id, user["home_ou"].int()?, &args.login_type)?;
335
336        let authtoken = format!("{:x}", md5::compute(util::random_number(20)));
337
338        let mut cache_val = eg::hash! {
339            "authtime": duration,
340            "userobj": user.clone(),
341        };
342
343        if args.login_type == LoginType::Persist {
344            // Add entries for endtime and reset_interval, so that we can
345            // gracefully extend the session a bit if the user is active
346            // toward the end of the duration originally specified.
347            cache_val["endtime"] = EgValue::from(date::epoch_secs().floor() as u32 + duration);
348
349            // Reset interval is hard-coded for now, but if we ever want to make it
350            // configurable, this is the place to do it:
351            cache_val["reset_interval"] = DEFAULT_RESET_INTERVAL.into();
352        }
353
354        let endtime = cache_val["endtime"].as_int();
355        let reset_interval = cache_val["reset_interval"].as_int();
356
357        Cache::set_global_for(&cache_key(&authtoken), cache_val, duration)?;
358
359        let auth_ses = Session {
360            user,
361            token: authtoken,
362            authtime: duration,
363            endtime,
364            reset_interval,
365            workstation: args.workstation.clone(),
366        };
367
368        Ok(auth_ses)
369    }
370
371    pub fn token(&self) -> &str {
372        &self.token
373    }
374
375    pub fn authtime(&self) -> u32 {
376        self.authtime
377    }
378
379    pub fn workstation(&self) -> Option<&str> {
380        self.workstation.as_deref()
381    }
382
383    pub fn endtime(&self) -> Option<i64> {
384        self.endtime
385    }
386
387    pub fn reset_interval(&self) -> Option<i64> {
388        self.reset_interval
389    }
390    pub fn user(&self) -> &EgValue {
391        &self.user
392    }
393}
394
395/// Returns the auth session duration in seconds for the provided
396/// login type, context org unit(s), and host settings.
397pub fn get_auth_duration(
398    editor: &mut Editor,
399    org_id: i64,
400    user_home_ou: i64,
401    auth_type: &LoginType,
402) -> EgResult<u32> {
403    // First look for an org unit setting.
404
405    let setting_name = match auth_type {
406        LoginType::Opac => "auth.opac_timeout",
407        LoginType::Staff => "auth.staff_timeout",
408        LoginType::Temp => "auth.temp_timeout",
409        LoginType::Persist => "auth.persistent_login_interval",
410    };
411
412    let mut settings = Settings::new(editor);
413    settings.set_org_id(org_id);
414
415    let mut interval = settings.get_value(setting_name)?;
416
417    if interval.is_null() && user_home_ou != org_id {
418        // If the provided context org unit has no setting, see if
419        // a setting is applied to the user's home org unit.
420        settings.set_org_id(user_home_ou);
421        interval = settings.get_value(setting_name)?;
422    }
423
424    let interval_binding;
425    if interval.is_null() {
426        // No org unit setting.  Use the default.
427
428        let setkey = // TODO change key names?
429            format!("apps/open-ils.auth_internal/app_settings/default_timeout/{auth_type}");
430
431        interval_binding = HostSettings::get(&setkey)?.clone();
432        interval = &interval_binding;
433    }
434
435    if let Some(num) = interval.as_int() {
436        Ok(num as u32)
437    } else if let Some(s) = interval.as_str() {
438        date::interval_to_seconds(s).map(|n| n as u32)
439    } else {
440        Ok(0)
441    }
442}