mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
1039 lines
37 KiB
Rust
1039 lines
37 KiB
Rust
use hashbrown::HashSet;
|
|
use std::collections::BTreeSet;
|
|
use std::num::NonZeroUsize;
|
|
use std::ops::{Add, Sub};
|
|
use std::path::Path;
|
|
use std::string::ToString;
|
|
use std::time::{Duration, SystemTime};
|
|
|
|
use kanidm_client::{ClientError, KanidmClient};
|
|
use kanidm_proto::v1::{OperationError, UnixGroupToken, UnixUserToken};
|
|
use lru::LruCache;
|
|
use reqwest::StatusCode;
|
|
use tokio::sync::{Mutex, RwLock};
|
|
|
|
use crate::db::{Cache, CacheTxn, Db};
|
|
|
|
use crate::unix_config::{HomeAttr, UidAttr};
|
|
use crate::unix_proto::{HomeDirectoryInfo, NssGroup, NssUser};
|
|
|
|
// use crate::unix_passwd::{EtcUser, EtcGroup};
|
|
|
|
const NXCACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(2048) };
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub enum Id {
|
|
Name(String),
|
|
Gid(u32),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum CacheState {
|
|
Online,
|
|
Offline,
|
|
OfflineNextCheck(SystemTime),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Resolver {
|
|
db: Db,
|
|
client: RwLock<KanidmClient>,
|
|
state: Mutex<CacheState>,
|
|
pam_allow_groups: BTreeSet<String>,
|
|
timeout_seconds: u64,
|
|
default_shell: String,
|
|
home_prefix: String,
|
|
home_attr: HomeAttr,
|
|
home_alias: Option<HomeAttr>,
|
|
uid_attr_map: UidAttr,
|
|
gid_attr_map: UidAttr,
|
|
allow_id_overrides: HashSet<Id>,
|
|
nxset: Mutex<HashSet<Id>>,
|
|
nxcache: Mutex<LruCache<Id, SystemTime>>,
|
|
}
|
|
|
|
impl ToString for Id {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
Id::Name(s) => s.clone(),
|
|
Id::Gid(g) => g.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Resolver {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn new(
|
|
db: Db,
|
|
// need db path
|
|
// path: &str,
|
|
// tpm_policy: &TpmPolicy,
|
|
|
|
// cache timeout
|
|
timeout_seconds: u64,
|
|
//
|
|
client: KanidmClient,
|
|
pam_allow_groups: Vec<String>,
|
|
default_shell: String,
|
|
home_prefix: String,
|
|
home_attr: HomeAttr,
|
|
home_alias: Option<HomeAttr>,
|
|
uid_attr_map: UidAttr,
|
|
gid_attr_map: UidAttr,
|
|
allow_id_overrides: Vec<String>,
|
|
) -> Result<Self, ()> {
|
|
// setup and do a migrate.
|
|
{
|
|
let dbtxn = db.write().await;
|
|
dbtxn.migrate().map_err(|_| ())?;
|
|
dbtxn.commit().map_err(|_| ())?;
|
|
}
|
|
|
|
if pam_allow_groups.is_empty() {
|
|
eprintln!("Will not be able to authorise user logins, pam_allow_groups config is not configured.");
|
|
}
|
|
|
|
// We assume we are offline at start up, and we mark the next "online check" as
|
|
// being valid from "now".
|
|
Ok(Resolver {
|
|
db,
|
|
client: RwLock::new(client),
|
|
state: Mutex::new(CacheState::OfflineNextCheck(SystemTime::now())),
|
|
timeout_seconds,
|
|
pam_allow_groups: pam_allow_groups.into_iter().collect(),
|
|
default_shell,
|
|
home_prefix,
|
|
home_attr,
|
|
home_alias,
|
|
uid_attr_map,
|
|
gid_attr_map,
|
|
allow_id_overrides: allow_id_overrides.into_iter().map(Id::Name).collect(),
|
|
nxset: Mutex::new(HashSet::new()),
|
|
nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)),
|
|
})
|
|
}
|
|
|
|
async fn get_cachestate(&self) -> CacheState {
|
|
let g = self.state.lock().await;
|
|
(*g).clone()
|
|
}
|
|
|
|
async fn set_cachestate(&self, state: CacheState) {
|
|
let mut g = self.state.lock().await;
|
|
*g = state;
|
|
}
|
|
|
|
// Need a way to mark online/offline.
|
|
pub async fn attempt_online(&self) {
|
|
self.set_cachestate(CacheState::OfflineNextCheck(SystemTime::now()))
|
|
.await;
|
|
}
|
|
|
|
pub async fn mark_offline(&self) {
|
|
self.set_cachestate(CacheState::Offline).await;
|
|
}
|
|
|
|
pub async fn clear_cache(&self) -> Result<(), ()> {
|
|
let mut nxcache_txn = self.nxcache.lock().await;
|
|
nxcache_txn.clear();
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn.clear().and_then(|_| dbtxn.commit()).map_err(|_| ())
|
|
}
|
|
|
|
pub async fn invalidate(&self) -> Result<(), ()> {
|
|
let mut nxcache_txn = self.nxcache.lock().await;
|
|
nxcache_txn.clear();
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn
|
|
.invalidate()
|
|
.and_then(|_| dbtxn.commit())
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
async fn get_cached_usertokens(&self) -> Result<Vec<UnixUserToken>, ()> {
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn.get_accounts().map_err(|_| ())
|
|
}
|
|
|
|
async fn get_cached_grouptokens(&self) -> Result<Vec<UnixGroupToken>, ()> {
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn.get_groups().map_err(|_| ())
|
|
}
|
|
|
|
async fn set_nxcache(&self, id: &Id) {
|
|
let mut nxcache_txn = self.nxcache.lock().await;
|
|
let ex_time = SystemTime::now() + Duration::from_secs(self.timeout_seconds);
|
|
nxcache_txn.put(id.clone(), ex_time);
|
|
}
|
|
|
|
pub async fn check_nxcache(&self, id: &Id) -> Option<SystemTime> {
|
|
let mut nxcache_txn = self.nxcache.lock().await;
|
|
nxcache_txn.get(id).copied()
|
|
}
|
|
|
|
pub async fn reload_nxset(&self, iter: impl Iterator<Item = (String, u32)>) {
|
|
let mut nxset_txn = self.nxset.lock().await;
|
|
nxset_txn.clear();
|
|
for (name, gid) in iter {
|
|
let name = Id::Name(name);
|
|
let gid = Id::Gid(gid);
|
|
|
|
// Skip anything that the admin opted in to
|
|
if !(self.allow_id_overrides.contains(&gid) || self.allow_id_overrides.contains(&name))
|
|
{
|
|
debug!("Adding {:?}:{:?} to resolver exclusion set", name, gid);
|
|
nxset_txn.insert(name);
|
|
nxset_txn.insert(gid);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn check_nxset(&self, name: &str, idnumber: u32) -> bool {
|
|
let nxset_txn = self.nxset.lock().await;
|
|
nxset_txn.contains(&Id::Gid(idnumber)) || nxset_txn.contains(&Id::Name(name.to_string()))
|
|
}
|
|
|
|
async fn get_cached_usertoken(
|
|
&self,
|
|
account_id: &Id,
|
|
) -> Result<(bool, Option<UnixUserToken>), ()> {
|
|
// Account_id could be:
|
|
// * gidnumber
|
|
// * name
|
|
// * spn
|
|
// * uuid
|
|
// Attempt to search these in the db.
|
|
let dbtxn = self.db.write().await;
|
|
let r = dbtxn.get_account(account_id).map_err(|_| ())?;
|
|
|
|
match r {
|
|
Some((ut, ex)) => {
|
|
// Are we expired?
|
|
let offset = Duration::from_secs(ex);
|
|
let ex_time = SystemTime::UNIX_EPOCH + offset;
|
|
let now = SystemTime::now();
|
|
|
|
if now >= ex_time {
|
|
Ok((true, Some(ut)))
|
|
} else {
|
|
Ok((false, Some(ut)))
|
|
}
|
|
}
|
|
None => {
|
|
// it wasn't in the DB - lets see if it's in the nxcache.
|
|
match self.check_nxcache(account_id).await {
|
|
Some(ex_time) => {
|
|
let now = SystemTime::now();
|
|
if now >= ex_time {
|
|
// It's in the LRU, but we are past the expiry so
|
|
// lets attempt a refresh.
|
|
Ok((true, None))
|
|
} else {
|
|
// It's in the LRU and still valid, so return that
|
|
// no check is needed.
|
|
Ok((false, None))
|
|
}
|
|
}
|
|
None => {
|
|
// Not in the LRU. Return that this IS expired
|
|
// and we have no data.
|
|
Ok((true, None))
|
|
}
|
|
}
|
|
}
|
|
} // end match r
|
|
}
|
|
|
|
async fn get_cached_grouptoken(
|
|
&self,
|
|
grp_id: &Id,
|
|
) -> Result<(bool, Option<UnixGroupToken>), ()> {
|
|
// grp_id could be:
|
|
// * gidnumber
|
|
// * name
|
|
// * spn
|
|
// * uuid
|
|
// Attempt to search these in the db.
|
|
let dbtxn = self.db.write().await;
|
|
let r = dbtxn.get_group(grp_id).map_err(|_| ())?;
|
|
|
|
match r {
|
|
Some((ut, ex)) => {
|
|
// Are we expired?
|
|
let offset = Duration::from_secs(ex);
|
|
let ex_time = SystemTime::UNIX_EPOCH + offset;
|
|
let now = SystemTime::now();
|
|
|
|
if now >= ex_time {
|
|
Ok((true, Some(ut)))
|
|
} else {
|
|
Ok((false, Some(ut)))
|
|
}
|
|
}
|
|
None => {
|
|
// it wasn't in the DB - lets see if it's in the nxcache.
|
|
match self.check_nxcache(grp_id).await {
|
|
Some(ex_time) => {
|
|
let now = SystemTime::now();
|
|
if now >= ex_time {
|
|
// It's in the LRU, but we are past the expiry so
|
|
// lets attempt a refresh.
|
|
Ok((true, None))
|
|
} else {
|
|
// It's in the LRU and still valid, so return that
|
|
// no check is needed.
|
|
Ok((false, None))
|
|
}
|
|
}
|
|
None => {
|
|
// Not in the LRU. Return that this IS expired
|
|
// and we have no data.
|
|
Ok((true, None))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn set_cache_usertoken(&self, token: &mut UnixUserToken) -> Result<(), ()> {
|
|
// Set an expiry
|
|
let ex_time = SystemTime::now() + Duration::from_secs(self.timeout_seconds);
|
|
let offset = ex_time
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.map_err(|e| {
|
|
error!("time conversion error - ex_time less than epoch? {:?}", e);
|
|
})?;
|
|
|
|
// Check if requested `shell` exists on the system, else use `default_shell`
|
|
let requested_shell_exists: bool = token
|
|
.shell
|
|
.as_ref()
|
|
.map(|shell| {
|
|
let exists = Path::new(shell).exists();
|
|
if !exists {
|
|
info!(
|
|
"User requested shell is not present on this system - {}",
|
|
shell
|
|
)
|
|
}
|
|
exists
|
|
})
|
|
.unwrap_or_else(|| {
|
|
info!("User has not specified a shell, using default");
|
|
false
|
|
});
|
|
|
|
if !requested_shell_exists {
|
|
token.shell = Some(self.default_shell.clone())
|
|
}
|
|
|
|
// Filter out groups that are in the nxset
|
|
{
|
|
let nxset_txn = self.nxset.lock().await;
|
|
token.groups.retain(|g| {
|
|
!(nxset_txn.contains(&Id::Gid(g.gidnumber))
|
|
|| nxset_txn.contains(&Id::Name(g.name.clone())))
|
|
});
|
|
}
|
|
|
|
let dbtxn = self.db.write().await;
|
|
token
|
|
.groups
|
|
.iter()
|
|
// We need to add the groups first
|
|
.try_for_each(|g| dbtxn.update_group(g, offset.as_secs()))
|
|
.and_then(|_|
|
|
// So that when we add the account it can make the relationships.
|
|
dbtxn
|
|
.update_account(token, offset.as_secs()))
|
|
.and_then(|_| dbtxn.commit())
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
async fn set_cache_grouptoken(&self, token: &UnixGroupToken) -> Result<(), ()> {
|
|
// Set an expiry
|
|
let ex_time = SystemTime::now() + Duration::from_secs(self.timeout_seconds);
|
|
let offset = ex_time
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.map_err(|e| {
|
|
error!("time conversion error - ex_time less than epoch? {:?}", e);
|
|
})?;
|
|
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn
|
|
.update_group(token, offset.as_secs())
|
|
.and_then(|_| dbtxn.commit())
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
async fn delete_cache_usertoken(&self, a_uuid: &str) -> Result<(), ()> {
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn
|
|
.delete_account(a_uuid)
|
|
.and_then(|_| dbtxn.commit())
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
async fn delete_cache_grouptoken(&self, g_uuid: &str) -> Result<(), ()> {
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn
|
|
.delete_group(g_uuid)
|
|
.and_then(|_| dbtxn.commit())
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
async fn set_cache_userpassword(&self, a_uuid: &str, cred: &str) -> Result<(), ()> {
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn
|
|
.update_account_password(a_uuid, cred)
|
|
.and_then(|x| dbtxn.commit().map(|_| x))
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
async fn check_cache_userpassword(&self, a_uuid: &str, cred: &str) -> Result<bool, ()> {
|
|
let dbtxn = self.db.write().await;
|
|
dbtxn
|
|
.check_account_password(a_uuid, cred)
|
|
.and_then(|x| dbtxn.commit().map(|_| x))
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
async fn refresh_usertoken(
|
|
&self,
|
|
account_id: &Id,
|
|
token: Option<UnixUserToken>,
|
|
) -> Result<Option<UnixUserToken>, ()> {
|
|
match self
|
|
.client
|
|
.read()
|
|
.await
|
|
.idm_account_unix_token_get(account_id.to_string().as_str())
|
|
.await
|
|
{
|
|
Ok(mut n_tok) => {
|
|
if self.check_nxset(&n_tok.name, n_tok.gidnumber).await {
|
|
// Refuse to release the token, it's in the denied set.
|
|
self.delete_cache_usertoken(&n_tok.uuid).await?;
|
|
Ok(None)
|
|
} else {
|
|
// We have the token!
|
|
self.set_cache_usertoken(&mut n_tok).await?;
|
|
Ok(Some(n_tok))
|
|
}
|
|
}
|
|
Err(e) => {
|
|
match e {
|
|
ClientError::Transport(er) => {
|
|
error!("transport error, moving to offline -> {:?}", er);
|
|
// Something went wrong, mark offline.
|
|
let time = SystemTime::now().add(Duration::from_secs(15));
|
|
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
|
.await;
|
|
Ok(token)
|
|
}
|
|
ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid) => {
|
|
match reason {
|
|
Some(OperationError::NotAuthenticated) =>
|
|
warn!("session not authenticated - attempting reauthentication - eventid {}", opid),
|
|
Some(OperationError::SessionExpired) =>
|
|
warn!("session expired - attempting reauthentication - eventid {}", opid),
|
|
e =>
|
|
error!(
|
|
"authentication error {:?}, moving to offline - eventid {}", e, opid
|
|
),
|
|
};
|
|
// Something went wrong, mark offline to force a re-auth ASAP.
|
|
let time = SystemTime::now().sub(Duration::from_secs(1));
|
|
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
|
.await;
|
|
Ok(token)
|
|
}
|
|
ClientError::Http(
|
|
StatusCode::BAD_REQUEST,
|
|
Some(OperationError::NoMatchingEntries),
|
|
opid,
|
|
)
|
|
| ClientError::Http(
|
|
StatusCode::NOT_FOUND,
|
|
Some(OperationError::NoMatchingEntries),
|
|
opid,
|
|
)
|
|
| ClientError::Http(
|
|
StatusCode::BAD_REQUEST,
|
|
Some(OperationError::InvalidAccountState(_)),
|
|
opid,
|
|
) => {
|
|
// We wele able to contact the server but the entry has been removed, or
|
|
// is not longer a valid posix account.
|
|
debug!("entry has been removed or is no longer a valid posix account, clearing from cache ... - eventid {}", opid);
|
|
if let Some(tok) = token {
|
|
self.delete_cache_usertoken(&tok.uuid).await?;
|
|
};
|
|
// Cache the NX here.
|
|
self.set_nxcache(account_id).await;
|
|
|
|
Ok(None)
|
|
}
|
|
er => {
|
|
error!("client error -> {:?}", er);
|
|
// Some other transient error, continue with the token.
|
|
Ok(token)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn refresh_grouptoken(
|
|
&self,
|
|
grp_id: &Id,
|
|
token: Option<UnixGroupToken>,
|
|
) -> Result<Option<UnixGroupToken>, ()> {
|
|
match self
|
|
.client
|
|
.read()
|
|
.await
|
|
.idm_group_unix_token_get(grp_id.to_string().as_str())
|
|
.await
|
|
{
|
|
Ok(n_tok) => {
|
|
if self.check_nxset(&n_tok.name, n_tok.gidnumber).await {
|
|
// Refuse to release the token, it's in the denied set.
|
|
self.delete_cache_grouptoken(&n_tok.uuid).await?;
|
|
Ok(None)
|
|
} else {
|
|
// We have the token!
|
|
self.set_cache_grouptoken(&n_tok).await?;
|
|
Ok(Some(n_tok))
|
|
}
|
|
}
|
|
Err(e) => {
|
|
match e {
|
|
ClientError::Transport(er) => {
|
|
error!("transport error, moving to offline -> {:?}", er);
|
|
// Something went wrong, mark offline.
|
|
let time = SystemTime::now().add(Duration::from_secs(15));
|
|
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
|
.await;
|
|
Ok(token)
|
|
}
|
|
ClientError::Http(
|
|
StatusCode::UNAUTHORIZED,
|
|
Some(OperationError::NotAuthenticated),
|
|
opid,
|
|
) => {
|
|
error!(
|
|
"transport unauthenticated, moving to offline - eventid {}",
|
|
opid
|
|
);
|
|
// Something went wrong, mark offline.
|
|
let time = SystemTime::now().add(Duration::from_secs(15));
|
|
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
|
.await;
|
|
Ok(token)
|
|
}
|
|
ClientError::Http(
|
|
StatusCode::BAD_REQUEST,
|
|
Some(OperationError::NoMatchingEntries),
|
|
opid,
|
|
)
|
|
| ClientError::Http(
|
|
StatusCode::NOT_FOUND,
|
|
Some(OperationError::NoMatchingEntries),
|
|
opid,
|
|
)
|
|
| ClientError::Http(
|
|
StatusCode::BAD_REQUEST,
|
|
Some(OperationError::InvalidAccountState(_)),
|
|
opid,
|
|
) => {
|
|
debug!("entry has been removed or is no longer a valid posix group, clearing from cache ... - eventid {}", opid);
|
|
if let Some(tok) = token {
|
|
self.delete_cache_grouptoken(&tok.uuid).await?;
|
|
};
|
|
// Cache the NX here.
|
|
self.set_nxcache(grp_id).await;
|
|
|
|
Ok(None)
|
|
}
|
|
er => {
|
|
error!("client error -> {:?}", er);
|
|
// Some other transient error, continue with the token.
|
|
Ok(token)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn get_usertoken(&self, account_id: Id) -> Result<Option<UnixUserToken>, ()> {
|
|
debug!("get_usertoken");
|
|
// get the item from the cache
|
|
let (expired, item) = self.get_cached_usertoken(&account_id).await.map_err(|e| {
|
|
debug!("get_usertoken error -> {:?}", e);
|
|
})?;
|
|
|
|
let state = self.get_cachestate().await;
|
|
|
|
match (expired, state) {
|
|
(_, CacheState::Offline) => {
|
|
debug!("offline, returning cached item");
|
|
Ok(item)
|
|
}
|
|
(false, CacheState::OfflineNextCheck(time)) => {
|
|
debug!(
|
|
"offline valid, next check {:?}, returning cached item",
|
|
time
|
|
);
|
|
// Still valid within lifetime, return.
|
|
Ok(item)
|
|
}
|
|
(false, CacheState::Online) => {
|
|
debug!("online valid, returning cached item");
|
|
// Still valid within lifetime, return.
|
|
Ok(item)
|
|
}
|
|
(true, CacheState::OfflineNextCheck(time)) => {
|
|
debug!("offline expired, next check {:?}, refresh cache", time);
|
|
// Attempt to refresh the item
|
|
// Return it.
|
|
if SystemTime::now() >= time && self.test_connection().await {
|
|
// We brought ourselves online, lets go
|
|
self.refresh_usertoken(&account_id, item).await
|
|
} else {
|
|
// Unable to bring up connection, return cache.
|
|
Ok(item)
|
|
}
|
|
}
|
|
(true, CacheState::Online) => {
|
|
debug!("online expired, refresh cache");
|
|
// Attempt to refresh the item
|
|
// Return it.
|
|
self.refresh_usertoken(&account_id, item).await
|
|
}
|
|
}
|
|
.map(|t| {
|
|
debug!("token -> {:?}", t);
|
|
t
|
|
})
|
|
}
|
|
|
|
async fn get_grouptoken(&self, grp_id: Id) -> Result<Option<UnixGroupToken>, ()> {
|
|
debug!("get_grouptoken");
|
|
let (expired, item) = self.get_cached_grouptoken(&grp_id).await.map_err(|e| {
|
|
debug!("get_grouptoken error -> {:?}", e);
|
|
})?;
|
|
|
|
let state = self.get_cachestate().await;
|
|
|
|
match (expired, state) {
|
|
(_, CacheState::Offline) => {
|
|
debug!("offline, returning cached item");
|
|
Ok(item)
|
|
}
|
|
(false, CacheState::OfflineNextCheck(time)) => {
|
|
debug!(
|
|
"offline valid, next check {:?}, returning cached item",
|
|
time
|
|
);
|
|
// Still valid within lifetime, return.
|
|
Ok(item)
|
|
}
|
|
(false, CacheState::Online) => {
|
|
debug!("online valid, returning cached item");
|
|
// Still valid within lifetime, return.
|
|
Ok(item)
|
|
}
|
|
(true, CacheState::OfflineNextCheck(time)) => {
|
|
debug!("offline expired, next check {:?}, refresh cache", time);
|
|
// Attempt to refresh the item
|
|
// Return it.
|
|
if SystemTime::now() >= time && self.test_connection().await {
|
|
// We brought ourselves online, lets go
|
|
self.refresh_grouptoken(&grp_id, item).await
|
|
} else {
|
|
// Unable to bring up connection, return cache.
|
|
Ok(item)
|
|
}
|
|
}
|
|
(true, CacheState::Online) => {
|
|
debug!("online expired, refresh cache");
|
|
// Attempt to refresh the item
|
|
// Return it.
|
|
self.refresh_grouptoken(&grp_id, item).await
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn get_groupmembers(&self, uuid: &str) -> Vec<String> {
|
|
let dbtxn = self.db.write().await;
|
|
|
|
dbtxn
|
|
.get_group_members(uuid)
|
|
.unwrap_or_else(|_| Vec::new())
|
|
.into_iter()
|
|
.map(|ut| self.token_uidattr(&ut))
|
|
.collect()
|
|
}
|
|
|
|
// Get ssh keys for an account id
|
|
pub async fn get_sshkeys(&self, account_id: &str) -> Result<Vec<String>, ()> {
|
|
let token = self.get_usertoken(Id::Name(account_id.to_string())).await?;
|
|
Ok(token
|
|
.map(|t| {
|
|
// Only return keys if the account is valid
|
|
if t.valid {
|
|
t.sshkeys
|
|
} else {
|
|
Vec::with_capacity(0)
|
|
}
|
|
})
|
|
.unwrap_or_else(|| Vec::with_capacity(0)))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn token_homedirectory_alias(&self, token: &UnixUserToken) -> Option<String> {
|
|
self.home_alias.map(|t| match t {
|
|
// If we have an alias. use it.
|
|
HomeAttr::Uuid => token.uuid.as_str().to_string(),
|
|
HomeAttr::Spn => token.spn.as_str().to_string(),
|
|
HomeAttr::Name => token.name.as_str().to_string(),
|
|
})
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn token_homedirectory_attr(&self, token: &UnixUserToken) -> String {
|
|
match self.home_attr {
|
|
HomeAttr::Uuid => token.uuid.as_str().to_string(),
|
|
HomeAttr::Spn => token.spn.as_str().to_string(),
|
|
HomeAttr::Name => token.name.as_str().to_string(),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn token_homedirectory(&self, token: &UnixUserToken) -> String {
|
|
self.token_homedirectory_alias(token)
|
|
.unwrap_or_else(|| self.token_homedirectory_attr(token))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn token_abs_homedirectory(&self, token: &UnixUserToken) -> String {
|
|
format!("{}{}", self.home_prefix, self.token_homedirectory(token))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn token_uidattr(&self, token: &UnixUserToken) -> String {
|
|
match self.uid_attr_map {
|
|
UidAttr::Spn => token.spn.as_str(),
|
|
UidAttr::Name => token.name.as_str(),
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
pub async fn get_nssaccounts(&self) -> Result<Vec<NssUser>, ()> {
|
|
self.get_cached_usertokens().await.map(|l| {
|
|
l.into_iter()
|
|
.map(|tok| NssUser {
|
|
homedir: self.token_abs_homedirectory(&tok),
|
|
name: self.token_uidattr(&tok),
|
|
gid: tok.gidnumber,
|
|
gecos: tok.displayname,
|
|
shell: tok.shell.unwrap_or_else(|| self.default_shell.clone()),
|
|
})
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
async fn get_nssaccount(&self, account_id: Id) -> Result<Option<NssUser>, ()> {
|
|
let token = self.get_usertoken(account_id).await?;
|
|
Ok(token.map(|tok| NssUser {
|
|
homedir: self.token_abs_homedirectory(&tok),
|
|
name: self.token_uidattr(&tok),
|
|
gid: tok.gidnumber,
|
|
gecos: tok.displayname,
|
|
shell: tok.shell.unwrap_or_else(|| self.default_shell.clone()),
|
|
}))
|
|
}
|
|
|
|
pub async fn get_nssaccount_name(&self, account_id: &str) -> Result<Option<NssUser>, ()> {
|
|
self.get_nssaccount(Id::Name(account_id.to_string())).await
|
|
}
|
|
|
|
pub async fn get_nssaccount_gid(&self, gid: u32) -> Result<Option<NssUser>, ()> {
|
|
self.get_nssaccount(Id::Gid(gid)).await
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn token_gidattr(&self, token: &UnixGroupToken) -> String {
|
|
match self.gid_attr_map {
|
|
UidAttr::Spn => token.spn.as_str(),
|
|
UidAttr::Name => token.name.as_str(),
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
pub async fn get_nssgroups(&self) -> Result<Vec<NssGroup>, ()> {
|
|
let l = self.get_cached_grouptokens().await?;
|
|
let mut r: Vec<_> = Vec::with_capacity(l.len());
|
|
for tok in l.into_iter() {
|
|
let members = self.get_groupmembers(&tok.uuid).await;
|
|
r.push(NssGroup {
|
|
name: self.token_gidattr(&tok),
|
|
gid: tok.gidnumber,
|
|
members,
|
|
})
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
async fn get_nssgroup(&self, grp_id: Id) -> Result<Option<NssGroup>, ()> {
|
|
let token = self.get_grouptoken(grp_id).await?;
|
|
// Get members set.
|
|
match token {
|
|
Some(tok) => {
|
|
let members = self.get_groupmembers(&tok.uuid).await;
|
|
Ok(Some(NssGroup {
|
|
name: self.token_gidattr(&tok),
|
|
gid: tok.gidnumber,
|
|
members,
|
|
}))
|
|
}
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
pub async fn get_nssgroup_name(&self, grp_id: &str) -> Result<Option<NssGroup>, ()> {
|
|
self.get_nssgroup(Id::Name(grp_id.to_string())).await
|
|
}
|
|
|
|
pub async fn get_nssgroup_gid(&self, gid: u32) -> Result<Option<NssGroup>, ()> {
|
|
self.get_nssgroup(Id::Gid(gid)).await
|
|
}
|
|
|
|
async fn online_account_authenticate(
|
|
&self,
|
|
token: &Option<UnixUserToken>,
|
|
account_id: &str,
|
|
cred: &str,
|
|
) -> Result<Option<bool>, ()> {
|
|
debug!("Attempt online password check");
|
|
// We are online, attempt the pw to the server.
|
|
match self
|
|
.client
|
|
.read()
|
|
.await
|
|
.idm_account_unix_cred_verify(account_id, cred)
|
|
.await
|
|
{
|
|
Ok(Some(mut n_tok)) => {
|
|
if self.check_nxset(&n_tok.name, n_tok.gidnumber).await {
|
|
// Refuse to release the token, it's in the denied set.
|
|
self.delete_cache_usertoken(&n_tok.uuid).await?;
|
|
Ok(None)
|
|
} else {
|
|
debug!("online password check success.");
|
|
self.set_cache_usertoken(&mut n_tok).await?;
|
|
self.set_cache_userpassword(&n_tok.uuid, cred).await?;
|
|
Ok(Some(true))
|
|
}
|
|
}
|
|
Ok(None) => {
|
|
error!("incorrect password");
|
|
// PW failed the check.
|
|
Ok(Some(false))
|
|
}
|
|
Err(e) => {
|
|
match e {
|
|
ClientError::Transport(er) => {
|
|
error!("transport error, moving to offline -> {:?}", er);
|
|
// Something went wrong, mark offline.
|
|
let time = SystemTime::now().add(Duration::from_secs(15));
|
|
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
|
.await;
|
|
match token.as_ref() {
|
|
Some(t) => self.check_cache_userpassword(&t.uuid, cred).await.map(Some),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid) => {
|
|
match reason {
|
|
Some(OperationError::NotAuthenticated) =>
|
|
warn!("session not authenticated - attempting reauthentication - eventid {}", opid),
|
|
Some(OperationError::SessionExpired) =>
|
|
warn!("session expired - attempting reauthentication - eventid {}", opid),
|
|
e =>
|
|
error!(
|
|
"authentication error {:?}, moving to offline - eventid {}", e, opid
|
|
),
|
|
};
|
|
// Something went wrong, mark offline to force a re-auth ASAP.
|
|
let time = SystemTime::now().sub(Duration::from_secs(1));
|
|
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
|
.await;
|
|
match token.as_ref() {
|
|
Some(t) => self.check_cache_userpassword(&t.uuid, cred).await.map(Some),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
ClientError::Http(
|
|
StatusCode::BAD_REQUEST,
|
|
Some(OperationError::NoMatchingEntries),
|
|
opid,
|
|
)
|
|
| ClientError::Http(
|
|
StatusCode::NOT_FOUND,
|
|
Some(OperationError::NoMatchingEntries),
|
|
opid,
|
|
)
|
|
| ClientError::Http(
|
|
StatusCode::BAD_REQUEST,
|
|
Some(OperationError::InvalidAccountState(_)),
|
|
opid,
|
|
) => {
|
|
error!(
|
|
"unknown account or is not a valid posix account - eventid {}",
|
|
opid
|
|
);
|
|
Ok(None)
|
|
}
|
|
er => {
|
|
error!("client error -> {:?}", er);
|
|
// Some other unknown processing error?
|
|
Err(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn offline_account_authenticate(
|
|
&self,
|
|
token: &Option<UnixUserToken>,
|
|
cred: &str,
|
|
) -> Result<Option<bool>, ()> {
|
|
debug!("Attempt offline password check");
|
|
match token.as_ref() {
|
|
Some(t) => {
|
|
if t.valid {
|
|
self.check_cache_userpassword(&t.uuid, cred).await.map(Some)
|
|
} else {
|
|
Ok(Some(false))
|
|
}
|
|
}
|
|
None => Ok(None),
|
|
}
|
|
/*
|
|
token
|
|
.as_ref()
|
|
.map(async |t| self.check_cache_userpassword(&t.uuid, cred).await)
|
|
.transpose()
|
|
*/
|
|
}
|
|
|
|
pub async fn pam_account_allowed(&self, account_id: &str) -> Result<Option<bool>, ()> {
|
|
let token = self.get_usertoken(Id::Name(account_id.to_string())).await?;
|
|
|
|
if self.pam_allow_groups.is_empty() {
|
|
// can't allow anything if the group list is zero...
|
|
eprintln!("Cannot authenticate users, no allowed groups in configuration!");
|
|
Ok(Some(false))
|
|
} else {
|
|
Ok(token.map(|tok| {
|
|
let user_set: BTreeSet<_> = tok
|
|
.groups
|
|
.iter()
|
|
.flat_map(|g| [g.name.clone(), g.spn.clone(), g.uuid.clone()])
|
|
.collect();
|
|
|
|
debug!(
|
|
"Checking if user is in allowed groups ({:?}) -> {:?}",
|
|
self.pam_allow_groups, user_set,
|
|
);
|
|
let intersection_count = user_set.intersection(&self.pam_allow_groups).count();
|
|
debug!("Number of intersecting groups: {}", intersection_count);
|
|
debug!("User has valid token: {}", tok.valid);
|
|
|
|
intersection_count > 0 && tok.valid
|
|
}))
|
|
}
|
|
}
|
|
|
|
pub async fn pam_account_authenticate(
|
|
&self,
|
|
account_id: &str,
|
|
cred: &str,
|
|
) -> Result<Option<bool>, ()> {
|
|
let state = self.get_cachestate().await;
|
|
let (_expired, token) = self
|
|
.get_cached_usertoken(&Id::Name(account_id.to_string()))
|
|
.await?;
|
|
|
|
match state {
|
|
CacheState::Online => {
|
|
self.online_account_authenticate(&token, account_id, cred)
|
|
.await
|
|
}
|
|
CacheState::OfflineNextCheck(_time) => {
|
|
// Always attempt to go online to attempt the authentication.
|
|
if self.test_connection().await {
|
|
// Brought ourselves online, lets check.
|
|
self.online_account_authenticate(&token, account_id, cred)
|
|
.await
|
|
} else {
|
|
// We are offline, check from the cache if possible.
|
|
self.offline_account_authenticate(&token, cred).await
|
|
}
|
|
}
|
|
_ => {
|
|
// We are offline, check from the cache if possible.
|
|
self.offline_account_authenticate(&token, cred).await
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn pam_account_beginsession(
|
|
&self,
|
|
account_id: &str,
|
|
) -> Result<Option<HomeDirectoryInfo>, ()> {
|
|
let token = self.get_usertoken(Id::Name(account_id.to_string())).await?;
|
|
Ok(token.as_ref().map(|tok| HomeDirectoryInfo {
|
|
gid: tok.gidnumber,
|
|
name: self.token_homedirectory_attr(tok),
|
|
aliases: self
|
|
.token_homedirectory_alias(tok)
|
|
.map(|s| vec![s])
|
|
.unwrap_or_else(Vec::new),
|
|
}))
|
|
}
|
|
|
|
pub async fn test_connection(&self) -> bool {
|
|
let state = self.get_cachestate().await;
|
|
match state {
|
|
CacheState::Offline => {
|
|
debug!("Offline -> no change");
|
|
false
|
|
}
|
|
CacheState::OfflineNextCheck(_time) => {
|
|
match self.client.write().await.auth_anonymous().await {
|
|
Ok(_uat) => {
|
|
debug!("OfflineNextCheck -> authenticated");
|
|
self.set_cachestate(CacheState::Online).await;
|
|
true
|
|
}
|
|
Err(e) => {
|
|
debug!("OfflineNextCheck -> disconnected, staying offline. {:?}", e);
|
|
let time = SystemTime::now().add(Duration::from_secs(15));
|
|
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
|
.await;
|
|
false
|
|
}
|
|
}
|
|
}
|
|
CacheState::Online => {
|
|
debug!("Online, no change");
|
|
true
|
|
}
|
|
}
|
|
}
|
|
}
|