Authentication (#37)

* Initial authentication implementation

* Store user info in the database, improve encapsulation in profiles

* Add user list, remove unused dependencies, add spantraces

* Implement user remove, update UUID crate

* Add user set-default

* Revert submodule macro usage

* Make tracing significantly less verbose
This commit is contained in:
Danielle
2022-07-15 15:39:38 +00:00
committed by GitHub
parent 53948c7a5e
commit b223dc7cba
27 changed files with 1490 additions and 851 deletions

View File

@@ -10,10 +10,11 @@ pub struct DirectoryInfo {
impl DirectoryInfo {
/// Get all paths needed for Theseus to operate properly
#[tracing::instrument]
pub async fn init() -> crate::Result<Self> {
// Working directory
let working_dir = std::env::current_dir().map_err(|err| {
crate::Error::FSError(format!(
crate::ErrorKind::FSError(format!(
"Could not open working directory: {err}"
))
})?;
@@ -21,12 +22,12 @@ impl DirectoryInfo {
// Config directory
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
.or_else(|| Some(dirs::config_dir()?.join("theseus")))
.ok_or(crate::Error::FSError(
.ok_or(crate::ErrorKind::FSError(
"Could not find valid config dir".to_string(),
))?;
fs::create_dir_all(&config_dir).await.map_err(|err| {
crate::Error::FSError(format!(
crate::ErrorKind::FSError(format!(
"Error creating Theseus config directory: {err}"
))
})?;

View File

@@ -13,6 +13,7 @@ use std::collections::LinkedList;
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
const METADATA_DB_FIELD: &[u8] = b"metadata";
// TODO: store as subtree in database
#[derive(Encode, Decode, Debug)]
pub struct Metadata {
pub minecraft: MinecraftManifest,
@@ -48,6 +49,7 @@ impl Metadata {
})
}
#[tracing::instrument(skip_all)]
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
let mut metadata = None;
@@ -84,7 +86,10 @@ impl Metadata {
db.flush_async().await?;
Ok(meta)
} else {
Err(crate::Error::NoValueFor(String::from("launcher metadata")))
Err(
crate::ErrorKind::NoValueFor(String::from("launcher metadata"))
.as_error(),
)
}
}
}

View File

@@ -8,17 +8,19 @@ mod dirs;
pub use self::dirs::*;
mod metadata;
pub use metadata::*;
mod settings;
pub use settings::*;
pub use self::metadata::*;
mod profiles;
pub use profiles::*;
pub use self::profiles::*;
mod settings;
pub use self::settings::*;
mod users;
pub use self::users::*;
// Global state
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
#[derive(Debug)]
pub struct State {
/// Database, used to store some information
pub(self) database: sled::Db,
@@ -28,52 +30,62 @@ pub struct State {
pub io_semaphore: Semaphore,
/// Launcher metadata
pub metadata: Metadata,
// TODO: settings API
/// Launcher configuration
pub settings: RwLock<Settings>,
/// Launcher profile metadata
pub profiles: RwLock<Profiles>,
pub(crate) profiles: RwLock<Profiles>,
/// Launcher user account info
pub(crate) users: RwLock<Users>,
}
impl State {
#[tracing::instrument]
/// Get the current launcher state, initializing it if needed
pub async fn get() -> crate::Result<Arc<Self>> {
LAUNCHER_STATE
.get_or_try_init(|| async {
// Directories
let directories = DirectoryInfo::init().await?;
.get_or_try_init(|| {
async {
// Directories
let directories = DirectoryInfo::init().await?;
// Database
// TODO: make database versioned
let database =
sled_config().path(directories.database_file()).open()?;
// Database
// TODO: make database versioned
let database = sled_config()
.path(directories.database_file())
.open()?;
// Settings
let settings =
Settings::init(&directories.settings_file()).await?;
// Settings
let settings =
Settings::init(&directories.settings_file()).await?;
// Metadata
let metadata = Metadata::init(&database).await?;
// Launcher data
let (metadata, profiles) = tokio::try_join! {
Metadata::init(&database),
Profiles::init(&database),
}?;
let users = Users::init(&database)?;
// Profiles
let profiles = Profiles::init(&database).await?;
// Loose initializations
let io_semaphore =
Semaphore::new(settings.max_concurrent_downloads);
// Loose initializations
let io_semaphore =
Semaphore::new(settings.max_concurrent_downloads);
Ok(Arc::new(Self {
database,
directories,
io_semaphore,
metadata,
settings: RwLock::new(settings),
profiles: RwLock::new(profiles),
}))
Ok(Arc::new(Self {
database,
directories,
io_semaphore,
metadata,
settings: RwLock::new(settings),
profiles: RwLock::new(profiles),
users: RwLock::new(users),
}))
}
})
.await
.map(Arc::clone)
}
#[tracing::instrument]
/// Synchronize in-memory state with persistent state
pub async fn sync() -> crate::Result<()> {
let state = Self::get().await?;

View File

@@ -12,8 +12,7 @@ use tokio::fs;
const PROFILE_JSON_PATH: &str = "profile.json";
const PROFILE_SUBTREE: &[u8] = b"profiles";
#[derive(Debug)]
pub struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
pub(crate) struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
// TODO: possibly add defaults to some of these values
pub const CURRENT_FORMAT_VERSION: u32 = 1;
@@ -84,15 +83,17 @@ pub struct JavaSettings {
}
impl Profile {
#[tracing::instrument]
pub async fn new(
name: String,
version: String,
path: PathBuf,
) -> crate::Result<Self> {
if name.trim().is_empty() {
return Err(crate::Error::InputError(String::from(
return Err(crate::ErrorKind::InputError(String::from(
"Empty name for instance!",
)));
))
.into());
}
Ok(Self {
@@ -114,11 +115,13 @@ impl Profile {
// TODO: deduplicate these builder methods
// They are flat like this in order to allow builder-style usage
#[tracing::instrument]
pub fn with_name(&mut self, name: String) -> &mut Self {
self.metadata.name = name;
self
}
#[tracing::instrument]
pub async fn with_icon<'a>(
&'a mut self,
icon: &'a Path,
@@ -136,17 +139,20 @@ impl Profile {
Ok(self)
} else {
Err(crate::Error::InputError(format!(
Err(crate::ErrorKind::InputError(format!(
"Unsupported image type: {ext}"
)))
))
.into())
}
}
#[tracing::instrument]
pub fn with_game_version(&mut self, version: String) -> &mut Self {
self.metadata.game_version = version;
self
}
#[tracing::instrument]
pub fn with_loader(
&mut self,
loader: ModLoader,
@@ -157,6 +163,7 @@ impl Profile {
self
}
#[tracing::instrument]
pub fn with_java_settings(
&mut self,
settings: Option<JavaSettings>,
@@ -165,6 +172,7 @@ impl Profile {
self
}
#[tracing::instrument]
pub fn with_memory(
&mut self,
settings: Option<MemorySettings>,
@@ -173,6 +181,7 @@ impl Profile {
self
}
#[tracing::instrument]
pub fn with_resolution(
&mut self,
resolution: Option<WindowSize>,
@@ -181,6 +190,7 @@ impl Profile {
self
}
#[tracing::instrument]
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
self.hooks = hooks;
self
@@ -188,6 +198,7 @@ impl Profile {
}
impl Profiles {
#[tracing::instrument(skip(db))]
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
Ok(Default::default()),
@@ -218,19 +229,23 @@ impl Profiles {
Ok(Self(profiles))
}
#[tracing::instrument(skip(self))]
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
self.0.insert(
profile
.path
.canonicalize()?
.to_str()
.ok_or(crate::Error::UTFError(profile.path.clone()))?
.ok_or(
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
)?
.into(),
Some(profile),
);
Ok(self)
}
#[tracing::instrument(skip(self))]
pub async fn insert_from<'a>(
&'a mut self,
path: &'a Path,
@@ -238,12 +253,14 @@ impl Profiles {
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
}
#[tracing::instrument(skip(self))]
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
self.0.remove(&path);
Ok(self)
}
#[tracing::instrument(skip_all)]
pub async fn sync<'a>(
&'a self,
batch: &'a mut sled::Batch,

View File

@@ -19,6 +19,7 @@ pub struct Settings {
pub custom_java_args: Vec<String>,
pub java_8_path: Option<PathBuf>,
pub java_17_path: Option<PathBuf>,
pub default_user: Option<uuid::Uuid>,
pub hooks: Hooks,
pub max_concurrent_downloads: usize,
pub version: u32,
@@ -32,6 +33,7 @@ impl Default for Settings {
custom_java_args: Vec::new(),
java_8_path: None,
java_17_path: None,
default_user: None,
hooks: Hooks::default(),
max_concurrent_downloads: 64,
version: CURRENT_FORMAT_VERSION,
@@ -40,14 +42,16 @@ impl Default for Settings {
}
impl Settings {
#[tracing::instrument]
pub async fn init(file: &Path) -> crate::Result<Self> {
if file.exists() {
fs::read(&file)
.await
.map_err(|err| {
crate::Error::FSError(format!(
crate::ErrorKind::FSError(format!(
"Error reading settings file: {err}"
))
.as_error()
})
.and_then(|it| {
serde_json::from_slice::<Settings>(&it)
@@ -58,13 +62,15 @@ impl Settings {
}
}
#[tracing::instrument(skip(self))]
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
fs::write(to, serde_json::to_vec_pretty(self)?)
.await
.map_err(|err| {
crate::Error::FSError(format!(
crate::ErrorKind::FSError(format!(
"Error saving settings to file: {err}"
))
.as_error()
})
}
}

View File

@@ -0,0 +1,79 @@
//! User login info
use crate::{auth::Credentials, config::BINCODE_CONFIG};
const USER_DB_TREE: &[u8] = b"users";
/// The set of users stored in the launcher
#[derive(Clone)]
pub(crate) struct Users(pub(crate) sled::Tree);
impl Users {
#[tracing::instrument(skip(db))]
pub fn init(db: &sled::Db) -> crate::Result<Self> {
Ok(Self(db.open_tree(USER_DB_TREE)?))
}
#[tracing::instrument(skip_all)]
pub fn insert(
&mut self,
credentials: &Credentials,
) -> crate::Result<&Self> {
let id = credentials.id.as_bytes();
self.0.insert(
id,
bincode::encode_to_vec(credentials, *BINCODE_CONFIG)?,
)?;
Ok(self)
}
#[tracing::instrument(skip(self))]
pub fn contains(&self, id: uuid::Uuid) -> crate::Result<bool> {
Ok(self.0.contains_key(id.as_bytes())?)
}
#[tracing::instrument(skip(self))]
pub fn get(&self, id: uuid::Uuid) -> crate::Result<Option<Credentials>> {
self.0.get(id.as_bytes())?.map_or(Ok(None), |prof| {
bincode::decode_from_slice(&prof, *BINCODE_CONFIG)
.map_err(crate::Error::from)
.map(|it| Some(it.0))
})
}
#[tracing::instrument(skip(self))]
pub fn remove(&mut self, id: uuid::Uuid) -> crate::Result<&Self> {
self.0.remove(id.as_bytes())?;
Ok(self)
}
pub fn iter(&self) -> UserIter<impl UserInnerIter> {
UserIter(self.0.iter().values(), false)
}
}
alias_trait! {
pub UserInnerIter: Iterator<Item = sled::Result<sled::IVec>>, Send, Sync
}
/// An iterator over the set of users
#[derive(Debug)]
pub struct UserIter<I: UserInnerIter>(I, bool);
impl<I: UserInnerIter> Iterator for UserIter<I> {
type Item = crate::Result<Credentials>;
#[tracing::instrument(skip(self))]
fn next(&mut self) -> Option<Self::Item> {
if self.1 {
return None;
}
let it = self.0.next()?;
let res = it.map_err(crate::Error::from).and_then(|it| {
Ok(bincode::decode_from_slice(&it, *BINCODE_CONFIG)?.0)
});
self.1 = res.is_err();
Some(res)
}
}