use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{DirBuilder, File};
use std::io::prelude::*;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tracing::{debug, info, trace, warn};
use crate::auth::{
authtoken::{AuthToken, AuthTokenScope},
AuthState,
};
#[derive(Clone, Default, Deserialize, Serialize, Debug)]
pub(crate) struct ScopeAuths(HashMap<AuthTokenScope, AuthToken>);
impl ScopeAuths {
fn filter_invalid_auths(&mut self) -> &mut Self {
self.0.retain(|_, v| AuthState::Valid == v.get_state(None));
self
}
fn find_valid_unscoped_auth(&self) -> Option<AuthToken> {
for (k, v) in self.0.iter() {
if let AuthTokenScope::Unscoped = k {
if let AuthState::Valid = v.get_state(None) {
return Some(v.clone());
}
}
}
None
}
fn find_first_valid_auth(&self) -> Option<AuthToken> {
for (_, v) in self.0.iter() {
if let AuthState::Valid = v.get_state(None) {
return Some(v.clone());
}
}
None
}
}
#[derive(Clone, Default, Deserialize, Serialize)]
pub(crate) struct State {
auth_state: ScopeAuths,
base_dir: PathBuf,
auth_hash: u64,
auth_cache_enabled: bool,
}
impl State {
pub fn new() -> Self {
let state = Self {
auth_hash: 0,
auth_state: Default::default(),
auth_cache_enabled: false,
base_dir: dirs::home_dir()
.unwrap_or_default()
.join(".osc"),
};
DirBuilder::new()
.recursive(true)
.create(state.base_dir.clone())
.ok();
state
}
pub fn set_auth_hash_key(&mut self, auth_hash: u64) -> &mut Self {
self.auth_hash = auth_hash;
self
}
pub fn enable_auth_cache(&mut self, val: bool) -> &mut Self {
self.auth_cache_enabled = val;
self
}
pub fn set_scope_auth(&mut self, scope: &AuthTokenScope, authz: &AuthToken) {
self.auth_state.filter_invalid_auths();
self.auth_state.0.insert(scope.clone(), authz.clone());
if self.auth_cache_enabled {
self.save_scope_auth_to_file(scope, authz);
}
}
pub fn get_scope_auth(&mut self, scope: &AuthTokenScope) -> Option<AuthToken> {
trace!("Get authz information for {:?}", scope);
self.auth_state.filter_invalid_auths();
match self.auth_state.0.get(scope) {
Some(authz) => Some(authz.clone()),
None => {
if let (Some(state), true) = (self.load_auth_state(None), self.auth_cache_enabled) {
if let Some((scope, authz)) = self.find_scope_authz(&state, scope) {
trace!("Found valid authz in the state file");
self.auth_state.0.insert(scope, authz.clone());
return Some(authz);
}
}
None
}
}
}
pub fn find_valid_auth(&self, state: &ScopeAuths) -> Option<AuthToken> {
if let Some(unscoped) = state.find_valid_unscoped_auth() {
return Some(unscoped);
}
if let Some(scoped) = state.find_first_valid_auth() {
return Some(scoped);
}
None
}
pub fn get_any_valid_auth(&mut self) -> Option<AuthToken> {
if let Some(auth) = self.find_valid_auth(&self.auth_state) {
return Some(auth);
}
if let (Some(state), true) = (self.load_auth_state(None), self.auth_cache_enabled) {
if let Some(auth) = self.find_valid_auth(&state) {
return Some(auth);
}
}
None
}
fn find_scope_authz(
&self,
state: &ScopeAuths,
scope: &AuthTokenScope,
) -> Option<(AuthTokenScope, AuthToken)> {
trace!("Searching requested scope authz in state");
for (k, v) in state.0.iter() {
trace!("Analyse known auth for scope {:?}", k);
match scope {
AuthTokenScope::Project(project) => {
if let AuthTokenScope::Project(cached) = k {
if project.id.is_some() && project.id == cached.id {
return Some((k.clone(), v.clone()));
} else if project.name == cached.name {
if let (Some(requested_domain), Some(state_domain)) =
(&project.domain, &cached.domain)
{
if (requested_domain.id.is_some()
&& requested_domain.id == state_domain.id)
|| (requested_domain.id.is_none()
&& requested_domain.name == state_domain.name)
{
return Some((k.clone(), v.clone()));
}
}
}
}
}
AuthTokenScope::Domain(domain) => {
if let AuthTokenScope::Domain(cached) = k {
if domain.id == cached.id
|| (domain.id.is_none() && domain.name == cached.name)
{
return Some((k.clone(), v.clone()));
}
}
}
AuthTokenScope::System(system) => {
if let AuthTokenScope::System(cached) = k {
if system.all == cached.all {
return Some((k.clone(), v.clone()));
}
}
}
AuthTokenScope::Unscoped => {
if let AuthTokenScope::Unscoped = k {
return Some((k.clone(), v.clone()));
}
}
}
}
None
}
fn get_auth_state_filename(&self, auth_hash: u64) -> PathBuf {
let mut fname_buf = self.base_dir.clone();
fname_buf.push(auth_hash.to_string());
fname_buf
}
fn load_auth_state(&self, path: Option<PathBuf>) -> Option<ScopeAuths> {
let fname = path.unwrap_or(self.get_auth_state_filename(self.auth_hash));
match File::open(fname.as_path()) {
Ok(mut file) => {
let mut contents = vec![];
match file.read_to_end(&mut contents) {
Ok(_) => match postcard::from_bytes::<ScopeAuths>(&contents) {
Ok(mut auth) => {
auth.filter_invalid_auths();
trace!("Cached Auth info: {:?}", auth);
Some(auth)
}
Err(x) => {
info!(
"Corrupted cache file `{}`: {:?}. Removing ",
fname.display(),
x
);
let _ = std::fs::remove_file(fname);
None
}
},
Err(e) => {
info!("Error reading file `{}`: {:?}", fname.display(), e);
None
}
}
}
Err(e) => {
debug!("Error opening file `{}`: {:?}", fname.display(), e);
None
}
}
}
pub fn save_scope_auth_to_file(&self, scope: &AuthTokenScope, data: &AuthToken) {
let fname = self.get_auth_state_filename(self.auth_hash);
let mut state = self
.load_auth_state(Some(fname.clone()))
.unwrap_or_default();
let _ = state.0.insert(scope.clone(), data.clone());
match File::create(fname.as_path()) {
Ok(mut file) => {
match file.metadata() {
Ok(metadata) => {
let mut permissions = metadata.permissions();
#[cfg(unix)]
{
permissions.set_mode(0o600);
let _ = file.set_permissions(permissions);
}
#[cfg(windows)]
{
permissions.set_readonly(true);
let _ = file.set_permissions(permissions);
}
}
Err(_) => {
warn!("Cannot set permissions for the cache file");
return;
}
}
if let Err(e) = postcard::to_io(&state, &mut file) {
warn!("Error serializing state: {:?}", e);
}
}
_ => {
warn!("Error writing state file");
}
}
}
}